数据结构与算法:从leetcode中学习链表算法

前言

链表是一种典型的数据结构,同时又有许多关于链表的算法,如链表反转、排序等等。

最近也了解到面试对算法的考察是重要的一部分,因此我想通过leetcode做专项练习,把其中关于链表的好题认认真真做一遍,总结出来,化为己用。希望能对以后找工作有所帮助。


终于把Leetcode上链表专题中非会员题目都做了一遍,真的有种受益匪浅的感觉,既复习了之前学过的知识和做过的题目,也学到了新的知识。总体感觉自己对链表这一类型的题目有那种知道如何思考的意识,希望这种感觉是对的,希望。

下面列出来的题都是对我自己有帮助的,或是学到了新的思路,或是学到了如何更简洁高效的方法。


提醒

要完整做完这篇博客提到的题目,且学到高效的做法,需要较长时间(大佬除外),希望有兴趣的朋友可以坚持下来,一定会有收获。


正题

1、两数相加

这是一道入门题,但我还是想记录下来,因为对于简单的题,我希望能够学会更高效或者更简洁的做法。

这题我一开始的想法是用三个while循环和一个if,依次处理当l1和l2都不为空、只有l2为空、只有l1为空以及l1和l2都为空的情况,再想一下,可以把前三个while合并起来,用逻辑或做循环条件。写出代码后感觉处理l1和l2都为空的情况也是可以合并到while里,因此最终的解法可以是这样:(我觉得已经十分简洁了)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode ans(0);
        ListNode* p = &ans;
        int carry=0,sum=0;
        while(l1||l2||carry){
            sum=(l1?l1->val:0)+(l2?l2->val:0)+carry;
            l1=(l1?l1->next:NULL);
            l2=(l2?l2->next:NULL);
            p->next = new ListNode(sum%10);
            p=p->next;
            carry=sum/10;
        }
        return ans.next;
    }
};

2、删除链表的倒数第N个节点

题意:给定一个链表,删除链表的倒数第 个节点,并且返回链表的头结点。

这题我觉得也是简单题,但如何只遍历一遍链表就可以解决问题就得稍加思考了。

我自己是遵循着“时间与空间复杂度守恒定理”(我乱起的名字),要想节约时间复杂度就有必要牺牲一些空间复杂度。所以我想用数组或者队列来辅助实现。用数组的话最简单,把链表转为数组就好了,申请一个vector<ListNode*>,这种方法可以解决大多数与链表有关的问题,但这样做水平不高,而且可能占用很多空间

我本来以为用队列的话会比较高端一点,但其实做完之后发现只是比用数组好了一点而已。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        
        //tail指示队尾,pre指示队首的上一个节点
        ListNode *ans=head,*tail=NULL,*pre=NULL;
        queue<ListNode*> q;    //队列只保存 min(n,链表总长度) 个数的界定啊
        while(n--){
            q.push(head);
            tail=head;
            head=head->next;
        }
        
        //移动队列,不如说是设置一个大小为n的窗口,移动这个窗口直至链表结尾
        while(tail->next){
            tail=tail->next;
            pre=q.front();
            q.pop();
            q.push(tail);
        }

        //两种情况:要删除头节点 或者 删除其他节点
        if(pre)
            pre->next=pre->next->next;//删除其他节点
        else{
            return ans=ans->next;//删除头节点
        }
        return ans;
    }
};

然而,在写这篇博客的时候,我突然发现上面的代码中,队列不是必须的。因为上面的队列只有一个作用,那就是帮忙记录pre。没必要,可以删去。于是有了下面更简洁的题解:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        
        ListNode *ans=head,*tail=NULL,*pre=NULL;
        
        while(n--){
            tail=head;
            head=head->next;
        }
        head=ans;            //head重新指向头节点
        
        while(tail->next){    //pre和tail共同维护一个长度为n的子链表
            tail=tail->next;
            pre=head;
            head=head->next;
        }

        if(pre)
            pre->next=pre->next->next;//删除非头节点的节点
        else{
            return ans=ans->next;//删除头节点
        }
        return ans;
    }
};

