0
点赞
收藏
分享

微信扫一扫

单链表的一些 OJ 题

  • C++中,引用结构体时,不需要再写"struct", 此举可以简化掉,平时在 C 语言中常用的 typedef 语句。

1 移除链表中等于 val 的结点(两种方法)

单链表的一些 OJ 题_结点

方法1遍历链表,然后再原链表中“指定位置删除”

方法 2新建链表,将原链表中不等于 val 的结点都尾插到新结点

  • 此操作亦会使原链表跟着新链表改变,因为每个结点都只能有一个指向。而创建“新链表”其实就是创建两个新指针:newhead 和 newtail,让他们随着 pcur 在原链表的遍历,不断尾插,从而得到新的链表。

首先,“创建新链表”,即创建新的头结点和尾结点,然后创建pcur 遍历链表,直到链表为空(链表如果最开始就为空的话,那么就不遍历链表,不进行任何操作,直接返回 newhead)。

在pcur 链表循环内,分为等于 val 和不等于 val 的两种情况

不等于 val时,对新链表“尾插”,同时又分为两种情况,

(1) 对头节点尾插:将 newhead 和 newtail 同时尾插至 pcur

(2)后续节点的尾插:newhead 不动,将 newtail 一直伴随着 pcur 操作。(在头节点插入之后,newhead 和 newtail 都在 head 位置, 所以不需要将 newtail 后移),而现在对 newtail尾插,然后将newtail 后移。

等于 val 时不做任何操作,等待 pcur 循环至下一个节点。

随后跳出循环,此时 newtail ->next 并没有被置空,所以要置空,但是“如果原链表全是 val或者本来就为空,那么新链表将为空,此时不能对空链表解引用,所以 要设置一个 if 语句限制只有 newtail 不为空才能置空”。

//1——删除链表的等于 val 的结点

struct ListNode* removeElements(struct ListNode* head, int val) {
      //方法一:创建新链表,将等于 val 的结点一一尾插到新链表中,最后返回新头结点
    ListNode* newhead = NULL;
    ListNode* newtail = NULL;
    ListNode* pcur = head;
    //定义 pcur 遍历原链表
    while (pcur)
    {
        //不等于 val 时,尾插新链表
        if (pcur->val != val)
        {
            //分为两种情况:头结点插入和其余
            if (newhead == NULL)
            {
                //头结点时,直接将新结点头尾都指向它
                newhead = newtail = pcur;
            }
            else
            {
                newtail->next = pcur;
                newtail = newtail->next;
            }
        }
        pcur = pcur->next;
    }
    //如果原链表全是 val或者为空,那么新链表将为空,此时不能对空链表解引用
    if(newhead != NULL){
        newtail->next = NULL;
    }
    return newhead;
}

2 反转链表


单链表的一些 OJ 题_链表_02

思路 1:创建新链表,然后将原链表节点不断头插到新链表

较为简单,基本上就是普通的头插操作

思路 2:创建三个指针,改变链表指向


单链表的一些 OJ 题_结点_03

(1)创建三个指针:n1,n2,n3, 初始时其中n1 指向 NULL,n2 指向头结点,n3 指向后一个节点。

ListNode* n1,* n2,* n3;
//连续书写三个指针的申请的时候,都要写星号“*”。
n1 = NULL;n2 = head;n3 = n2->next;

(2)然后随之遍历链表,n2 指向 n1,随后三个指针都往后走一位,直到 n2 为空(n1 走到尾节点)时,所有节点的指向均已被改变,此时的 n1 即是新的链表的头结点。

  • 特殊情况:链表为空的情况,此时单独处理“直接返回原链表头结点即可”

单链表的一些 OJ 题_结点_04

//-2——反转链表
struct ListNode* reverseList(struct ListNode* head) {
    if(head == NULL)
    {
        return head;
    }
    ListNode* n1,* n2,* n3;
    n1 = NULL;n2 = head;n3 = n2->next;
    while(n2)
    {
        n2->next = n1;//n2指向改变
        n1 = n2;//n1 后移
        n2 = n3;//n2 后移
        if(n3)//防止结尾n3 走到空,再对空节点解引用
        {
            n3 = n3->next;//n3 后移
        }
    }
    //返回新的头结点
    return n1;
}

3找到并返回链表的中间节点

单链表的一些 OJ 题_链表 OJ 题_05

