Part 2 新手小白数据结构刷题的理解

目录

2.单链表

(5)Leetcode 21题

(6)Leetcode 02.04题​

(7)Leetcode 0.27题

(8)Leetcode 023题

(9)Leetcode 141题

(10)Leetcode 142题

(11)Leetcode 138题


2.单链表

(5)Leetcode 21题

思路一:(简单粗暴)(迭代)

因为两个链表都是升序的,所以给了我们很大的便利,既然需要进行比较,就需要两个指针l1,l2,分别在两个链表上一步一步遍历。l1->val和l2->val进行对比,谁更小就把它尾插到一个新的链表即可,然后该指针往后走,若相等或者更大的一方不动。一定有一个链表先走完,然后把另一个链表的遍历指针继续尾插到新链表就可以了。遍历结束条件是两个链表都为空。

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
   //定义两个分别指向两个链表的指针,还有一个新的哨兵位头结点和往后连接的tail指针
   struct ListNode* l1 = list1;
   struct ListNode* l2 = list2;
   struct ListNode* newhead = (struct ListNode*)malloc(sizeof(struct ListNode));
   struct ListNode* tail = newhead;


    //一般链表题目都要先判断是否为空
    if(list1==NULL)
        return list2;
    if(list2==NULL)
        return list1;
    
    //两个指针依次遍历两个链表
    while(list1!=NULL&&list2!=NULL)
    {
        if(list1->val<list2->val)
        {
            tail->next = list1;
            tail = list1;
            list1 = list1->next;
        }
        else
        {
            tail->next = list2;
            tail = list2;
            list2 = list2->next;
        }
    }

    //一定有一个链表先走完,另一个链表还剩下一部分,剩下部分继续尾插
    if(list1==NULL)
    {
        tail->next = list2;
    }
    if(list2==NULL)
    {
        tail->next = list1;
    }

    //返回的是哨兵位头结点的下一个结点
    //哨兵位头结点(没有有效数据,单纯为了让数据能够进行尾插,假如没有哨兵位,让头结点head=NULL,那么head->next出现访问空指针问题了,也无法进行连接)
    return newhead->next;
}

分析时间复杂度O(N+M)(两个指针都要遍历完自己的链表,tail指针还要遍历到完两个链表的完整合并,所以时间复杂度为O(N+M+N+M)N,M是两个链表各自的长度),空间复杂度O(1)(只需要两个链表,常数个空间,newhead下的链表其实是原先两个链表结点相连的链表,所以没有额外的第三个空间)。按理来讲,因为单链表的结构有next的递归性,所以可以采用递归,便有了思路二。

思路二:(递归)

之前的递归因为只有一个链表,只需要一个递归函数递归即可,这里有两个链表都需要遍历到底,所以需要两个递归函数,同于之前的三大部分(入栈结束条件+递归函数+出栈结束条件)

代码二:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
        //入栈结束条件
        if (list1 == NULL) 
        {
            return list2;
        } 
        else if (list2 == NULL) 
        {
            return list1;
        }

        //出栈结束条件和递归函数写在一起了
        else if (list1->val < list2->val) 
        {
            list1->next = mergeTwoLists(list1->next, list2);
            return list1;
        }
        else
        {
            list2->next = mergeTwoLists(list1, list2->next);
            return list2;
        }
}

分析:时间复杂度O(N+M)空间复杂度O(N+M)(两个链表都递归到各自长度的深度了)。这样空间复杂度还更高,也不好想,看起来很简单,自己实现的时候总会丢东丢西,所以个人觉得不熟悉递归,用循环去迭代也没问题的。

总结:1.递归的实现主要注意三大部分的实现:入栈结束条件,递归函数的实现,出栈结束条件;入栈结束条件写在递归函数上方,出栈条件一般在递归函数下面,递归函数和出栈结束条件有时可以包含,比如题21。

(6)Leetcode 02.04题

思路一:(简单粗暴)(迭代)(穿针引线)