上面这种简洁的方法让我想起了双指针,这也确实是属于一种双指针的方法。在我的理解中,关于链表的算法有一种比较常用的是利用两个指针来遍历链表,一快一慢,根据实际需要来觉得两个指针的相对距离。这个得有具体题目,做一下就有印象了。

3、合并K个排序链表

合并K个有序链表,这是链表很经典的题了。比较容易想到用直接比较的方法去也就是每次比较两个链表,将结果与剩下的其他一个链表比较,如此反复直到所有链表都比较完得到最后的结果。这种做法可以理解为降维,将合并K个转换为合并2个

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        ListNode ans(0),*p=&ans;    //p存放每次比较的结果
        if(lists.empty())
            return NULL;
        ListNode *l1=lists[0];
        for(int i=1;i<lists.size();++i){
            ListNode* l2=lists[i];
            //合并两个有序列表
            while(l1&&l2){
                if(l1->val<l2->val){
                    p=p->next=l1;
                    l1=l1->next;
                }
                else{
                    p=p->next=l2;
                    l2=l2->next;
                }
            }
            if(l1)
                p->next=l1;
            else
                p->next=l2;
            //l1指向合并后的链表
            l1=ans.next;
            p=&ans;       
        }
        return l1; 
    }
};

上面的做法很常规,但复杂度很高,一个链表中的元素会被反复比较。比如假设lists有n个,则lists[0]的每个元素要比较n-1次,以此类推。。。

为了提高自身水平,我在讨论区学习了一种利用优先队列使得每个链表的每个元素只需要比较一次的方法:借助优先队列

优先队列默认队首元素是最小值,但由于要比较的类型是我们自定义的ListNode*,故我们要定义ListNode*的比较方式。

优先队列初始化方式:

priority_queue<类型,vector<类型>,比较方式>

这里的比较方式可以忽略,但当我们要定义时需要定义为一个带有()方法的结构体,如下:

struct cmp{
    int operator()(const 类型 a,const 类型 b){
        return ...比较方式
    }
}

回到上题,利用优先队列的性质,先把所有链表的头节点入队,再从队列中依次取出队首元素(最小值),然后将队首元素的下一个节点入队。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    struct cmp{
        int operator()(const ListNode* a,const ListNode* b)
        {
            return a->val > b->val;
        }
    };

    ListNode* mergeKLists(vector<ListNode*>& lists) {
        priority_queue<ListNode*,vector<ListNode*>,cmp> q;
        for(ListNode* list:lists)
            if(list)
                q.push(list);

        //相当于lists为空
        if(q.empty())
            return NULL;

        ListNode* ans=q.top();
        q.pop();
        ListNode* move=ans;
        
        //每次取出队首元素后若其下一个节点不为空则入队
        if(ans->next)
            q.push(ans->next);
        while(!q.empty())
        {
            move->next = q.top();
            q.pop();
            move=move->next; 
            if(move->next)
                q.push(move->next);
        }
        return ans;
    }
};

我觉得以后若面试真的碰上这种问题,用优先队列肯定会让你在一堆用常规方法的竞争对手中脱颖而出。强烈建议掌握这种方法

4、K 个一组翻转链表

这道题是翻转整个链表的进阶,在leetcode上的难度为hard,要求用常数的额外空间以及必须进行实际的节点交换,而不是投机取巧——只改变节点的值。

先考虑翻转整个链表,我的思路是用三个指针,一个pre指向当前链表的头节点(这个随着每一步的翻转会不断改变),一个cur指向当前遍历到的节点,一个post执行cur的下一个节点。对整个链表的翻转实际上就是依次移动节点位置。每次我将cur指向post的下一个节点,将post指向pre,相当于将以pre开头以post结尾这一子链表的尾节点移到开头。

用代码来说明:

while(cur->next!=NULL){    //确保post不为null
    post=cur->next;
    cur->next=post->next;
    post->next=pre;
    pre=post;
}