思路 1:遍历两次链表,第一次找链表总长度,并计算出中间节点位置,第二次遍历直接走到中间节点位置。

思路 2:快慢指针

创建快慢指针,slow 走一步,fast 就走两步,等到 fast 到结尾时,slow 就到中间节点了。

两种情况:

(1 )链表为偶数个,满指针 slow 走到中间时,快指针 fast 刚好走到尾结点,此时 fast->next 为空。

(2 )链表为奇数个,slow 走到中间时,fast 刚好为空。

所以只需要在一个循环内同时限制这两个条件即可。但是循环条件可能会有点迷惑人:如下

while(fast && fast->next)
//此时循环条件要为“且”,代表只要有 fast 或者 fast->next 任意一个为空,就会跳出循环。
//而且不能把 fast->next 放到前头“whlie(fast->next && fast)”,因为不能对空指针解引用,在链表结点数为奇数个时,fast 会先为 NULL,如果此条件放到前头,那么就会对空指针解引用。 
{..................
//快慢指针循环
}

(3)最后直接返回 slow 即可

  • 此时不用单独处理链表为空的情况,因为链表为空时,fast 也为空,直接就跳过循环了。

单链表的一些 OJ 题_链表 OJ 题_06

//3——找到并返回链表中间节点
struct ListNode* middleNode(struct ListNode* head) {
    //创建快慢指针
    ListNode* slow = head,* fast = head;
    //设置循环条件,遍历链表
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

4 合并两个有序链表

大思路:创建新链表,遍历原链表比较大小,谁小就尾插到新链表内。

单链表的一些 OJ 题_链表 OJ 题_07

怎么说呢,都是老思路,与合并两个有序数组如出一辙。只不过“合并数组”只需要创建三个指针:其中两个指针 n1 ,n2分别在两个数组中遍历并进行比较,而 n3 从第一个数组的末尾开始往前遍历,依次将大的数据放入。

而“合并链表”需要创建除两个“比较指针”外的头结点与尾结点,初始时均置为空,由于链表的特性:只能从前往后遍历。所以我们在进行两个链表的比较时,要将较小的链表依次尾插到新的链表内(从前往后遍历,与上述数组相反),直到其中一个链表遍历完,直接将剩余的另一个链表尾插到新链表即可。(原链表均不为空的情况下,当其中遍历循环结束后,则必然出现“一个链表为空,另一个链表不为空”,此时将不为空的链表直接尾插到 newtail 后就行了,不用再让 newtail 往后遍历了,此时只需要返回 newhead,所以 newtail 走到哪里都无所谓,只要节点能接上就行)。

  • 特殊情况:原链表 list1,list2 其中一方或者均为空,需要单独处理,在遍历开始前,如果 list1 为空则返回 list2,反之返回 list1。 这样也可以处理两者均为空的情况,反正两个均为空,返回哪一个都一样。
  • 动态空间申请后,没有初始化的话,系统会自动初始化,而在动态内存释放后,地址没有置空的才会是野指针。

//4——合并两个有序链表
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    //在最初时,处理 list1,2 任意一方为空,或者全空的情况
    if(list1 == NULL)//list1为空,直接把list2传回,当两者均为空时,传回的依旧为空。
    {
        return list2;
    }
    if(list2 == NULL)//同理
    {
        return list1;
    }
    //定义新链表
    ListNode* newhead = NULL,* newtail = NULL;
    //定义两个指针
    ListNode* l1 = list1,* l2 = list2;
    while(l1 && l2)
    {
        if(l1->val <= l2->val)
        {
            //l1 尾插到新链表
            if(newhead == NULL)//当首次尾插时,新链表头结点为空
            {
                newhead = newtail = l1;
                l1 = l1->next;
            }
            else//非头结点插入时
            {
                newtail->next = l1;
                newtail = newtail->next;
                l1 = l1->next;
            }
        }
        else
        {
            //l2 尾插到新链表
            if(newhead == NULL)//当首次尾插时,新链表头结点为空
            {
                newhead = newtail = l2;
                l2 = l2->next;
            }
            else//非头结点插入时
            {
                newtail->next = l2;
                newtail = newtail->next;
                l2 = l2->next;
            }
        }
    }
    //正常运行遍历后,l1 与 l2 只要任意一方为真,另一方就为空
    //直接将不为空的链表继续尾插即可。但是其无法处理最初时链表为空的情况,因为无法对 newtail 空链表解引用。
    if(l1)
    newtail->next = l1;
    if(l2)
    newtail->next = l2;
    return newhead;
}

