LeetCode链表类高频题大全

链表的理论基础

  • 首先链表是一种线性表,通过指针将结点串联起来形成的
  • 每一个结点由两部分组成,一部分是值域,另一部分是指针域(指向下一个结点或nullptr)

链表的分类

  • 单链表:结点只有一个指针域,指向下一个结点
  • 双链表:结点有两个指针域,一个指向上一个结点,一个指向下一个结点
  • 循环链表:以循环单链表为例,就是链表头尾相连

链表的存储方式

  • 区别于数组(也叫顺序表)的顺序存储,链表是链式存储,即结点在内存空间中是不连续的,而是通过指针串联

链表的定义

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
  • 这里仅给出单链表的定义,双链表类似,只不过是多加一个pre指针域

性能分析

  • 链表的好处在于插入删除是O(1)时间复杂度,因为不需要对数据进行位移,只需要改变指针指向即可
  • 但链表相对于数组而言不利于随机访问(链表需要从头遍历一遍)

注意事项

  • 在实际做题过程中,但凡涉及到链表的删除或者其他一些场景,建议添加虚拟头结点dummyHead,这样会方便很多
  • 链表是一个递归的结构,所以其实可以用递归的方法来解决问题

实战刷题

  • 首先,鉴于链表是一种数据结构,肯定离不开增删改查,于是就有了这第一题,此题可以对链表有基础的编程能力
  • 接下来,就会围绕着链表的基础操作来展开,各种花式删结点,花式合并结点,绕成环之类的,可好玩了!

707. 设计链表

题目

设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:valnextval 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

示例:

MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2);   //链表变为1-> 2-> 3
linkedList.get(1);            //返回2
linkedList.deleteAtIndex(1);  //现在链表是1-> 3
linkedList.get(1);            //返回3

提示:

  • 0 <= index, val <= 1000
  • 请不要使用内置的 LinkedList 库。
  • get, addAtHead, addAtTail, addAtIndexdeleteAtIndex 的操作次数不超过 2000

题解

class MyLinkedList {
public:
    MyLinkedList() 	//构造函数
    {
        this->size = 0;	//初始长度为0
        this->head = new ListNode(0);	//构造一个空的头结点
    }
    
    int get(int index)	//获取链表指定位置index的值
    {
        if (index < 0 || index >= size) 	//判断是否超出范围越界,index范围是[0,this->size-1]
        {
            return -1;
        }
        ListNode *cur = head;	//生成遍历的指针
        // for (int i = 0; i <= index; i++) 
        // {
        //     cur = cur->next;
        // }
        while (index>=0)	//注意这里是有头结点的,index代表偏移量,这样好理解
        {
            cur=cur->next;
            --index;
        }
        return cur->val;
    }
    
    void addAtHead(int val)	//特殊情况可以当做普遍情况来看待 
    {
        addAtIndex(0, val);	//在头加结点相当于在index=0位置加结点
    }
    
    void addAtTail(int val) //在尾部加结点等于在index=size处加结点
    {
        addAtIndex(size, val);
    }
    
    void addAtIndex(int index, int val) 	//在指定index位置加结点
    {
        if (index > size)	//如果超出size范围,则不做任何操作 
        {
            return;
        }
        index = max(0, index);	//如果index<0,则默认都在头部插入,而不算越界
        size++;	//插入前先将链表长度+1
        ListNode *pred = head;	//用来遍历链表
        for (int i = 0; i < index; i++)	//偏移到index-1位置 
        {
            pred = pred->next;
        }
        ListNode *toAdd = new ListNode(val);	//新建一个结点,赋值
        toAdd->next = pred->next;	//先连上pred->next以免后面的链表丢失
        pred->next = toAdd;	//再把前面的连到新节点上
    }
    
    void deleteAtIndex(int index) 
    {
        if (index < 0 || index >= size)	//越界判断 
        {
            return;
        }
        size--;	//记得链表长度-1
        ListNode *pred = head;
        for (int i = 0; i < index; i++)	//同理偏移到index-1位置 
        {
            pred = pred->next;
        }
        ListNode *p = pred->next;	//记住删除的结点,以免释放内存
        pred->next = pred->next->next;
        delete p;
    }
private:
    int size;	//表示链表长度
    ListNode *head;	//表示链表头结点
};
  • 掌握此题就可以对链表做增删改查了,接下来看一些比较复杂的题