回到题目,将原链表切割成一个个长度为K的子链表,然后做相同的操作。需要注意的是前一个子链表翻转后的结尾要指向后一个子链表翻转后的开头。所以我声明了一个tail指针指向前一个子链表翻转后的结尾,当下一个子链表翻转完成时我令tail指向该子链表的pre。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        if(head==NULL||k==1)
            return head;

        ListNode *pre=NULL,*cur=head,*tail=NULL,*ans=NULL,*post=NULL,*save_head=head;
        int n=0;    //n记录每个子链表的长度

        while(head||n==k){    //n==k是为了处理链表总长度等于K的情况
            if(n==k){
                n=0;
                //翻转子链表,一开始开头即是cur,即pre=cur
                pre=cur;
                while(cur->next!=head){    //此时head是下一个子链表的开头
                    post=cur->next;
                    cur->next=post->next;
                    post->next=pre;
                    pre=post;
                }
                //前一个子链表指向后一个子链表
                if(tail)
                    tail->next=pre;
                tail=cur;

                cur=head;    //重新定义子链表开头
                if(ans==NULL)    //答案是第一个翻转的子链表的开头
                    ans=pre;
            }

            else{
                head=head->next;
                ++n;
            }
        }
        
        //下面的条件表达式是为了处理K大于链表总长度的情况
        //K大于链表总长度时链表不需要翻转
        return ans?ans:save_head;
    }
};

上面的做法是正确可行的,但我总感觉看起里不是那么简洁,所以我到讨论区学了另一种我认为比较简洁的做法。

这种做法定义了一个dummy节点作为链表的头节点,使得翻转每个子链表时不需要在一开始时令pre=cur,且这样做可以直接让上一个子链表的尾节点指向下一个子链表的头节点,而不用进行特判。这也让我想起以前做链表题时确实有定义一个dummy节点作为辅助节点来简化的方法


class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        if(head==NULL||k==1)
            return head;

        ListNode dummy(0);
        dummy.next=head;
        ListNode *pre=&dummy,*cur=NULL,*post=NULL;
        int len=0;

        while(head){        //先计算总长度
            head=head->next;
            ++len;
        }

        while(len>=k){     //分组翻转
            //这种方法下,pre的下一个节点始终是子链表的头节点
            cur=pre->next;
            for(int i=1;i<k;++i){
                post=cur->next;
                cur->next=post->next;
                post->next=pre->next;
                pre->next=post;
            }
            pre=cur;
            len-=k;
        }
        return dummy.next;
    }
};

5、删除排序链表中的重复元素 II

这题要求删除一个有序链表中的所有含有重复数字的节点。只保留原始链表中没有重复出现的节点。

我觉得这题难点在于可能会修改原始链表的头节点 以及 需要一个指针pre指向当前节点的上一个节点。

如果按常规思路(节点前后比较,若相等则删除后一个节点,若不相等,则看当前节点是否是重复节点,是的话就把当前节点删除)的话应该问题不大,但我觉得比较繁琐,所以我到讨论区学习了一种比较简洁的方法,只不过这种方法没有真正删除重复节点,只是把它们忽略了。


class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        if(head==NULL||head->next==NULL)
            return head;
        ListNode *pre=NULL,*cur=head,*post=NULL,*dummy=new ListNode(0);
        dummy->next=head;   pre=dummy;
        
        //pre始终指向当前节点的前一个节点
        while(cur){
            while(cur->next&&cur->val==cur->next->val){
                cur=cur->next;
            }    
            if(pre->next==cur){    //当前节点无重复节点
                pre=cur;
            }
            else{                  //当前节点是重复节点
                pre->next=cur->next;
            }
            cur=cur->next;
        }
        
        return dummy->next;
    }
};

发现做链表相关题目时,若空间复杂的为常数级,则经常是定义一个cur指针指向当前访问到的节点,再定义一个指针post/next指向当前节点的下一个节点,对于较复杂的题目则可能需要再定义一个指针pre指向当前节点的前一个节点,有时候需要先定义一个dummy节点使其指向头节点来避免遍历链表时,初始的pre与cur指向同一个节点。

定义完上述指针,剩下的就是理清各个指针之间的逻辑关系,然后根据题目需要让他们指向不同节点。这是一个大概的思路,相信只要时间足够,一般是能得到题解的。