代码冗余的改善

单链表的一些 OJ 题_链表_08

如图,在l1,l2 尾插的过程,因为要分为对空链表和非空链表的尾插(对空链表的尾插要考虑头节点),从而显得代码冗余,加上 l1 和 l2 要执行两次重复操作,代码更冗余了。而改善的情况可以是(1)将此重复代码封装成函数,(2)也可以是修改代码逻辑。修改如下:

(1)首先考虑,两个原链表遍历的时候都需要考虑新链表非空和空的情况,导致代码冗余,原本的逻辑是“将本来为空的 newhead 和 newtail 均指向头节点后,再进行正常的尾插”。

(2)现在我们改变逻辑,直接为newhead 和 newtail 申请同一片动态空间, 让 newhead 成为“哨兵位”,然后遍历时,直接让原链表尾插到 newtail 后,并让newtail 后移,这样每一步遍历的操作就都相同了。

(3)当然了,最后尾插完,就不需要 newhead 了,这里的 newhead 并不是真正意义上的“newhead”,它只不过是一个“用来指向新头节点的一个临时节点”,用完之后就要释放掉内存并且置空了。

(4)注意:最后返回的是 newhead->next,而我们要在函数返回之前把 newhead 释放掉呀!?所以要创建个临时变量ret保存一下 newhead ,最后返回 ret->next 。

单链表的一些 OJ 题_链表_09

//4——2 改善上述代码冗余现象
//为 newhead 和 newtail 申请动态空间,让其成为临时节点,以供尾插,去掉头节点单独处理的情况。
struct ListNode* mergeTwoLists2(struct ListNode* list1, struct ListNode* list2) {
    //在最初时,处理 list1,2 任意一方为空,或者全空的情况
    if(list1 == NULL)
    {
        return list2;
    }
    if(list2 == NULL)
    {
        return list1;
    }
    //定义新链表
    ListNode* newhead,* newtail;
    //为其申请动态空间,让其成为一个临时节点,以供后续插入
    newhead = newtail = (ListNode*)malloc(sizeof(ListNode));
    //定义两个指针
    ListNode* l1 = list1,* l2 = list2;
    while(l1 && l2)
    {
        if(l1->val <= l2->val)
        {
                newtail->next = l1;
                newtail = newtail->next;
                l1 = l1->next;
        }
        else
        {
                newtail->next = l2;
                newtail = newtail->next;
                l2 = l2->next;
        }
    }
    //正常运行遍历后,l1 与 l2 只要任意一方为真,另一方就为空
    //直接将不为空的链表继续尾插即可。但是其无法处理最初时链表为空的情况,因为无法对 newtail 空链表解引用。
    if(l1)
    newtail->next = l1;
    if(l2)
    newtail->next = l2;
    //末尾要记得释放空间并置空
    //释放之前要先存起来,因为返回值要用
    ListNode* ret = newhead->next;
    free(newhead);
    newhead = NULL;
    //最后返回临时节点的下一个指针,即新的头指针
    return ret;
}

5 链表的分割


单链表的一些 OJ 题_链表_10

思路 1:创建一个新链表,将原链表遍历两次,第一次找小于 x 的节点,第二次找大于等于 x 的节点,依次放入新链表中

单链表的一些 OJ 题_头结点_11

粗略思路:遍历两遍,先找小的,再找大的,分批依次放入新链表,新链表临时节点申请动态空间,直接后续插入,最开始时考虑一下链表为空的情况,最后把新链表临时节点的下一位节点保存一下,然后释放并置空,返回保存的临时节点的下一位节点

思路 2:创建两个新链表:lesshead,morehead。遍历原链表,小于 x 的节点放入 lesshead,大于等于x 的节点放入 morehead,最后将 lesshead 和 morehead 连接起来。

单链表的一些 OJ 题_头结点_12

(1)首先创建 lesshead /lesstail和 morehead/lesstail 两个临时链表(需要申请动态空间),然后 pcur遍历原链表,当<x 就尾插到”小链表“,>=就尾插到“大链表”,直到原链表pcur 遍历结束。

(2)将大链表尾插到小链表后://lesstail->next = morehead->next;。此时并没有万事大吉,因为大链表的尾结点并没有置空,如果不置空直接运行就会陷入死循环。