给了一个单链表,要求链表中小于x的结点相对顺序不改变放在左边,等于或大于x的结点放在右边。不管三七二十一,既然要比较就需要遍历,而且不是结点之间互相比较(至少需要两个指针),目前至少需要一个指针cur。如上图,cur遍历1,小于3,要把它放到链表左边,所以需要一个左边存放val小于x的指针left,left初始值如果给head,那么后面的赋值操作无法进行,如:如果写成less = head,如果第一个结点比x大,那么left要找到第一个比x小的值才能进行赋值,所以这里采用了两个无数据的哨兵位头结点lesshead和morehead,分别用来进行连接比x小的结点和比x大的结点,这里就体现了单链表有无哨兵位的优缺点了。(很多解题思路只是单纯给出了两个哨兵位,并没有解释为什么和如果没有哨兵位能不能解题的问题,这里博主试了一下,没有哨兵位是不方便的)

cur遍历,遇到val小于x的尾插到lesshead,遇到val等于大于x的尾插到morethead,最后遍历完,把两个链表连接起来,返回lesshead->next即可。

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* partition(struct ListNode* head, int x){
    //定义cur遍历当前链表,
    //lesshead,morehead是两个不存放数据,单纯方便为尾插的哨兵位
    //less,more是记录两个链表结点当前结点位置
    //less,more的next置空是为了避免野指针

    struct ListNode* cur = head;
     struct ListNode* lesshead = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* less = lesshead;
    less->next = NULL;
    struct ListNode* morehead = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* more = morehead;
    more->next = NULL;

    //cur遍历
    while(cur!=NULL)
    {
        if(cur->val<x)
        {
            less->next = cur;
            less = cur;
            cur = cur->next;
        }
        else
        {
            more->next = cur;
            more = cur;
            cur = cur->next;
        }
    }

    //遍历完了连接两个链表,并把最后一个结点的next连到NULL
    less->next = morehead->next;
    more->next = NULL;
    return lesshead->next;
}

分析时间复杂度O(N)(cur遍历完整个链表,less走完小于x的结点,more走完大于等于x的接结点,所以总的时间复杂度为O(N+N)),空间复杂度O(1)(有人会说,不是有lesshead和morehead的两个新链表吗?其实这里要注意,这两个哨兵位只是为了方便尾插而已,大家可以跟着代码走画画图,只是在原来的链表上进行连接而已,没有复制出新的结点,也就没有用额外的空间,两个头结点的空间只是常数级别)。能不能用递归呢?

思路二:(递归)(引用大佬题解)