6、分隔链表

给定一个链表和一个特定值X,对链表进行分隔使得所有小于X的节点都在大于或等于X的节点之前,且两个分区中每个节点的初始相对位置。

由于要保持相对顺序,所以我的想法是按顺序遍历整个链表然后根据节点的值做不同的处理。当节点值大于等于X时不做处理,当节点值小于X时,要做的是:

1、让当前节点的上一个节点指向当前节点的下一个节点;

2、将当前节点插入到 已经遍历过的最后的小于X的节点 之后。

因此需要记录当前节点的上一个节点,以及已经遍历过的最后的小于X的节点。这比较麻烦,为此我也定义了一个dummy节点指向链表的开头。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* partition(ListNode* head, int x) {
        ListNode *pre=NULL,*cur=head,*last=NULL,*post=NULL,*dummy=new ListNode(x-1);
        dummy->next=head,pre=last=dummy;

        while(cur){            
            post=cur->next;    //因为下面cur有变所以先保存一下
            if(cur->val<x){
                last->next=cur->next;    //将当前节点从原位置移走

                cur->next=pre->next;     //将当前节点插入到对应位置
                pre->next=cur;
                pre=cur;

                if(last->val<x)          //若没此步操作,last并非指向当前节点的上一个节点
                    last=cur;
            }
            else
                last=cur;
            cur=post;
        }
        return dummy->next;
    }
};

上面的做法我觉得还是有点复杂,在讨论区我看到了另一种真正理解这道题的本质的解法,简单明了。就是将小于X的节点构建成一条链表,将其他节点构建成另一条链表,再把两者串起来。很简洁的想法,可惜我一开始没往这方面走,也让我发现我有点死板,就只会创建一个dummy节点而不会多创建一个。。。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* partition(ListNode* head, int x) {
        ListNode small(0),large(0);
        ListNode *p1=&small,*p2=&large;
        while(head){
            if(head->val<x){
                p1->next=head;
                p1=p1->next;
            }
            else{
                p2->next=head;
                p2=p2->next;
            }
            head=head->next;
        }
        p1->next=large.next;
        p2->next=NULL;
        return small.next;
    }
};

7、有序链表转换二叉搜索树

这题我一开始的想法是把链表转为数组去做,即将问题转换为有序数组转换二叉搜索树。题目要求二叉树是平衡二叉树,也比较容易想到每次取数组的中点作为子树的根去构建二叉树。这样构建方法也是有关二叉树的问题中常见的算法。

class Solution {
public:
    TreeNode* sortedListToBST(ListNode* head) {
        vector<int> vals;
        while(head){
            vals.push_back(head->val);
            head=head->next;
        }        
        int n=vals.size();
        return buildtree(vals,0,n);
    }
    TreeNode* buildtree(vector<int>& vals,int l,int r){
        if(l>=r)
            return NULL;
        int ind=(l+r)/2;    //取中点是因为要构建的是 平衡 二叉搜索树
        TreeNode *node=new TreeNode(vals[ind]);
        //中点左边的元素都比中点小,一定位于左子树
        node->left=buildtree(vals,l,ind);
        //中点右边的元素都比中点大,一定位于右子树
        node->right=buildtree(vals,ind+1,r);
        return node;
    }
};

但这样做总感觉违背题目本意,跟链表毫无关系(这也是我上面说过的,有时我们可以将链表转换为数组去解题,这样往往会降低很多难度)。但是看了一下讨论区,众人解题的核心思想大都一样,只不过可以用双指针找链表中点的方式来代替将链表数组化。

class Solution {
public:
    
    TreeNode* buildTree(ListNode* head,ListNode* tail)
    {
        if(head==NULL||head==tail)
            return NULL;
        ListNode *slow=head,*fast=head;
        while(fast!=tail&&fast->next!=tail)    //双指针找中点
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        TreeNode* root = new TreeNode(slow->val);
        root->left = buildTree(head,slow);
        root->right = buildTree(slow->next,tail);
        return root;
        
    }
    