203. 移除链表元素

题目

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例 1:

img

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例 2:

输入:head = [], val = 1
输出:[]

示例 3:

输入:head = [7,7,7,7], val = 7
输出:[]

提示:

  • 列表中的节点数目在范围 [0, 104]
  • 1 <= Node.val <= 50
  • 0 <= val <= 50

题解

//常规方法
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyHead=new ListNode(0,head);	//鉴于此题有删除操作,所以加一个虚拟头结点,以便于统一代码逻辑
        ListNode* temp = dummyHead;	//遍历指针
        while (temp->next != NULL)	
        {
            if (temp->next->val==val)	//两种情况,如果这个值要删,就删,但temp不继续遍历
            {
                ListNode* tmp=temp->next;
                temp->next=tmp->next;
                delete tmp;
            }else	//如果这个值不用删,则遍历到下一个结点,并不是无论如何都要执行这句话
            {
                temp=temp->next; 
            }
        }
        return dummyHead->next;	//最好记得把dummyhead也一起释放掉
    }
};

//递归法
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        if (nullptr==head) return nullptr;
        head->next=removeElements(head->next,val);
        return (head->val)==val?head->next:head;
    }
};


206. 反转链表

题目

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

img

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

img

输入:head = [1,2]
输出:[2,1]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目范围是 [0, 5000]
  • -5000 <= Node.val <= 5000

**进阶:**链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

题解

//递归法翻转链表
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        //该函数返回这个链表反转后的头指针
        //缩小问题规模,对于整个链表来说,反转整个链表等于反转除头结点外所有链表,再屁股后面再连上一个头结点
        if (nullptr==head || nullptr==head->next) return head;
        ListNode* tmp=reverseList(head->next);//缩小问题规模,这个地方虽然是返回指针,但其实内部已经把所有后面的指针都转了方向了,这是我没有理解到的

        //接下来的步骤只有当跳出递归往深了走的时候才会执行,所以此时问题规模是最小的,开始处理
        //跳出时一定至少有两个结点,所以可以用next->next
        head->next->next=head;
        head->next=nullptr;
        return tmp;
    }
};


// 头插法翻转链表
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* cur=head;
        head=nullptr;
        while (cur)	//遍历整个链表
        {
            ListNode* tmp=cur;	//把当前结点取出来
            cur=cur->next;	//走到下一个结点
            tmp->next=head;	//头插法插入
            head=tmp;
        }
        return head;
    }
};


92. 反转链表 II

题目

给你单链表的头指针 head 和两个整数 leftright ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表

示例 1:

img

输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]

示例 2:

输入:head = [5], left = 1, right = 1
输出:[5]

提示:

  • 链表中节点数目为 n
  • 1 <= n <= 500
  • -500 <= Node.val <= 500
  • 1 <= left <= right <= n

进阶: 你可以使用一趟扫描完成反转吗?

题解

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) { //该函数用于翻转链表
        ListNode* cur=head;
        head=nullptr;
        while (cur)	//遍历整个链表
        {
            ListNode* tmp=cur;	//把当前结点取出来
            cur=cur->next;	//走到下一个结点
            tmp->next=head;	//头插法插入
            head=tmp;
        }
        return head;
    }
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        ListNode *dummyHead=new ListNode(0,head),*pre=dummyHead,*suc=dummyHead;
        //找到断掉的链表头的前一个结点A
        for (int i=2;i<=left;++i)
        {
            pre=pre->next;
        }
        //找到断掉的链表的尾部的后一个结点B
        for (int i=1;i<=right;++i)
        {
            suc=suc->next;
        }
        //记录一下要反转的链表的准备工作
        ListNode *tmp=suc->next;
        suc->next=nullptr;  //注意这步很重要
        //翻转链表
        ListNode *after=reverseList(pre->next);
        pre->next=after;    //把头连上
        while (after->next)
        {
            after=after->next;
        }
        after->next=tmp;    //把尾连上
        return dummyHead->next;
    }
};