代码二:(点击这里看原文

(7)Leetcode 0.27题

思路一:(简单粗暴)

先不管三七二十一,先cur遍历,遇到第一个结点,要和最后一个结点的值相同。单链表不能随机访问,所以首先要找到尾,用变量length记录长度,然后再走到最后一个结点判断是否相同,第二个结点又要遍历到length-1的结点处(倒数第二个结点),如此反复。显然这样遍历消耗的时间太多了,相对好一点的办法我们可以用一个数组把遍历的每个结点的值存储下来,因为数组有随机访问的优势,可以前后访问,我们便让数组帮我们解决。

先要指针cur遍历一遍数组,用遍历length记录链表长度(当然,我们也可以直接开辟比题目要求链表最大长度更大的数组,这里题目要求是链表长度小于10^5),然后再创建length大小的数组,这时再去遍历链表才能把结点的值存到数组内。存完后,只需要变量begin,end两头进行比较下去即可。

代码一:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 

bool isPalindrome(struct ListNode* head){
    if(head==NULL)
        return NULL;

    //先定义指针cur遍历一遍链表,用length记录长度,好让数组能开辟多大空间
    struct ListNode* cur = head;
    int length = 0;
    while(cur!=NULL)
    {
        cur = cur->next;
        ++length;
    }
    
    //开辟length大小数组空间,并把链表结点值放入数组
    int* nums = (int*)malloc(sizeof(int)*length);
    int i = 0;
    cur = head;
    while(cur!=NULL)
    {
        nums[i++] = cur->val;
        cur = cur->next;
    }

    //用begin,end变量遍历一遍数组
    int begin = 0;
    int end = length-1;
    while(begin<end)
    {
        if(nums[begin]!=nums[end])
        {
            return false;
        }
        else
        {
            ++begin;
            --end;
        }
    }
    return true;
}

分析:很显然很多链表的题目都可以这样放到数组做,这是最好想到也是最好写代码的情况。时间复杂度O(N)(cur遍历两遍链表,begin和end遍历的时候同时进行判断,判断进行了N/2,也就是一半的数组长度,所以总的时间复杂度为O(5/2N)),空间复杂度O(N)(开辟了length长度的数组)。我们能不能不开辟新的空间呢?只在当前链表进行操作呢?这里要用到之前链表的两个操作,我们先想一下,首先回文结构一定是对称的,有偶数个结点,如1 2 3 3 2 1。而之前有个操作叫做找到单链表的中间结点,我们可以找到这里的第四个结点也就是第二个3,如果结点数据是这样就好了1 2 3 1 2 3,这里又根据前面的反转操作,我们可以找到中间结点,然后把后面的链表进行反转,这样我们就可以用两个指针去遍历判断了,不创建额外的空间。这是思路二。

思路二:(中间结点+反转)

把之前写过的找中间结点(Leetcode 876题),反转链表(Leetcode 206题)的题解代码搬过来,找到中间结点,反转,然后就和数组一样方便了。

代码二:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

 //反转链表的接口
struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* cur = head;
    struct ListNode* newhead = NULL;
    while(cur!=NULL)
    {
        struct ListNode* next = cur->next;
        cur->next = newhead;
        newhead = cur;
        //迭代
        cur = next;
    }
    return newhead;
}

//找中间结点的接口
struct ListNode* middleNode(struct ListNode* head) {
    //快慢指针
    struct ListNode *fast,*slow;
    fast = head; slow = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}


bool isPalindrome(struct ListNode* head){
    if(head==NULL)
        return NULL;
   //找中间结点
   struct ListNode* MiddleNode =  middleNode(head);

   //反转中间结点后面的链表,返回新的newhead
   struct ListNode*  newhead = reverseList(MiddleNode);

   //两个指针去遍历判断结点的值是否相同即可
   struct ListNode* cur = head;
   struct ListNode* newcur = newhead;
   while(cur!=NULL && newcur!=NULL)
   {
    if(cur->val!=newcur->val)
    {
        return false;
    }
    else
    {
        cur = cur->next;
        newcur = newcur->next;
    }
   }
   return true;
}

分析:时间复杂度O(N),空间复杂度O(1)

(8)Leetcode 023题

要求:1.如果是相交链表返回第一个相交结点,如果不是返回NULL

           2.不能改变链表的原始结构

思路一:(简单粗暴)(存入数组)

最好想的办法一定是把结点的指针存入数组,然后利用数组进行比较即可。这里代码就不写了。

思路二:(双指针同步走到相交处)

1.首先要知道什么是相交链表,也就是两个链表在某一处有相同的结点(不是相同的结点值val,而是完全相同的结点,其指针也是相同的)而且根据单链表结构只有一个指针next可知,后面的结点都是相同的,不会出现像倒着的X的形状的情况或如下图情况

2.只要知道有一个相同的结点就能说明是相交链表,这个结点可以是第一个相交的结点,也可是其后面的任意一个结点,比如最后一个结点,那么我们是否可以分别用curA->next->next==NULL,curB->next->next==NULL作为遍历结束条件,直接看curA和curB是否相同,其他情况(链表为空或者两个链表都只有两个结点)再单独列出来,这样的解法很好用来判断是否是相交链表,但是我们还需要返回第一个结点,所以我们采用别的思路,得用第一个相同结点,这样在判断是相交链表的同时也可以返回。