    TreeNode* sortedListToBST(ListNode* head) {
        if(head == NULL)
            return NULL;
        return buildTree(head,NULL);
    }
};

这也再次提醒了我双指针(快慢指针)在链表中的用处。

8、环形链表 II

判断链表有没有环,有的话则找出环的起点。这是关于链表的十分经典的题目了。刚好上一题说到快慢指针,这种题就是用快慢指针做的。

试想若链表有环,那么我们用一个每次移动一个节点的慢指针和一个每次移动两个节点的快指针从链表的头节点开始遍历链表,这快慢两个指针必定会相遇。就好比在环形跑道上跑步,跑得快的的人会比跑得慢的人多跑一圈然后再次相遇。

这题还要求我们找出环的起始点,画个图比较明显:

由上图可知,fast指针和head指针(链表的头节点)与环的起点的距离是相同的。因此接下来,我们让fast和head同时每次只走一个节点,当它们相遇时,它们所指向的节点就是环的起点。


class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        //处理特殊情况:链表空和链表仅有一个节点且无环
        if(head==NULL||head->next==NULL)
            return NULL;

        ListNode *slow=head,*fast=head;
        //判断有无环
        while(fast&&fast->next){
            slow=slow->next;
            fast=fast->next->next;
            if(slow==fast)
                break;
        }
        //有环则可以找到起点,无环则最终fast必为NULL
        while(head&&fast){
            if(head==fast)
                break;
            head=head->next;
            fast=fast->next;
        }
        return fast;
    }
};

9、对链表进行插入排序

一开始我以为,插入排序而已嘛,很简单,但自己一写就乱了,所以我想记录下来。

最终的解法我是维护了一个已经排好序的列表,因为若一个节点要插入到前方有序链表中我们需要做的是:

1、从原链表删除该节点,故需要令该节点的上一个节点(即有序链表的尾节点指向该节点的下一个节点);

2、将该节点插入到有序链表,如需要遍历有序链表找到第一个大于该节点的节点(设为节点A)并插入(这需要知道A的上一个节点)

重点在于知道有序链表的尾节点就是当前我们要进行插入排序的节点的上一个节点。


class Solution {
public:
    ListNode* insertionSortList(ListNode* head) {
        if(head==NULL)
            return NULL;
        ListNode *ordered_head=new ListNode(0),*ordered_tail=head,*ordered_last=NULL,*ordered_cur=NULL;
        ListNode *cur=head->next;    
        ordered_head->next=head;
        
        while(cur){
            ordered_last=ordered_head;
            ordered_cur=ordered_head->next;
            
            bool insert=false;
            //剪枝:若当前节点的值大于等于有序链表的最后一个节点的值,则不必插入
            if(ordered_tail->val>cur->val){    
                while(ordered_cur!=cur){
                    if(ordered_cur->val>cur->val){
                        //从原链表中删除节点
                        ordered_tail->next=cur->next;  
                        //向有序链表中插入节点  
                        cur->next=ordered_cur;    
                        ordered_last->next=cur;
                        insert=true;
                        break;
                    }
                    else{
                        ordered_last=ordered_cur;
                        ordered_cur=ordered_cur->next;
                    }
                }
            }
            //更新有序链表的尾节点
            if(insert==false){
                ordered_tail=cur;
            }     
            cur=ordered_tail->next;
        }
        
        return ordered_head->next;
    }
};

写完代码我觉得链表的插入排序更能体现插入排序的本质。

10、重排链表

这题一开始我的想法是用一个数组来存储节点,然后对数组进行操作,这是可行的,但我想试试空间复杂度为O(1)的方法。

但想了挺久也想不到,只会暴力的做法,就是每次找到链表的尾节点然后将其插入到对应的位置,然后修改倒数第二个节点为尾节点。这种做法很简单,但效率太低,我就不贴出来了。

下面贴出来的是在讨论区学习到的一种比较巧妙的思路,效率也很高。分三步:

1、寻找原链表的中点;

2、翻转原链表中点后的子链表(记为L);

3、将L插入到原链表中点前的子链表的对应位置;


