【总结】链表类相关题目

1.基本操作

1.两数相加(leetcode 2

  • 题目描述:

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。

如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。

您可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例:

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807

  • 分析:

首先,两个链表为逆序存放,那么开始位置为个位,依次十位,百位...,

  1. 同时遍历两个链表,两个链表对应的值相加,再加上进位标识符(sum);
  2. 判断当前左右链表的地址是否为NULL
  3. 接着,判断其和(sum)是否大于等于10,如果大于等于10,则标志位为1,sum为余数,否则为0,将sum赋值为左结点的数据域,更新左右结点;
/**
 * 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* left=l1;
        ListNode* right=l2;

        ListNode* Pre_l=l1;
        ListNode* Pre_r=l2;

        //进位标志
        int flag=0;
        int sum=0;
        while(left||right)
        {
            if(left==NULL)
            {
                //和
                sum=right->val+flag;
                if(sum>=10)
                {
                    sum %=10;
                    flag=1;
                }
                else
                    flag=0;
                //赋值给当前右指针
                right->val=sum;
                //前一帧的下一结点的地址为当前右指针的地址
                Pre_l->next=right;

                //更新前一帧的指针
                Pre_l=Pre_l->next;
                Pre_r=right;
                right=right->next;
            }
            else if(right==NULL)
            {
                sum=left->val+flag;
                if(sum>=10)
                {
                    sum %=10;
                    flag=1;
                }
                else
                    flag=0;
                left->val=sum;
                Pre_l=left;
                left=left->next;
            }
            else
            {
                //和
                sum=left->val+right->val+flag;
                if(sum>=10)
                {
                    sum %=10;
                    flag=1;
                }
                else
                    flag=0;

                //赋值给左链表当前结点
                left->val=sum;
                //更新前一帧指针和当前指针
                Pre_l=left;
                Pre_r=right;
                left=left->next;
                right=right->next;
            }
        }
        if(flag==1)
        {
            ListNode* node=new ListNode(1);
            Pre_l->next= node;
        }
        return l1;
    }
};

 2.两数相加II(leetcode 445

  • 题目描述:

给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。

你可以假设除了数字 0 之外,这两个数字都不会以零开头。

 

进阶:

如果输入链表不能修改该如何处理?换句话说,你不能对列表中的节点进行翻转。

 

示例:

输入:(7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 8 -> 0 -> 7

  • 实现: 

首先,我们知道单链表无法像数组一样通过索引值获得其对应的元素,而且我们也不知道其对应的长度(有多少个元素)。对于加法实现,依次是:个位->十位->百位->....,然后个位在链表的末尾,我们无法直接获取,因此我们需要对链表进行遍历,但是当遍历到末尾时,由于链表内部存储的是下一个节点的地址,所以我们无法获得十位,百位...,因此我们在遍历的同时需要对数据进行存储,什么数据结构能满足此过程呢?答案是栈(先进后出),我们在遍历的同时,将遍历的结果压入栈中,然后在依次出栈,进行加法操作,并对其进行重构链表,但是我们需要一个正序的链表,而不是倒序,因此我们可以对栈进行翻转,这里我又使用了一次栈结构,将重构的链表节点存储在栈中,然后再依次出栈连接起来。

/**
 * 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<ListNode* > s_L;
        stack<int> s1,s2;
        //遍历链表,压入栈中
        ListNode* l1_temp=l1;
        while(l1_temp)
        {
            s1.push(l1_temp->val);
            l1_temp=l1_temp->next;

        }
        ListNode* l2_temp=l2;
        while(l2_temp)
        {
            s2.push(l2_temp->val);
            l2_temp=l2_temp->next;
        }
        //出栈,进行加法操作
        int flag=0;//进位标志
        while(!s1.empty()||!s2.empty())
        {
            ListNode* new_node=new ListNode;
            if(s1.empty())
            {
                new_node->val=0+s2.top()+flag;
                // cout<<"val: "<<new_node->val<<endl;
                s2.pop();
            }               
            else if(s2.empty())
            {
                new_node->val=s1.top()+0+flag;
                // cout<<"val: "<<new_node->val<<endl;
                s1.pop();
            }
            else
            {
                new_node->val=s1.top()+s2.top()+flag;              
                // cout<<"val: "<<new_node->val<<endl;
                s1.pop();
                s2.pop(); 
            }
            //判断是否进位
            if(new_node->val>=10)
            {
                new_node->val-=10;
                flag=1;
            }
            else
                flag=0;
            s_L.push(new_node);
        }
        //重构链表
        ListNode* res=new ListNode;
        ListNode* temp=res;
        //如果进位标志还存在,新建一个节点
        if(flag)
        {           
            res->val=flag;
        }
        //如果不存在,则取栈顶元素
        else
        {
            res->val=s_L.top()->val;
            s_L.pop();
        }
        while(!s_L.empty())
        {
            temp->next=s_L.top();
            temp=temp->next;
            s_L.pop();
        }
        return res;

    }
};

 

2.快慢指针

1.返回倒数第K个节点(leetcode 面试题22)(剑指Offer 22

  • 题目描述:

实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。

注意:本题相对原题稍作改动

示例:

输入: 1->2->3->4->5 和 k = 2
输出: 4

说明:给定的 n 保证是有效的。

  • 实现:
  1. 首先,快、慢指针同时指向head;
  2. 其次,快指针比慢指针多走K步;
  3. 最后,快指针和慢指针同步前进,直到快指针到达末尾。
  4. 快指针的主要目标:遍历整个单向链表。
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    int kthToLast(ListNode* head, int k) {
        
        ListNode* slow=head;
        ListNode* fast=head;
        while(k--)
        {
            fast=fast->next;
        }
        while(fast)
        {
            fast=fast->next;
            slow=slow->next;
        }
        return slow->val;

    }
};

2.链表的中间节点(leetcode 876

  • 题目描述:

给定一个带有头结点 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例 1:

输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

示例 2:

输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。

提示:

给定链表的结点数介于 1 和 100 之间。

  • 实现:

由于要返回链表的中间节点,就需要知道链表的头到中间节点的长度,因为链表的长度未知。

  • 常规作法:
  1. 遍历整个链表,知道链表的长度;
  2. 通过链表的长度得到链表中间节点的位置;
  3. 然后再次遍历到中间位置,返回该节点。
  • 快慢指针做法:
  1. 使用慢指针 slow 和快指针fast 两个指针同时遍历链表;
  2. 快指针一次前进两个结点,速度是慢指针的两倍;
  3. 直到fast和fast->next为空。

快慢指针不同速度前进(动图)

  •  常规做法:
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        int k=0;
        ListNode* temp=head;
        while(temp)
        {
            k++;
            temp=temp->next;
        }
        k=k/2+1;
        // cout<<k<<endl;
        ListNode* slow=head;
        ListNode* fast;
        while(--k)
        {
            slow=slow->next;
        }
        // cout<<slow->val<<endl;
        return slow;

    }
};
  • 快慢指针:
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode* slow=head;
        ListNode* fast=head;
        while(fast&&fast->next)
        {
            fast=(fast->next)->next;
            slow=slow->next;
        }
        return slow;
    }
};

3.相交链表(leetcode 160)(剑指offer 52)

  • 题目描述:

编写一个程序,找到两个单链表相交的起始节点。

如下面的两个链表:

在节点 c1 开始相交。

示例 1:

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
 

示例 2:

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
 

示例 3:

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。
 

注意:

如果两个链表没有交点,返回 null.
在返回结果后,两个链表仍须保持原有的结构。
可假定整个链表结构中没有循环。
程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。

  • 分析:

这道题题目稍微有点难懂,相交指的是节点的地址相同而不是值相同。

  1. 对于不相交的情况可以将其看作相交节点为NULL的情况。
  2. 对于相交的第一个节点后面的所有结点,快慢指针走的步数是相同的;
  3. 对于不相交的部分,快指针走完A+快指针走完B=慢指针走完B+慢指针走完A,总的步数是一样的,所以会相遇。
  4. 最后只需判断相遇结点是否为空。
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(headA==NULL||headB==NULL)
            return NULL;
        ListNode* cur1=headA;
        ListNode* cur2=headB;
        while(cur1!=cur2)
        {
            cur1=cur1?cur1->next:headB;
            cur2=cur2?cur2->next:headA;
        }
        return cur1;
    }
};

4.环形链表(leetcode 141

  • 题目描述:

给定一个链表,判断链表中是否有环。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

 

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。


示例 2:

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。


示例 3:

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

进阶:

你能用 O(1)(即,常量)内存解决此问题吗?

  • 分析:
  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 hasCycle(ListNode *head) {
        if(head==NULL||head->next==NULL)
            return false;
        ListNode* fast=head->next;
        ListNode* slow=head;
        while(fast!=slow)
        {
            if(fast==NULL||fast->next==NULL)
                return false;
            fast=fast->next->next;
            slow=slow->next;
        }
        return true;
    }
};

 

 

 

 

3.删除节点

1.删除中间节点(面试题02.03

  • 题目描述:

实现一种算法,删除单向链表中间的某个节点(即不是第一个或最后一个节点),假定你只能访问该节点。

示例:

输入:单向链表a->b->c->d->e->f中的节点c
结果:不返回任何数据,但该链表变为a->b->d->e->f

  • 分析:
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    void deleteNode(ListNode* node) {
        node->val=node->next->val;
        node->next=node->next->next;
    }
};

2.删除排序链表中重复元素(leetcode 83

  • 题目描述:

给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。

示例 1:

输入: 1->1->2
输出: 1->2


示例 2:

输入: 1->1->2->3->3
输出: 1->2->3

  • 分析:
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        
        ListNode* fast=head;
        while(fast&&fast->next)
        {
            if(fast->val==fast->next->val)
                fast->next=fast->next->next;
            else
                fast=fast->next;
        }
        return head;
    }
};

 

 

 

 

 

1.反转链表(leetcode 24,206

  • 题目描述:

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
  • 实现: 
  1. 定义三个指针: pre 、cur 、next。
  2. pre 在前 ,cur在中,next在后。
  3. next暂存cur->next的地址。
  4. 让cur 的 next 指向 pre ,实现一次局部反转。
  5. 局部反转完成之后, pre 和 cur 同时往前移动一个位置。
  6. 循环上述过程,直至 cur 到达链表尾部。

迭代.gif

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre=NULL;
        ListNode* cur=head;
        ListNode* next;
        while(cur)
        {
            next=cur->next;
            cur->next=pre;
            pre=cur;
            cur=next;
        }
        return pre;

    }
};

2.K个一组反转链表(leetcode 25)

  • 题目描述:

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

示例 :

给定这个链表:1->2->3->4->5

当 k = 2 时,应当返回: 2->1->4->3->5

当 k = 3 时,应当返回: 3->2->1->4->5

  • 实现:

本题的实现步骤和上一道题反转链表类似,只不过这是一个子过程,当K个数反转完后,剩余的部分使用递归重复前面的过程,直到剩余的部分的节点数少于K时,返回头指针(递归结束)。 

/**
 * 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) {
        //判断剩余的链表节点数是否还有K个
        ListNode* temp=head;
        for(int i=0;i<k;i++)
        {
            //如果链表已经遍历到末尾,但是未满K个,直接返回head
            if(temp==NULL)
                return head;
            temp=temp->next;
        }
        //从head处开始反转
        ListNode* pre=NULL;
        ListNode* cur=head;
        ListNode* next;
        int t=0;
        while(t!=k)
        {
            next=cur->next;
            cur->next=pre;
            pre=cur;
            cur=next;
            t++;
        }
        //递归调用
        head->next=reverseKGroup(cur,k);
        return pre;
    }
};

3.反转从位置 m 到 n 的链表(leetcode 92

  • 题目描述:

反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。

说明:
1 ≤ m ≤ n ≤ 链表长度。

示例:

输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL

  • 实现:
  1. 判断m是不是头节点,如果是,那么直接按反转前n-m+1个节点
  2. 如果不是,先找到需要反转的起始节点的前一个节点,保存该节点。
  3. 对后面的节点进行反转;
  4. 连接各部分 

参考:https://leetcode-cn.com/problems/reverse-linked-list-ii/solution/bu-bu-chai-jie-ru-he-di-gui-di-fan-zhuan-lian-biao/

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int m, int n) {
        ListNode* pre=head; 
        ListNode* cur; 
        ListNode* next;
        int s;
        //反转前N-M+1个结点
        if(m<2)
        {
            pre=NULL;
            cur=head;
            s=n-m+1;
            while(cur&&s--)
            {
                next=cur->next;
                cur->next=pre;
                pre=cur;
                cur=next;
            }
            //反转后的尾部指向未反转部分的头部
            head->next=cur;
            head=pre;
        }
        //m!=1           
        else
        {
            //先找到反转的起始点
            int t=m-2;
            while(t--)
            {
                pre=pre->next;
            }
            //保存前m个的末尾
            ListNode* temp=pre;
            //反转的起点
            cur=pre->next;
            s=n-m+1;
            while(cur&&s--)
            {
                next=cur->next;
                cur->next=pre;
                pre=cur;
                cur=next;
            }
             //连接
            (temp->next)->next=cur;
            temp->next=pre;
        }
        return head;

    }
};

6.合并两个有序链表(leetcode 21

  • 题目描述:

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

  • 分析:

这道题和归并排序的合并操作类似。首先,我们需要合并两个链表,那么就需要比较链表中的元素,这时我们需要两个指针用来移动索引链表的节点再比较,合并后的链表也需要一个指针来进行索引。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode  head; 
        ListNode* pointL1=l1;
        ListNode* pointL2=l2;
        ListNode* point=&head;
        while(pointL1||pointL2)
        {
            if(pointL1==NULL)
            {
                point->next=pointL2;
                point=point->next;
                pointL2=pointL2->next;
            }
            else if(pointL2==NULL)
            {
                point->next=pointL1;
                point=point->next;
                pointL1=pointL1->next;
            }
            else if(pointL1->val>pointL2->val)
            {
                point->next=pointL2;
                point=point->next;
                pointL2=pointL2->next;
            }
            else
            {
                point->next=pointL1;
                point=point->next;
                pointL1=pointL1->next;
            }
        }
        return head.next;
    }
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

火柴的初心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值