19. 删除链表的倒数第 N 个结点

题目

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

img

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

**进阶:**你能尝试使用一趟扫描实现吗?

题解

  • 此题关键在于要找到待删除的结点的前一个结点的位置,以下提供两种方法实现
//先完整遍历一次,统计链表长度,再算出下标,一步步挪到指定位置
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead=new ListNode(0,head),*cur=head;
        int len=0;
        while (cur) //数一下整个链表有多长
        {
            len+=1;
            cur=cur->next;
        }
        //比如长度为5,则倒数第一个结点应该要拿到倒数第二个结点才能删除,倒数第二个结点的下标就是3,也就是len-n-1
        int preIndex=len-n-1;
        cur=dummyHead;
        for (int i=0;i<=preIndex;++i)
        {
            cur=cur->next;
        }
        //此时cur->next就是要删除的那个结点
        ListNode* tmp=cur->next;
        cur->next=tmp->next;
        delete tmp;
        return dummyHead->next;
    }
};


//进阶版:如果只让你遍历一次链表,应该怎么解决
//问题还是在于如何遍历到待删结点的前一个结点
//换一句话来说,就是在遍历cur的时候知道什么时候停下来
//于是,就有了双指针法,fast和slow是两个指针,fast领先slow一定距离使得当fast到达链表末尾时,slow恰好在指定位置上
//这里一定要画一个图来理解
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead=new ListNode(0,head);
        ListNode* slow=dummyHead,*fast=dummyHead->next;
        for (int i=1;i<=n;i++)
        {
            fast=fast->next;
        }
        while (fast)	//一直遍历到末尾nullptr
        {
            slow=slow->next;
            fast=fast->next;
        }
        //此时slow->next指向的就是待删除结点
        ListNode* tmp=slow->next;
        slow->next=tmp->next;
        delete tmp;
        return dummyHead->next;
    }
};
  • 接下来又是一道双指针的题目


141. 环形链表

题目

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false

示例 1:

img

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

示例 2:

img

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

示例 3:

img

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

提示:

  • 链表中节点的数目范围是 [0, 104]
  • -105 <= Node.val <= 105
  • pos-1 或者链表中的一个 有效索引

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

题解

//思路是:假如一个链表有环,那么有结点一定会被重复访问,且第一个被重复访问的结点就是这个环的入口
class Solution {
public:
    bool hasCycle(ListNode *head) {
        unordered_set<ListNode*> s;	//记录每一个内存地址是否被遍历过
        while (head)
        {
            if (s.find(head)!=s.end()) return true;	//如果发现有被遍历过的,就直接返回,否则会一直循环
            else s.insert(head);	//没有遍历过,就插入
            head=head->next;
        }
        return false;	//如果整个链表全部遍历完,就会执行这步,说明链表没有环
    }
};


//思路是:如果有环,将其类比于操场,设计两个人,一个快一个慢,同时出发,则一定会碰到快的追上慢的
//但是追上并不是最重要的,最重要的是要发现两个重合,否则在实际代码中是没有办法表示追上的
//这里设计慢的人一次走一步,快的人一次走两步
//为什么能保证重合?答:首先快的一定追得上慢的,其次,这里设置快的一次走两步也是有道理的,比如这道题形成的环有n个结点,如果快指针的步长是n的整数倍+1,则快指针永远也无法碰上慢指针,因为他们相对位置不变,但是设计为2的话,因为环最少也有2个结点,所以不可能复合n的整数倍+1,所以一定会重合
class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode* slow=head,*fast=head;
        while (fast && fast->next)	
        {
            slow=slow->next;
            fast=fast->next->next;
            if (slow==fast) return true;
        }      
        return false;
    }
};


142. 环形链表 II

题目

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

img

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

示例 2:

img

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

示例 3:

img

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

提示:

  • 链表中节点的数目范围在范围 [0, 104]
  • -105 <= Node.val <= 105
  • pos 的值为 -1 或者链表中的一个有效索引