class Solution {
public:
    void reorderList(ListNode* head) {
        if(head==NULL||head->next==NULL)
            return ;
        
        ListNode *mid=head,*p=head;
        //求链表中点,如1-2-3则中点取2,1-2-3-4则中点取2
        while(p->next&&p->next->next){
            mid=mid->next;
            p=p->next->next;
        }
        
        ListNode *pre=mid,*cur=mid->next,*next=NULL;
        //翻转中点后的子链表,
        //如1-2-3-4则得到1-2-4-3;1-2-3-4-5则得到1-2-3-5-4
        while(cur->next){
            next=cur->next;
            cur->next=next->next;
            next->next=pre->next;
            pre->next=next;
        }
    
        //根据题设要求将翻转后的子链表插入到中点前的子链表    
        while(head!=mid){
            cur=mid->next;
            mid->next=cur->next;
            cur->next=head->next;
            head->next=cur;
            head=cur->next;
        }
    
    }
};

上述方法涉及了:

1、快慢指针——寻找链表中点,是十分常见的做法;

2、链表翻转,是学习链表算法必须掌握的;

3、链表节点的删除和插入,这是链表的基础操作了,无非是指针指来指去,如果不熟悉的话建议一开始先用笔画画。

11、相交链表

这题的标签是easy,但我觉得要比较好的做出来,对我来说不easy。

简单的思路就是先遍历其中一个链表然后存储在一个set里面,再遍历另一个链表判断该链表的节点有没有在set里。

这样做的话空间复杂度不是O(1),而且时间复杂度的话得看我们用的是unordered_set还是set,前者理想情况下查询的复杂度是O(1),后者查询的复杂度是O(lgn)。

另一种思路:先遍历其中一个链表,然后将其所有节点独立(或者说分隔开),然后再遍历另一个链表直到访问到一个没有下一个节点的节点,那么该节点要不就是两个链表的交点,要不就是NULL(表示两个链表没有交点)。

但我发现题目要求不能改变链表的结构。。。

所以我又有了另一种想法:先遍历其中一个链表找到尾节点,将尾节点指向另一个链表的头节点,题目转为求环形链表的入口,就跟上面的第八题一样了。

但鉴于这题的难度为easy,上面这种想法实际上把问题复杂化了。

最后,我自己只能用先求出两个链表的长度的方法来实现时间复杂度O(n)且空间复杂度O(1):


class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode *p=headA,*q=headB;
        int A=0,B=0;    //两个链表的长度
        while(headA){
            headA=headA->next;
            ++A;
        }
        while(headB){
            headB=headB->next;
            ++B;
        }
        //使p始终指向较长的链表
        if(A<B)
            swap(p,q);
        //使以p开头的子链表与以q开头的子链表长度相同
        int n=abs(A-B);
        while(n--)
            p=p->next;
        //同时遍历两个长度相同的链表,若其有交点则必有一次p=q
        while(p){
            if(p==q)
                return p;
            p=p->next;
            q=q->next;
        }
        return NULL;
    }
};

然后我在讨论区看到一种十分巧妙的做法,时间复杂度为O(N+M),N和M表示两个链表的长度。

令指针A指向以headA为头节点的链表,指针B指向以headB为头节点的链表,然后同时移动指针A和B,当A移到headA链表的尽头时让A指向headB,当B移到headB链表的尽头时让B指向headA。看下图,很容易理解:

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(headA==NULL||headB==NULL)
            return NULL;
        ListNode *A=headA,*B=headB;
        while(A!=B){
            A=(A?A->next:headB);
            B=(B?B->next:headA);
        }
        return A;
    }
};

讨论区一位老哥说出了众人的心声:

好好学吧。

12、回文链表

判断一个链表是不是回文链表。

可以用递归的方式:

 


class Solution {
public:
    ListNode *p=NULL;
    bool isPalindrome(ListNode* head) {
        p=head;
        return cmp(head);
    }
    bool cmp(ListNode* head){
        if(head==NULL)
            return true;
        //利用全局指针p指向头节点,利用递归先比较尾节点,同时向中间节点移动。
        bool is=cmp(head->next)&(head->val==p->val);
        p=p->next;
        
        return is;
    }
    
};