3.如何找到第一个相同的结点呢?定义两个指针curA,curB分别用来遍历两个链表,curA遍历第一个结点时,curB去遍历另一个链表的所有结点,然后依次进行比较,遇到第一个结点与curA相同就说明是相交链表,并且curB此时就是相交链表的第一个结点,如果没有遇到,此时curA再往后走一步,curB再次去遍历另一个链表,如此直到curA遍历完,如果始终都没有curA等于curB的情况,说明两个链表不相交。但是这样的操作显然非常消耗时间,原因在于curA和curB在两个链表的相对位置上是异步的, 如下图(链表A和链表B的长度不一样的时候,curA和curB如果同时走,curA走两步到4,而curB走3步到4)

4.我们最希望看到的结果就是curA和curB同时走到4(同步),这样就不要像之前一样,curA每走一步就要停下来让curB走完另一个链表。这里有个神奇的操作:让长的链表先走完两个链表长度差的步数,然后两个指针再一起走,每走一步就比较一下,直到相同即可。

代码二:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */



struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    //headA=NULL或者headB=NULL,两者有一个链表为空,也表示相交于NULL处
    if(headA==NULL)
        return headB;
    if(headB==NULL)
        return headA;
    if(headA==headB)
        return headA;

    //curA,curB遍历出两个链表的长度
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;
    int lengthA = 0;
    int lengthB = 0;
    while(curA!=NULL)
    {
        ++lengthA;
        curA = curA->next;
    }
    while(curB!=NULL)
    {
        ++lengthB;
        curB = curB->next;
    }

    //用distance表示链表长度差的绝对值
    //长的链表先走distance步,然后两个链表一起走,同时进行比较
    int distance = abs(lengthA-lengthB);
    curA = headA;
    curB = headB;
    if(lengthA>lengthB)
    {
        while(distance>0)
        {
            curA = curA->next;
            --distance;
        }
        while(curA!=NULL && curB!=NULL)
        {
            if(curA==curB)
            {
                return curA;
            }
            curA = curA->next;
            curB = curB->next;
        }
    }
    else
    {
        while(distance>0)
        {
            curB = curB->next;
            --distance;
        }
        while(curA!=NULL && curB!=NULL)
        {
            if(curA==curB)
            {
                return curA;
            }
            curA = curA->next;
            curB = curB->next;
        }
    }

    //这里不能写if(curA==NULL && curB==NULL) return NULL;//到底为什么,博主也不清楚,只是leetcode会报错,😓,有知道的站友评论区说一下
    return NULL;

}

分析:时间复杂度O(N+M)(curA,curB先遍历一遍各自链表一次算长度,然后再遍历一遍去比较,总的时间复杂度为O(2N+2M)),空间复杂度O(1)(没有额外空间)目前已经比较优了,单链表的题目因为其next指针,所以基本可以采用递归,这里博主先把别的大佬的题解搬过来(自己还没学java,看不懂,日后再看看😭)

思路三:(递归Java)(引用大佬题解)(已获转载认可)

代码三:

(9)Leetcode 141题

思路一:(粗暴)

上来我们先想着数组或者遍历能不能暴力求解,单链表带环,如果去遍历,会发现找不到遍历结束的条件,因为没有NULL,没有真正意义上的尾结点;所以想把结点指针存入数组也会因为无法跳出循环而无法进行,当然我们也可以开辟动态数组,每存入一个结点,就把这个结点和以前的所有结点进行比较,有相同的就直接跳出循环,这里代码就不写了。

思路二:(迭代)(快慢指针环内追逐相遇)

要想清楚遍历结束的条件,一个指针去遍历,没有cur==NULL这个选项,又因为不能找到以前的结点,不能让后面的cur==前面的cur结束遍历,所以至少需要两个指针。假设一个指针是cur,另一个是prev,如果让prev在cur往后走之前进行保存,那么prev就是cur的前一个,可是环的大小我们并不清楚,环内有几个结点不知道,就需要更多的前置结点,至少需要环内结点个数的prev变量(prev1,prev2,prev3……)分别存cur第前一个,第前二个……,cur==prev的其中一个就说明有环,而prev要创建n个,这就有点难办了,还不如动态数组。

