文章目录
- 链表的理论基础
- 实战刷题
- [707. 设计链表](https://leetcode.cn/problems/design-linked-list/)
- [203. 移除链表元素](https://leetcode.cn/problems/remove-linked-list-elements/)
- [206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/)
- [92. 反转链表 II](https://leetcode.cn/problems/reverse-linked-list-ii/)
- [19. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/)
- [141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/)
- [142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/)
- [2. 两数相加](https://leetcode.cn/problems/add-two-numbers/)
- [24. 两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/)
- 总结
链表的理论基础
- 首先链表是一种线性表,通过指针将结点串联起来形成的
- 每一个结点由两部分组成,一部分是值域,另一部分是指针域(指向下一个结点或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. 设计链表
题目
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val
和 next
。val
是当前节点的值,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
,addAtIndex
和deleteAtIndex
的操作次数不超过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:
输入: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:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入: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
和两个整数 left
和 right
,其中 left <= right
。请你反转从位置 left
到位置 right
的链表节点,返回 反转后的链表 。
示例 1:
输入: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:
输入: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:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入: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:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入: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:
输入: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:
输入: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;
}
};
总结
- 链表类的题目我认为一定要在纸上画出来,这样会非常清晰,且注意一些技巧即可