若想要用O(n)的时间复杂度和O(1)的空间复杂度,则没有那么简单,思路类似于上面第10题重排链表。

1、找到链表中点;

2、反转链表中点之后的子链表;

3、比较从头节点到中点的子链表和中点之后的子链表对应位置上的值。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool isPalindrome(ListNode* head) {
        if(head==NULL||head->next==NULL)
            return true;
        //找中点
        ListNode *mid=head,*p=head;
        while(p->next&&p->next->next){
            mid=mid->next;
            p=p->next->next;
        }
        //翻转中点之后的子链表
        ListNode *pre=mid,*next=NULL;
        p=mid->next;
        while(p->next){
            next=p->next;
            p->next=next->next;
            next->next=pre->next;
            pre->next=next;
        }
        //比较对应位置上的值
        p=mid->next;
        while(p){
            if(p->val!=head->val)
                return false;
            p=p->next;
            head=head->next;
        }
        return true;
    }
};

13、两数相加 II

与第一题不同的是这题的两个链表中头节点表示数字的最高位。

因为题目说不能修改链表,所以我理所当然地用栈stack来模拟链表的翻转。只是我用了三个栈,两个用于模拟翻转,一个用于存储相加后的新数,这样比较繁琐,但我没去细想。在讨论区看到别人简洁的代码后觉得不错,所以记录下来。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        stack<int> nodes1,nodes2;
        
        for(;l1;l1=l1->next)
            nodes1.push(l1->val);
        for(;l2;l2=l2->next)
            nodes2.push(l2->val);
        
        //可以学学res和next
        ListNode *res=new ListNode(0);
        int sum=0;
        while(!nodes1.empty()||!nodes2.empty()){
            if(!nodes1.empty()){
               sum+=nodes1.top();
               nodes1.pop();
            }
            if(!nodes2.empty()){
               sum+=nodes2.top();
               nodes2.pop();
           }
            res->val=sum%10;
            sum/=10;
            ListNode *next=new ListNode(sum);
            next->next=res;
            res=next;
        }
        return res->val?res:res->next;
    }
};

14、链表中的下一个更大节点

这一题我一开始的想法是用栈,从链表的头节点开始依次访问链表节点,若当前节点比栈顶节点大则说明当前节点是栈顶节点的下一个更大节点。而为了记录对应的答案,我在遍历链表时记录遍历到第几个节点,然后同时入栈节点的值以及节点的索引(也就是该节点是第几个节点)。直接看代码比较容易理解。


class Solution {
public:
    vector<int> nextLargerNodes(ListNode* head) {
        int n=0;    //计算链表长度
        ListNode *p=head;
        for(;p;p=p->next,++n);
        
        vector<int> ans(n,0);
        stack<int> indexs,vals;    //indexs保存vals中对应值的索引
        int ind=0;
        while(head){
            while(!vals.empty()&&vals.top()<head->val){
                ans[indexs.top()]=head->val;    //根据索引确定答案
                indexs.pop();vals.pop();        //indexs和vals的对应关系应该保持一致
            }
            vals.push(head->val);
            indexs.push(ind++);
            head=head->next;
        }
        return ans;
    }
};

但写完一想,我这跟把链表转为数组没什么区别啊,没有真正的做一道链表题。

然后我看了讨论区一些比较好的解法,觉得基本思路也是一样,只不过写法比较简洁和巧妙。


class Solution {
public:
    vector<int> nextLargerNodes(ListNode* head) {
        vector<int> ans;    //答案数组:先存节点值,再修改为答案
        stack<int> indexs;  //存节点索引
        while(head){
            int val=head->val;
            //indexs中所有元素的最大值不超过ans.size(),所以这样写不会越界(看下面indexs是怎么push的)
            while(!indexs.empty()&&ans[indexs.top()]<val){
                ans[indexs.top()]=val;  //若找到节点的下一个更大节点则修改答案数组
                indexs.pop();           //找到下一个更大节点则将节点索引出栈,故最后栈中剩下的就是没有下一个更大节点的节点
            }
            indexs.push(ans.size());    //节点索引入栈
            ans.push_back(val);         //存储节点值
            head=head->next;
        }
        //栈中剩下的就是没有下一个更大节点的节点
        while(!indexs.empty()){
            ans[indexs.top()]=0;
            indexs.pop();
        }
        return ans;
    }
};