那这里应该怎么设置才能让遍历有结束条件呢?这里有个神奇的操作,要想到环的特点:假设我们在环形操场赛跑,一个跑的慢的人先跑,另一个跑的快的人后跑,而且两个人都是匀速且不停下,那么快的人不管怎样,在未来的某一刻一定会追上那个慢的人,舔狗除外啊。那么我们在链表内可以创建两个指针slow,fast,都在head处,不需要slow先走,同时出发,slow走一步,fast走两步,只要都走进环内,两个指针一直走一定会相遇。相遇就说明带环,而且相遇就是结束条件

这里注意,为什么slow走一步,fast走两步呢?其实这个很容易理解:操场上跑步,我们的路程是连续的,而结点是正整数,是间断的,最小的单位差是一个结点,环内慢的比快的差n个结点,慢的走一步,快的走两步,此时差n-1个结点,如此下去,只要n次就可以追上慢的。假如slow走1步,fast走3步,slow和fast差n个,两指针走一次,差n-2,n-4,……,如果n是偶数,那么未来可以相遇,但是如果n是奇数,最后会相差1,然后slow走一步,fast走三步后,fast比slow领先一步,这个时候slow和fast相差length-1步,如果length-1仍是奇数,那么最后fast和slow就会错过。如此,fast走4步也是这样,只有slow走一步,fast走两步,两人差距一步一步缩小能够保证无论环的长度是多大都不会错过

代码二:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
bool hasCycle(struct ListNode *head) {
    //链表为空,或者只有一个结点或者只有两个结点的情况先判断掉
    if(head==NULL)
        return false;
    if(head->next==NULL)
        return false;
    if(head->next->next==NULL)
        return false;
    
    //slow,fast一个走一步一个走两步去遍历
    struct ListNode* slow = head;
    struct ListNode* fast = head;
    do
    {
        slow = slow->next;
        fast = fast->next->next;
        if(fast==NULL || fast->next==NULL)//防止不带环单链表结点是奇数个,fast->next->next走不下去
            return false;
    }while(slow!=fast);//用do while是为了防止slow==fast在第一个结点就被判断了

    return true;
    
}

分析:时间复杂度O(N)(1.当链表中不存在环的时候,fast遍历到NULL也就结束了,fast走完整个链表,slow走了一半,总的时间复杂度是O(N+N/2)。2.当链表中有环的时候,slow,fast先要走完环外的一段固定长度,slow,fast进环了,slow、fast在环内某一处相遇,我们可以根据数学原理去解释什么时候会相遇,如下),空间复杂度O(1)

时间复杂度的具体分析:

我们假设,环外结点长度为20,环内结点长度为5,总的链表长度(不同结点个数)为24(N,这里注意N是链表不同结点的个数,而环外结点包括了重合点,主要是为了方便后面计算)

假设环外结点长度为length环外 Lout,环内结点长度为length环内 Lin,N实际是Lout+Lin-1

1.fast走到星星处(重合点,该点算进环内结点,也算环外结点),fast走了20,slow走了10(一半)

2.slow走到星星处的时候,走了20,fast走了40,在环内循环了4圈且刚好和slow相遇,这个时候是最好的情况,在slow刚进环的入口就和fast相遇了,不需要追逐了。总的时间复杂度的是O(Lout+2Lout)。

3.最坏的情况是如何呢?首先slow和fast应该在环内追逐,且slow在环入口的时候和fast相差的距离最大,这样每走一次就走进一步,需要相差的步数次。如:slow走到星星处的时候,fast刚好在slow前面,假设fast绕顺时针和slow相差这样slow下一步就算走到fast重合了,fast也要先走了(因为是fast去追slow,而不是slow追fast(区别在于代码实现)),fast下次重合的情况是fast和slow都走了Lout-1次,所以总的时间复杂度为O(Lout+2Lout+(Lin-1)+2(Lin-1)),也就是O(3N)。