(3)最后将小链表的头节点保存一下,然后将大小链表的头结点均释放并置空。

//5——链表分割
ListNode* partition(ListNode* pHead, int x) {
        //创建大小链表,并申请动态空间
        ListNode* lesshead,* lesstail;
        lesshead = lesstail = (ListNode*)malloc(sizeof(ListNode));
        ListNode* morehead,* moretail;
        morehead = moretail = (ListNode*)malloc(sizeof(ListNode));
        //遍历原链表,分别插入大小链表
        ListNode* pcur = pHead;
        while(pcur)
        {
            //插入小链表
            if(pcur->val < x)
            {
                lesstail->next = pcur;
                lesstail = lesstail->next;
            }
            //插入大链表
            else
            {
                moretail->next = pcur;
                moretail = moretail->next;
            }
            pcur = pcur->next;
        }
        //链表遍历结束后,要将新链表的尾节点的下一位置空
        moretail->next = NULL;
        //将大小链表连接起来
        lesstail->next = morehead->next;
        //最后先把新链表的头节点保存下来,然后再释放并置空大小节点的动态空间
        ListNode* ret = lesshead->next;
        free(lesshead);
        lesshead = NULL;
        free(morehead);
        morehead = NULL;
        return ret;
    }

6 链表的回文结构

  • 回文结构:就是轴对称图形。如回文数字“1221”,回文字符串“abcba”。
  • bool 类型的头文件为“#include<stdbool.h>”。

单链表的一些 OJ 题_链表 OJ 题_13

思路 1:将链表中的数据遍历依次存入另一种数据结构“数组”中,这样就可以利用数组的可随机寻址的特性,直接双指针判断回文结构。