**进阶:**你是否可以使用 O(1) 空间解决此题?

题解

  • 这题与上一题一样,也可以用两种方法
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */

//第一种就是最简单的:如果有结点被二次访问,那么一定有环,且这个第一次被重复访问的结点就是这个环的入口
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        unordered_set<ListNode *> s;
        while (head)
        {
            if (s.find(head)!=s.end())
            {
                return head;
            }else
            {
                s.insert(head);
            }
            head=head->next;
        }
        return nullptr;
    }
};

//通过数学算出来的,当快慢相遇后将快指针移到头结点,快慢指针同步向后移,当再次重合时,就是环的入口
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode *slow=head,*fast=head;
        while (fast && fast->next)
        {
            slow=slow->next;
            fast=fast->next->next;
            if (slow==fast) //说明此时遇到了
            {
                fast=head;
                while (fast!=slow)
                {
                    fast=fast->next;
                    slow=slow->next;
                }
                return fast;
            }
        }
        return nullptr;
    }
};


2. 两数相加

题目

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

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

示例 1:

img

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

示例 2:

输入:l1 = [0], l2 = [0]
输出:[0]

示例 3:

输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

提示:

  • 每个链表中的节点数在范围 [1, 100]
  • 0 <= Node.val <= 9
  • 题目数据保证列表表示的数字不含前导零

题解

  • 这题我之前写过一个版本,还是不如官方题解这样来的清楚
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode *head = nullptr, *tail = nullptr;
        int carry = 0;  //表示进位
        while (l1 || l2)    //当两个链表都没处理完
        {
            int n1 = l1 ? l1->val: 0;   //如果链表处理完了,该位置可以当做0来处理,不影响结果
            int n2 = l2 ? l2->val: 0;
            int sum = n1 + n2 + carry;  //算出和
            if (!head)  //如果还没有头结点,先创建一个,并用尾插法插入数据 
            {
                head = tail = new ListNode(sum % 10);
            } else {
                tail->next = new ListNode(sum % 10);
                tail = tail->next;
            }
            carry = sum / 10;
            if (l1) {   //注意这里一定要判断,如果为空就不可以继续遍历下去了
                l1 = l1->next;
            }
            if (l2) {
                l2 = l2->next;
            }
        }
        if (carry > 0)  //非常重要,如果最后还有进位,必须加一位数 
        {
            tail->next = new ListNode(carry);
        }
        return head;
    }
};


24. 两两交换链表中的节点

题目

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:

img

输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例 2:

输入:head = []
输出:[]

示例 3:

输入:head = [1]
输出:[1]

提示:

  • 链表中节点的数目在范围 [0, 100]
  • 0 <= Node.val <= 100

题解

  • 提供两种解法
//第一种解法:递归
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if (!head || !head->next) return head;	//如果没有结点或者只有一个结点,那么直接返回
        ListNode* tmp=swapPairs(head->next->next);	//留下当前的两个结点,把后面的拿去处理
        ListNode *newHead=head->next;   //因为两两翻转后头结点会变,所以先保存以下头结点以便后序返回
        newHead->next=head; //开始翻转
        head->next=tmp; //注意这里要连上后序的链表
        return newHead;
    }
};

// 第二种方法:遍历
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if (!head || !head->next) return head;  //同上个方法
        ListNode* dummyHead=new ListNode(0,head),*cur=dummyHead;    //为了交换方便,设计一个头结点
        //注意为啥一定要个头结点,因为处理时需要拿到待处理两个元素的前一个元素,才可以处理
        while (cur->next && cur->next->next)    //有两个结点可以处理时
        {
            ListNode* tmp1=cur->next,*tmp2=cur->next->next,*tmp3=cur->next->next->next; //把所有结点单独拎出来
            //开始翻转
            //这里自己画一张图就明白了
            cur->next=tmp2;
            tmp2->next=tmp1;
            tmp1->next=tmp3;
            cur=cur->next->next;
        }
        return dummyHead->next;
    }
};

总结

  • 链表类的题目我认为一定要在纸上画出来,这样会非常清晰,且注意一些技巧即可
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值