4.所以时间复杂度范围为O(3Lout~3N),因为Lout也是N的一部分,所以时间复杂度就是O(N)。

思路三:(引用大佬题解)

点击这里看原文

点击这里看原文

(10)Leetcode 142题

要求:1.返回链表相交的第一个结点

           2.不允许修改链表

思路一:(指针相遇)

题141只是判断出了链表是否相交,而这里要求我们返回第一个相交结点,我们先引用题141的题解判断出是相交的情况,此时slow和fast在环内某一处相交,但具体是在哪,这跟环的大小有关,

但这里有个神奇的操作,跟环的大小无关slow和fast相遇处如果此时slow继续顺时针走,同步的cur从head一步一步走,cur按理会进环,然后slow和fast永远不会有交集,但是这里根据数学原理可以知道,cur和slow会在环的入口相遇(也就是第一个相交的那个结点)。具体证明如下:

证明

假设环外长度为L,环的长度为C(圆),相遇点星星到相交处的长度为D(红线长度),

要证明的是low从相遇点和cur从head同时走,两者一定会相遇在环的入口(相交处)(如果环比较小,那么slow可能在环里饶了很多圈了,但最后和cur相遇一定在环入口),其实也就是要证明数学中L和nC-D相等,我们直接从slow和fast的两倍关系入手就行。slow走到相交处走了(L+D)(slow和fast一定在slow的第一圈就相遇了,这里根据上面时间复杂度的最坏情况分析就可以知道,具体细节自己思考一下,这里不再赘诉),fast走了(L+nC+D)(fast可能在圈里走了n圈了,至于多少圈不重要),而slow,fast还有两倍的关系,所以2slow=2(L+D) = fast=(L+nC+D),能解出L = nC-D,即得证。

代码一:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *detectCycle(struct ListNode *head) {
    //把题141题解搬过来判断是否带环,并把false改为NULL,删掉返回值
    //同时得到了slow和fast相遇点
    if(head==NULL)
        return NULL;
    if(head->next==NULL)
        return NULL;
    if(head->next->next==NULL)
        return NULL;

    struct ListNode* slow = head;
    struct ListNode* fast = head;
    do
    {
        slow = slow->next;
        fast = fast->next->next;
        if(fast==NULL || fast->next==NULL)
            return NULL;
    }while(slow!=fast);

    //cur从head走,slow从相遇点走,返回它俩相遇点
    struct ListNode* cur = head;
    while(cur!=slow)
    {
        slow = slow->next;
        cur = cur->next;
    }
    return cur;
  
}

分析:时间复杂度O(N)(这里要加上以前的时间复杂度,我们只要算后加的,后面cur从head走到环入口,而slow可能走了环内很多圈,然后再走C-D,也就是nC+C-D,具体关系就不分析了,反正是链表长度的常数倍,所以最终时间复杂度是N阶的),空间复杂度O(1)。

思路二:(引用大佬题解)

代码二:

(点击这里看原文)

(11)Leetcode 138题

要求:1.创建新的结点复制原来的链表

           2.复制结点不能指向原来的链表

思路一:(结点相对位置连接断开法)

首先看到一大段题目,看起来复杂其实很简单,就是在我们单链表结构的基础上,多了一个随机指针,这个指针可以连接到链表的任何一个结点上,也能连接到NULL;而题目要求我们复制这样一个链表,我们暂且叫做随机链表吧。对于单链表来讲,这种复制非常简单,只需要遍历一遍把链表长度计算出来,重新开辟这么多个结点,然后再遍历一遍,把所有值都复制到新链表中即可。那么这种链表可以这样吗?明显不行,为什么?单链表的结构是固定的,它的next就是指向后一个结点,而随机链表的random指针并不知道指向哪个结点,那我们可以用数组把randoms存起来,然后先把单链表创建好后再放入random吗?明显也是不行的,因为random是指向原链表的结点的。那么要如何才能让复制链表的random也按原随机链表指向自己的结点呢?