//6——链表的回文结构
//方法一:将链表中的数据插入到数组中进行双指针判断回文结构
//但是此方法要申请数组空间,空复较高,为O(n)
bool chkPalindrome(ListNode* A) {
        //定义数组,以供后续用来存放数组
       int arr[900];//可以初始化,也可以不初始化,因为后续会进行赋值。
       //遍历原链表,将链表数据存入数组中
        ListNode* pcur = A;
        int i = 0;
        while (pcur) {
            arr[i++] = pcur->val;
            pcur = pcur->next;
        }
        //然后在数组中用"双指针"判断回文结构
        int left = 0;
        int right = i-1;
        while (left < right) {
            if(arr[left] != arr[right])
            {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }

  • 但是这种解法,时间复杂度虽然为 O(n),但是申请了 n 个数组的空间,空间复杂度为 O(n),较为暴力和复杂,不推荐。

思路 2:利用快慢指针,找到中间节点位置,然后创建新链表(可申请动态空间),将原链表后半段直接头插到新链表中(逆置链表),然后用两指针遍历两链表进行比较,来判断回文结构。

//方法二:找中间节点,将后半点逆置,然后与原链表对比。最优方案。
bool chkPalindrome2(ListNode* A) {
        //1 找到中间节点
        ListNode* mid = middleNode(A);
        //2 逆置后半段链表
        ListNode* newNode = reverseList(mid);
        //比较链表
        ListNode* l1 = A,* l2 = newNode;
        while (l2) {
            if(l1->val != l2->val)
            {
                return false;
            }
            l1 = l1->next;
            l2 = l2->next;
        }
        return true;
    }

  • 时间复杂度 O(n),空间复杂度 O(1)。虽然新建指针较多,但是只申请了一个新结点。具体思路(最优思路):

(1)按照上述 题型 3 中思路,找中间节点

(2)将中间节点后的链表“反转”。快慢指针遍历指针找中间节点时,因为有链表奇与偶的情况,那么要怎么考虑呢,其实只用一种处理方法即可:直接从 slow 开始(包括slow 指针),将后面的节点全部头插到新链表(这样就会实现后半链表的倒置了)。

链表为奇时,恰好被分割成两相等的部分,正好比较。但是链表为偶的时候,会出现中间节点被多插入到了新链表里,其实我们不用管它,因为它在新链表结尾,所以我们可以直接比较 slow前头的 节点;

单链表的一些 OJ 题_结点_14

单链表的一些 OJ 题_头结点_15

(3)比较两个链表。新链表创建后,用l1 ,l2遍历两个链表进行比较,而结束条件直接让l1 或者 l2 中任意一个指针走到空就跳出循环://while(l1 == slow)

单链表的一些 OJ 题_结点_16

循环中万一两者不相等,直接返回false。如果全部相等,则跳出循环,最后返回 true即可。

思路 3:把整个原链表直接逆置到新的链表里,然后直接进行比较两个链表。较为简单,并且也符合时间和空间复杂度。

  • 但是!你以为就这么简单吗?其实这里的逆置链表如果用头插的话,那么在新的链表插入之后,原链表就会消失,这还怎么比较呢!?所以还要给每个新节点都申请新的动态空间,这样的话,空间复杂度又成 O(n)了。


7——返回链表的倒数第 k 个节点中的数据

  • 很简单:先逆置,然后返回第二个节点的数据。

//7——返回倒数第 k 个节点中的数值
int kthToLast(struct ListNode* head, int k) {
    //空链表直接传空
    if(head == NULL)
        return 0;
    //将链表逆置一下
    ListNode* newNode = reverseList(head);
    //直接找第二个节点
    //定义 pcur 等于新链表的头结点
    ListNode* pcur = newNode;
    for(int i = 1;i < k;i++)
    {
        pcur = pcur->next;
    }
    return pcur->val;
}

8——相交链表

单链表的一些 OJ 题_链表_17

具体思路:

  1. 判断两个链表是否相交的一个思路:遍历两个链表,若两个链表的尾节点相同,则两者一定为“相交链表”。
  2. 找到相交节点:

情况一:两个链表结点数量相同(如下补齐后的图),定义两个指针遍历比较两个链表,即可找到相交节点。

单链表的一些 OJ 题_头结点_18

情况二:结点数数量不相同(如原图),此时就要想办法让其结点数相同,从而才能进行比较,

(1)分别定义两个指针,遍历两个链表,算出两链表节点数量的差值gap

利用 abs 函数计算差值gap的绝对值。(为了确定谁是长链表)先假设其中一个链表为长链表,并定义长链表指针 longlist,指向假设的长链表,定义短链表指针 shortlist 指向短链表,利用若 if(longlist < shortlist),则把长短链表指针调换一下。

  • int abs(int x)函数:计算括号后的绝对值

(2)让节点数多的那一个链表,先走“差值gap”个节点

利用 while(gap--), 将长链表走 gap 个节点,从而让长链表与短链表一样长。当然如果本来就一样长,那么这个语句就会被跳过。

(3) 然后再进行两个链表同步遍历,进行比较,从而找到相交节点。

既然两个链表已经同长了,那么怎么判断两链表是否为相交链表呢,其实直接用 while 遍历比较两链表即可,如果有相同节点,则直接返回该结点,若没有则等到遍历结束,返回 NULL。

//8——相交链表
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    //1.遍历两个链表,找两链表差值gap
    int sizeA = 0,sizeB = 0;
    ListNode* l1 = headA,* l2 = headB;
    while(l1)
    {
        sizeA++;
        l1 = l1->next;
    }
    while(l2)
    {
        sizeB++;
        l2 = l2->next;
    }
    int gap = abs(sizeA - sizeB);
    //2.长链表先走 gap 个节点,使两链表同长
    //找到长节点
    ListNode* longlist = headA;
    ListNode* shortlist = headB;
    if(sizeA < sizeB)
    {
        longlist = headB;
        shortlist = headA;
    }
    //让长节点走 gap 个节点
    while(gap--)
    {
        longlist = longlist->next;
    }
    //3.同步遍历比较两链表
    while(longlist && shortlist)
    {
        if(longlist == shortlist)
        {
            return longlist;
        }
        longlist = longlist->next;
        shortlist = shortlist->next;
    }return NULL;
}

9——环状链表

环状链表:尾节点的 next 指针不为空即为带环链表,尾节点也可以指向自己。

单链表的一些 OJ 题_链表_19

思路:快慢指针:慢指针走一步,快指针走两步,若二者相遇,则为带环链表,若有一方为 NULL,则为普通链表。

单链表的一些 OJ 题_链表_20

//9——环状链表
//较为简单,重在理解快慢指针为何会相遇,快指针走2,慢指针走1
bool hasCycle(struct ListNode *head) {
    //直接定义快慢指针
    ListNode* fast = head,* slow = head;
    //不断遍历,直至相遇,返回 true,若 fast 或者 fast->next 为空,则跳出循环
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {
            return true;
        }
        
    }
    return false;
}

  • 为什么进入链表的“环”后,快慢指针一定会相遇呢?