15、从链表中删去总和值为零的连续节点

这题不错,需要用到前缀和的知识,如果之前不了解这部分的知识的话可能比较难以想到。下图说明:

上面两图实现为代码即为:


class Solution {
public:
    ListNode* removeZeroSumSublists(ListNode* head) {
        if(head==NULL)
            return NULL;

        unordered_map<int,ListNode*> prefix;    //记录前缀和
        ListNode newhead(0);    //扩展原链表
        newhead.next=head;
        prefix[0]=&newhead;

        int sum=0;
        for(;head;head=head->next){
            sum+=head->val;
            if(prefix.count(sum)){
                ListNode *p=prefix[sum]->next;
                prefix[sum]->next=head->next;
                //必须删除 要删去的子链表对应节点的前缀和 的记录
                int presum=sum;
                while(p!=head){
                    prefix.erase(presum+=p->val);
                    p=p->next;
                }
            }
            else{
                prefix[sum]=head;
            }
        }
        return prefix[0]->next;
    }
};

16、排序链表

这题要求用O(nlogn)的时间复杂度和常数级空间复杂度对链表进行排序。这个两个限制让我觉得这是一道很不错的题。

 O(nlogn)的时间复杂度很容易联想到归并和快速排序,鉴于归并的时间复杂度比较稳定,所以往归并方向思考。但是我之前学习的归并排序都是基于递归的,递归产生了函数栈,无法满足空间复杂度为常数级的情况。

其实一开始我没注意到要求常数级的空间复杂度,所以就用递归做了,后来才发现这个要求。在中文讨论区看到一种简单易懂的做法,故记录下来。思路就是归并排序的思路:

1、先将链表划分为长度为1的多个子链表,然后两两合并,变成多个长度为2的排序后的子链表;

2、与1同理,两两合并长度为2的子链表。。。直到子链表长度为原链表长度

需要懂得:如何划分子链表 以及 如何合并两个有序链表


class Solution {
public:
    ListNode* sortList(ListNode* head) {
        ListNode newhead(0),*pre=&newhead,*cur=head,*left=NULL,*right=NULL;
        newhead.next=head;
        int length=0;    //原链表长度
        for(;cur;cur=cur->next,++length);
        
        //block为子链表长度,为2的整数次幂
        for(int block=1;block<length;block<<=1){
            cur=newhead.next;
            pre=&newhead;
            //切割子链表+合并子链表
            //left和right是要合并的两个子链表
            while(cur){
                left=cur;
                right=cut(left,block);
                cur=cut(right,block);
                pre->next=merge(left,right);
                while(pre->next)    //pre指针实际上指向排好序的子链表的结尾
                    pre=pre->next;
            }
        }
        return newhead.next;
    }
    //合并两个有序链表
    ListNode* merge(ListNode* l1,ListNode* l2){
        ListNode head(0),*p=&head;
        while(l1&&l2){
            if(l1->val<l2->val){
                p=p->next=l1;
                l1=l1->next;
            }
            else{
                p=p->next=l2;
                l2=l2->next;
            }
        }
        p->next=l1?l1:l2;
        return head.next;
    }
    //切割链表,返回切除以head为头节点的链表的前n个节点之后的链表
    ListNode* cut(ListNode* head,int n){
        while(--n&&head)
            head=head->next;
        if(head==NULL)
            return NULL;
        ListNode *next=head->next;
        head->next=NULL;
        return next;
    }
};

后话

以前总是急于求成,没有多加思考就去讨论区看别人的想法,结果虽然一下子可以“刷”比较多的题目,但没什么效果,很多过后就忘了。这次的题目我都有先用自己的思路做一遍,再学习更好的思路。这样下来我自我感觉学习效果不错。重质不重

 

 

 

 

 

 

 

 

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值