这里我们要深刻理解相对位置这个概念,既然复制的random要指向复制链表的结点,那么我们就让复制链表和原随机链表有相对关系即可。这里有个神奇的操作:我们把创建的新结点每一个都尾插到原结点的后面,然后把新结点的random按照原random相对位置连接到后一个结点,如下图;处理所有结点后把这些新结点断开再连起来,然后修复原链表,操作似乎有点多,但至少理论上清晰了。

1.每创建一个结点插入到相应结点的后面,这样才会有相对位置的关系,拷贝数据进去,连接好next,完成后在有了相对位置后,然后再解决random,新结点newNode的random要注意给的是原结点random的下一个这里利用了相对位置,需要大家好好画图理解一下)当然如果原先结点的random如果指向的是NULL或者是自身,那么就需要先额外判断一下,让新结点newNode指向NULL或者自身。这样就把newNode连接上了原先链表,而且结构(数据val,指针next,random)都完整了。

2.利用指针把newNode都拆下来,并newNode的next按原先顺序连接起来,得到的就是我们想要的复制链表,同时把断开的原链表重新连起来(这一步可有可无,如果题目没有要求不能破坏链表的结构,我们就需要还原原链表)返回新的链表头即可。

代码一:

/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */

struct Node* copyRandomList(struct Node* head){ 
    //cur遍历为了把新结点插入到原结点的后面
    //为了解决新结点val和next
    struct Node* cur = head;
    while(cur!=NULL)
    {
        struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
        newNode->val = cur->val;
        newNode->next = cur->next;
        cur->next = newNode;
        cur = newNode->next;
    }

    //cur再从头遍历一遍,为了解决random
    //此时复制链表结点和原链表已经有了相对位置了,可以进行连接复制链表结点的random了
    cur = head;
    while(cur!=NULL)
    {
        struct Node* newNode = cur->next;
        if(cur->random==NULL)
            newNode->random = NULL;
        //else if(cur->random==cur)
        //    newNode->random = newNode;//注意这里,如果cur的random是自己,那么newNode的random是自己也是cur的下一个,所以可以合并到下面语句,这里就注释掉了
        else
        {
            newNode->random = cur->random->next;
        }
        cur = newNode->next;
    }
  

    //上面已经把复制链表的val,next,random解决了,但是还是在原链表上,所以复制结点断开,并连接原链表
    //copyhead是复制链表的头结点,newcur用来遍历存放复制链表的结点
    cur = head;
    struct Node* copyhead = NULL;
    struct Node* newcur = NULL;
    while(cur!=NULL)
    {
        struct Node* copy = cur->next;
        struct NOde* next = copy->next;
        if(newcur==NULL)
        {
            copyhead=newcur=copy;
        }
        else
        {
            newcur->next = copy;
            newcur = copy;
        }
        cur->next = next;
        cur = next;
        
    }

    return copyhead;

}

分析:时间复杂度O(N)(cur先遍历一遍数组把新链表结点val,next解决,然后再有了相对位置后再遍历一遍,把random解决,然后再遍历一遍把cur拆开,虽然newcur也在遍历,但是时间复杂度看的是语句执行的次数,迭代方法看循环结束条件即可,所以总的时间复杂度是O(3N)),空间复杂度O(1)。至于其他的操作,这里就不写了,博主困了。

以上是单链表的一些简单题目,主要是写下了我的一些思路,为了提高自己的思维能力,构建解决问题的方法,博主之前也是看到题目就像看答案,而每次看完答案发现大吃一惊,但是自己就是想不到,我希望通过方法的总结能够让自己和大家在面对一些基础题的时候不会下不去手,一步一步能够完善自己的答案。后续持续更新。

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值