假设进入环后,快慢指针的距离为 N,那么慢指针每走一步,快指针就走两步,在不断的循环中,快指针一直在一步一步的追慢指针,而他们又都在环中无法逃离,所以没循环一遍,二者的距离就会减一,于是 N-1,N-2,N-3....... 直到距离为0,二者相遇。

单链表的一些 OJ 题_结点_21

思考:

  • 如果慢指针每走一步,快指针走3,4,5.....n 步,那么二者还会相遇吗?

一定会相遇!!!

设二者的距离为 N,慢指针走一步,快指针走三步,那么二者的距离就会不断缩小 2 个,就是让 N 不断减 2,距离N-2,N-4....到 N-2i(i 为整数),此时会有两种情况,

(1)N 为偶数,则两指针会恰好相遇。

(2)N 为奇数,则第一圈时二者的最近距离会变成“-1”,即快指针会在最接近慢指针的时候,走到了慢指针的后面一个节点,因此第一圈会出现“擦肩而过”的现象,那么第二圈一定会相遇吗?也是分为两种情况,设环的长度为 C,那么二者的距离为 C-1,:

(a)如果 C-1 为偶数,那么第二圈必定会相遇。

(b)如果 C-1 也为奇数,那么第二圈还是会出现快指针比慢指针多走了一步,等于无限循环了,那么一定不会相遇。

那么总结:若N 为奇数,C 为偶数,则二者一定不会相遇。(什么!!?????)

但是!从下图 2 中的公式的角度看,C 和 N 只能“同偶”或者“同奇‘’,当 N 是奇数的时候,C 只能是奇数,那么 C-1 只能是偶数,所以上述(2)(b)二者不会相遇的情况是不存在的

单链表的一些 OJ 题_结点_22


单链表的一些 OJ 题_头结点_23

上图 2 图示公式讲解:fast 走三步,slow 走一步,那么很显然 fast=3slow,而当 fast与 slow 的位置处于上图一的位置时,slow 恰好等于 L,fast等于 L +之前走的圈数乘以圈的长度xC + slow 与 fast 当前位置的距离。那么此式子可变换成如图二,左边是偶数,那么右边也一定是偶数,右边两数相减要得到偶数,只能是“同奇或者同偶”,于是 N 为奇时 C-1 就不可能为奇;

  • 于是当 fast 走三步时,也一定会追上 slow,也是可以判断链表是否成环的。
  • 虽然证明了无论快指针走几步都会在带环链表中相遇慢指针,但是在编写代码的时候会有额外步骤的引入,所以设计到快慢指针的算法题一般都是默认“快指针走两步,慢指针走一步”。


10——环形链表——2(寻找入环点)


单链表的一些 OJ 题_链表 OJ 题_24

1.大致思路:给出条件“快慢指针相遇后的相遇点,到入环点的距离,与头结点到入环点的距离相等”(稍后证明),那么只需要 首先利用快慢指针判断是否为带环链表,如果是,则此时快慢指针已经相遇,再定义一个指针从头节点开始往后遍历,slow 或者 fast 任意一个指针也同时在环内从相遇点开始往后遍历,两者相遇点即是入环点。

(1)首先判断是否为带环链表,若为带环链表,这次是快慢指针已经相遇

(2)然后创建一个指向头节点的指针pcur,让slow 在环内从相遇点往后遍历, 同时 pcur 同步往后遍历,二者一定会相遇,相遇后即是入环节点,返回 slow 或者pcur 任意一指针即可。

//10——环状链表 2——找入环点
struct ListNode *detectCycle(struct ListNode *head) {
    //快慢指针判断是否为带环
    ListNode* fast = head,* slow = head;
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        //如果相遇了,则必定可以找到入环点
        if(slow == fast)
        {
            ListNode* pcur = head;
            while(pcur != slow)
            {
                pcur = pcur->next;
                slow = slow->next;
            }
            return pcur;
        }
    }
    //若不为环,则返回空
    return NULL;
}

2.证明“相遇点与头结点,到入环点的距离相等”(L = R - X):

单链表的一些 OJ 题_结点_25


如果要证明此例,则从图中可知,要使 L 等于 R-X,而依旧是利用和上一例相同的思路,来证明这个式子,即“快指针等于慢指针的二倍”,经过式子的演变,最后得出L = nR-X(n 为走的圈数),此式也可演变成 L = (n-1)R + (R - X),其实在环中,快指针无论走了多少圈,都不影响相遇点和入环节点的距离;


11——随机链表的复制

单链表的一些 OJ 题_结点_26


单链表的一些 OJ 题_结点_27

单链表的一些 OJ 题_链表_28

单链表的一些 OJ 题_结点_29

大致思路:先在原链表中挨个复制每个节点,并连接在其后,以供后续改变新节点的random 指针,然后挨个改变新节点的random 指针:一般情况下,把新节点 random指向旧节点的 random 的 next 指针,当random 指向空时,直接让其指向 random。最后断开链表即可复制完成。

  • 需要创建新建节点的函数,也可单独把复制链表的操作写成一个函数,提高代码简洁性,可读性。
  • 也需要处理特殊情况:链表为空是返回 NULL;

(1)在原链表上复制节点:需创建两个指针,pcur 遍历原链表,Next 指针遍历 pcur 的下一个节点(用来插入新节点),同时申请新节点 newnode插入到 Next 处。然后 pcur 往后遍历至原链表下一个节点,Next 走到原链表下一个节点。

(2)置 random 指针:需要三个指针遍历操作链表复制完成后,让 pcur 指向头节点,创建 copy 指针指向新节点,Next 指针指向下一个原节点。

  1. 一般情况下,将 copy 中 random 指向 pcur 中 random 的 next,但是当原链表中 random 为空时,不用改变 copy 中 random 即可,因为 random 在复制创建的时候即为空。
  2. 循环内先把 Next = copy->next;( 在开始先把 Next 置于后一位 ),然后进行 a. 操作(置 random 指针),然后copy = Next->next;(copy 在新链表中后移),pcur = Next;(即可让 pcur 后移),不需要再操作一次 Next 后移,因为会在下一次循环开始时后移。
  3. 循环直到 pcur 为空(结尾 pcur 会和 Next 处于同一位置)结束。

(3)断开链表:创建三个指针,pcur 指向头节点, newtail 和 newhead 初始均置于第一个新节点。

  1. 先将pcur 后移,再将 newtail 指向 pcur->next, 然后 newtail 后移一位(虽然新链表复制完成了,但是原链表被破坏了)
  2. 直至 pcur 为空循环结束,复制完成新节点的 next 本来就是 NULL,所以不用做出改变。

//申请新节点
ListNode* BuyNode(int x)
{
    ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
    newnode->val = x;
    newnode->next = newnode->random = NULL;
    return newnode;
}
//复制新链表
void AddNode(ListNode* phead)
{
    ListNode* pcur = phead;
    //从头开始遍历
    while(pcur)
    {
         ListNode* copy = BuyNode(pcur->val);
         //先把 Next 指向原链表的下一位
        ListNode* Next = pcur->next;
        //然后用三个指针把 copy 插入到原链表
        pcur->next = copy;
        copy->next = Next;
        //pcur 后移
        pcur = Next;
    }
}
//随机链表的复制操作函数
struct Node* copyRandomList(struct Node* head)
{
    //特殊情况:链表为空,直接返回 NULL;
    if(head == NULL)
    {
        return NULL;
    }
     //复制链表
    AddNode(head);
     //置 random
     ListNode* pcur = head;
     while(pcur)
     {
        //创建三指针遍历链表
        ListNode* Next = pcur->next->next;
        //Next 保存下一个节点,以供 pcur 后移
        ListNode* copy = pcur->next;
        //当原链表中 random 不为空时才能对其解引用并置random 到新节点,否则不进行处理,因为其原本即为 NULL
        if(pcur->random != NULL)
        {
            copy->random = pcur->random->next;
        }
        pcur = Next;
     }
     //断开连接
     pcur = head;
     //创建新链表的头尾节点
        ListNode* newhead,* newtail;
        newhead = newtail = pcur->next;
        //当 pcur 走到尾节点时,新旧链表已被断完,新链表的尾节点本来就是 NULL,所以不用多余操作。
     while(pcur->next)
     {
        //破坏原链表,连接新链表
        pcur = newtail->next;
        newtail->next = pcur->next;
        newtail = newtail->next;
     }
    return newhead;
}

举报

相关推荐

0 条评论