链表专题

链表题目的一般做法

  • 对于链表类的题目,一般我们遇到的是单链表或者双链表,对于跳跃表、环状链表或者其他的一些链表我们遇到的不多。对于链表的题目我们画图来进行找思路、找解法都是比较有用的,第一,一定要画图;
  • 当链表的头节点可能被修改或者被删除的时候,我们可以new一个虚拟的头节点来指向head来避免head被改动的情况;
  • 链表题目的一般有双指针、快慢指针、栈stack、队列queue等来解决;
  • 掌握一些常见的删除节点、增加节点、反转链表、查询节点的方法比较重要。

单链表的结构类型

/**
* 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) {}
* };
*/

删除节点

head->···->pre->cur->next1->next2->···->final

上面所示的是一个三个节点的链表,pre、cur、next分别代表了指向三个节点的指针

我们现在要做的就是删除cur节点,做法有两种,一种是真正意义上的删除,另一种则是寻找一个替死鬼

方法一

方法一所面向的是知道链表的头节点,并且给出要删除的节点,我们直接进行删除即可;方法步骤就是找到pre、cur、next1三个节点,然后使得pre->next=next1即可;

ListNode* deleteNode(ListNode* head,ListNode* cur)
{
if(!head) return NULL;
//先构造一个虚拟头节点指向head,因为删除的节点可能是head,
//我们通过构造虚拟头节点来进行避免删除head之后找不到头节点的情况
auto dummy=new ListNode(-1);
dummy->next=head;
//遍历链表找到要删除的节点cur
auto pre=dummy,p=dummy->next;
while(p && p!=cur)
{
p=p->next;
pre=pre->next;
}
pre->next=p->next;
return dummy->next;
}

方法二

方法二讲述的其实是一个假删除,实际上是采用了替身的方法

由于是只把关键代码或者核心思想进行记录,一些具体的函数形式就没必要在意了

void deleteNode(ListNode* cur)
{
//这种情况是只给出了要删除的节点,并没有给出头节点,所以我们无法找到pre这个节点,
//并且遇到这种解法时,一般不会让你删除final节点,也就是尾节点
auto p=cur->next;
cur->val=p->val;
cur->next=p->next;
}

增加节点

增加节点其实是与删除节点的进行的相反的操作

head->···->pre->next1->next2->···->final

上面所示的是一个三个节点的链表,pre、cur、next分别代表了指向三个节点的指针

我们现在给出一个cur节点,要求其加在pre节点与next1节点之间,或者说加在pre节点后面

	//我们只需要经典三步
auto p=pre->next;//将p->next用p进行保存
pre->next=cur;//添加新的节点
cur->next=p;//将其进行链接

当然还有一些其他比较实用和常考的一些方法,这些将在下面的题目中进行表现

LeedCode实战

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

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

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

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

解法思路

  • 暴力的解法很容易想到进行扫描两遍,第一遍扫描得到链表长度len,第二遍扫描直接循环len-n+1次找到要删除的结点,进行删除即可;
  • 我们更希望的是只通过一次扫描来找到我们要删除的结点,这时我们双指针就很大的作用了,我们采用双指针的一个重要的想法就是利用两个指针的相对位置不变从而来得到这个倒数第N个结点的位置。具体我们的做法如下:
  1. 由于倒数第N个结点可能是head结点,所以我们要先new一个dummy虚拟头节点;
  2. 我们定义两个指针一个first,一个second,其中first走的比较快,second走的比较慢,初始first与second同时指向虚拟头节点dummy;
  3. 我们先让first向链表尾部走N步,这时second指向dummy;
  4. 然后我们让first与second同时往链表尾部进行移动,当first移动到链表尾部的空结点时,second正好是链表的倒数第N+1个结点;
  5. 于是可以参照删除节点的方法进行删除即可;
  6. 最后我们只需要返回dummy->next即可;
  7. 注意本题因为删除的可能是头节点head,所以构建了一个虚拟头节点;

代码如下

class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
auto dummy=new ListNode(-1);
dummy->next=head; auto first=dummy,second=dummy;
while(n--) first=first->next;//将first与second的间距设为N
while(first->next)
{
first=first->next;
second=second->next;
}
second->next=second->next->next;
return dummy->next;
}
};

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

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

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

解法思路

  • 我们可以先考虑对于只有两个节点的链表应该怎么进行交换,然后我们可以将其进行推广;

对于只有两个节点的链表,例如left->right->nullptr

我们在进行交换操作时,首先需要一个虚拟头节点,因为交换操作会改变head,加上dummy后,链表变为dummy->left->right->nullptr

其次我们只要进行交换left,right就可以了,最终返回的为dummy->next;交换的代码如下:

dummy->next=right;
left->next=right->next;
right->next=left;
  • 根据上面的分析,我们只需要用两个指针分别找到要交换的两个节点left、right,以及一个虚拟头节点,当扫描一遍链表后,交换操作也完成,这样时间复杂度为O(N),空间复杂度为O(1);

代码如下

class Solution {
public:
ListNode* swapPairs(ListNode* head) {
auto dummy=new ListNode(-1);
dummy->next=head;
auto left=head,right=head,dum=dummy;//dum为临时虚拟头节点
while(left && left->next)
{
right=left->next;
dum->next=right;
left->next=right->next;
right->next=left;
dum=left;
left=left->next;
}
return dummy->next;
}
};

LC61.旋转链表

LC61.旋转链表

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

解法思路

  • 这个题目可以用环形链表来做,题目中说是将链表的每个节点都向右移动k个位置,并且是循环进行的,多以其实就相当于环形链表的头节点进行移动而已;

下面我们来看看具体的步骤是什么:

  1. 首先我们要将单链表转换为环形链表,并且要记录下链表的长度len,因为k可能大于链表的长度len,每一个链表长度为一个循环,所以我们将k对len求余将减少程序的执行时间;
  2. 其次,我们进行考虑,对于环形链表向右进行移动k%len个位置,其实相当于head向左移动k%len个位置,也就是向右移动len-k%len个位置,这时候我们将head的前面的next置为NULL,返回head即可;

对于每一步其实可以在进行细分

代码如下

class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if(!head || !head->next) return head;
int len=1;
auto p=head;
//求len,并且将单链表变为环形链表
while(p->next)
{
len++;
p=p->next;
}
p->next=head;//将单链表成环
k=len-k%len;//计算要走多少步
while(k--) head=head->next,p=p->next;
p->next=NULL;//将环形链表断开,返回head即可
return head;
}
};

LC83.删除排序链表中的重复元素

LC83.删除排序链表中的重复元素

存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素 只出现一次 。

返回同样按升序排列的结果链表。

解法思路

  • 本题的解法很简单,双指针扫描一遍即可,遇到重复的节点删除即可;

代码如下

class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
auto cur=head;
while(cur)
{
if(cur->next && cur->next->val==cur->val)
cur->next=cur->next->next;
else
cur=cur->next;
}
return head;
}
};

LC206.反转链表

LC206.反转链表

反转一个单链表。

示例:

输入: 1->2->3->4->5->NULL

输出: 5->4->3->2->1->NULL

进阶:

你可以迭代或递归地反转链表。你能否用两种方法解决这道题?

本题给出了反转链表的两大方法,1、递归,2、就地反转

解法一 递归

  • 递归很简单,我们只需要递归到尾节点,然后将next的指向反过来就行了
  • 给个图解

初始链表 head->l1->l2->l3->···->final->nullptr

递归之后 head<->l1<-l2<-l3<-···<-final

结合代码解释

代码如下

class Solution {
public:
ListNode* ans;//设置一个公共变量用来返回结果
ListNode* reverseList(ListNode* head) {
if(!head || !head->next) return head; //如果是空链表或者链表只有一个节点,则不需要进行反转,直接返回head就行
recur(head); //否则进行递归处理
head->next=NULL;
return ans;
}
//递归函数
void recur(ListNode* head)
{
//找到尾节点,将尾节点给ans
if(!head || !head->next)
{
ans=head;
return;
}
//否则继续向下递归
recur(head->next);
//将next进行反转
head->next->next=head;
return;
}
};

解法二 就地反转

  • 就地反转的思想是扫描一遍,完成反转;对于每一次操作,将当前指针的next加到头节点上去,当扫描到尾节点,将尾节点加到头节点则完成反转;
  1. 由于head会进行变化,所以本题也需要一个虚拟头节点dummy
  2. dummy->l1->l2->···->p->pnext->···->final->nullptr
  3. 采用p进行扫描
  4. 将pnext加到头节点上
  5. 具体里面的操作是怎么样的,可以画图进行观察,确定操作的顺序

代码如下

class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(!head || !head->next) return head;
auto dummy=new ListNode(-1);
dummy->next=head;
auto p=head,pnext=head;
while(p->next)
{
pnext=p->next;
p->next=pnext->next;
pnext->next=dummy->next;
dummy->next=pnext;
}
return dummy->next;
}
};

LC92.反转链表II

LC92.反转链表II

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

解法思路

  • 本题与上一题没有本质的区别,只不过是虚拟头节点与循环结束的判断条件发生了改变,其余保持不变就行;

代码如下

class Solution {
public:
ListNode* reverseBetween(ListNode* head, int left, int right) {
if(left==right) return head;
auto dummy=new ListNode(-1);
dummy->next=head;
auto p=dummy,q=dummy,qnext=dummy;
right=right-left;
while(--left) p=p->next;
q=p->next;
while(right--)
{
qnext=q->next;
q->next=qnext->next;
qnext->next=p->next;
p->next=qnext;
}
return dummy->next;
}
};

LC142.环形链表II

LC142.环形链表II

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

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

说明:不允许修改给定的链表。

进阶:

  • 你是否可以使用 O(1) 空间解决此题?

解法思路

快慢指针的典型例题

  • 首先,我们要明确一点,对于一个有环的链表,我们采用一个指针p去扫描时,p会陷入环中,造成死循环,而对于没有环的链表,则当p==NULL时,则遍历完成。
  • 对于这个题,我们首先是判断是否有环,如果有则返回入环的节点;如果没有环就返回null。
  1. 我们考虑没有环的情况,当快慢指针遍历的过程中,如果出现快指针为NULL我们就直接可以判定无环,然后返回null
  2. 对于有环的情况,则由追及问题可以得到,快指针fast一定会在某一点追上慢指针slow。我们假设在经过n步后fast追上slow,并且设有环的部分链表长度为y,环外的链表长度为x;则有公式2n-x=n-x+y,由此公式可以得到,y=n
  3. 我们得到环的长度之后,此时slow指针距离走完整个链表重新进入环中还有x步,所以我们只需要将fast指针重新从头节点单步向下走,当fast==slow时,返回fast或者slow即可,此时fast与slow会在环的入口节点相遇。

代码如下

class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if(!head) return NULL;
auto dummy=new ListNode(-1);
dummy->next=head;
auto fast=dummy,slow=dummy;
int n=0;
while(n==0 || fast!=slow)
{
if(!fast->next || !fast->next->next) return NULL;
fast=fast->next->next;
slow=slow->next;
n++;
}
fast=dummy;
while(fast!=slow)
{
fast=fast->next;
slow=slow->next;
}
return fast;
}
};

LC237.删除链表中的节点

LC237.删除链表中的节点

请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点。传入函数的唯一参数为 要被删除的节点 。

典型的替身攻击问题

  • 具体的解法见上面的删除操作,这里直接给出代码

代码如下

class Solution {
public:
void deleteNode(ListNode* node) {
*(node)=*(node->next);//替身攻击
}
};

LC160.相交链表

LC160.相交链表

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

注意:

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

解法思路

  • 本题同样采用双指针,同样利用路径相等来进行查找相交节点
  • 本题同样要考虑是否有交点的问题,所以同样要分有交点和无交点两种情况进行讨论
  1. 无交点时,我们在采用这样一种方法,我们可以将其转换成有交点的情况,因为交点的存在判断条件是两个链表中有相同节点,我们可以将NULL节点也看成相同的,那么无交点的两个链表可以看成最终以NULL节点为交点的链表。
  2. 有交点时,我们不妨设链表A在相交之前的长度为lenA,同样的链表B在相交之前的长度为lenB,相交之后的链表公共长度为lenC;这样我们使用两个指针pA,pB进行扫描时(pA用来从headA进行,pB用来从headB进行),当扫描到尾节点,立马从另一个链表的表头进行扫描,这样当pA与pB会在交点处相遇。
  3. 原因 lenA+lenC+lenB=lenB+lenC+lenA

代码如下

class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
auto pA=headA,pB=headB;
while(pA!=pB)
{
if(!pA) pA=headB;
else pA=pA->next;
if(!pB) pB=headA;
else pB=pB->next;
}
return pA;
}
};

148.排序链表

148.排序链表

给你链表的头结点 head ,请将其按升序排列并返回排序后的链表

进阶:

你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

解法思路一

暴力解法

  • 通过两次循环进行重新构建一条升序链表最后返回
  • 这样的时间复杂度为O(n^2)

解法思路二

归并排序

  • 归并排序将时间复杂度降低到O(n log n)

代码如下

class Solution {
public:
ListNode* sortList(ListNode* head) {
int n=0;
for(auto p=head;p;p=p->next) n++; auto dummy=new ListNode(-1);
dummy->next=head; for(int i=1;i<n;i*=2)
{
auto cur=dummy;
for(int j=0;j<n;j+=(i*2))
{
auto L=cur->next,R=L;
for(int k=0;k<i*2;++k)
{
if(L->val>R->val)
{
cur->next=R;
L->next=R->next;
R->next=L;
}
}
}
}
}
};

最新文章

  1. [LeetCode] Intersection of Two Arrays 两个数组相交
  2. snmp学习笔记
  3. Process的Waitfor() 引起代码死锁
  4. log4net 记录日志到sqlserver
  5. iOS-CALayer
  6. JavaScript函数柯里化的一些思考
  7. 关系型数据库 VS 非关系型数据库
  8. IE8引用jQuery报$或者jQuery未定义
  9. 报错:/usr/sbin/mysqld: Can&#39;t find file: &#39;./performance_schema/events_waits_summary_by_account_by_event_name.frm&#39; (errno: 13 - Permission denied)
  10. crontab学习
  11. 【高速接口-RapidIO】6、Xilinx RapidIO核仿真与包时序分析
  12. 反调试手法之CreateProcess反调试
  13. AtomicInteger 源码阅读
  14. Oracle如何查询当前的crs/has自启动状态
  15. springmvc StringHttpMessageConverter 中文乱码的几种解决办法(亲测)
  16. 【定制Android系统】Android O 在ROM中添加自己的 so 库(1)——Android.mk 与 Android.bp 的区别【转】
  17. 如何区分Java中的方法重载和重写
  18. fedora装机运行第一脚本
  19. Linux 网络子系统之结构介绍
  20. 微信小程序 - 贝塞尔曲线(购物车效果)

热门文章

  1. js原型和原型链理解 constructor 构造函数
  2. 『Python』matplotlib实现动画效果
  3. Elasticsearch、XXLJob以及最近的学习记录
  4. Sentry 监控 - Distributed Tracing 分布式跟踪
  5. PyCharm插件开发实践-PyGetterAndSetter
  6. MySQL学习总结:提问式回顾 undo log 相关知识
  7. .NET 开发一个服务器 应用管理工具
  8. iNeuOS工业互联网操作系统,设备振动状态监测、预警和分析应用案例
  9. FastAPI 学习之路(二十)接口文档配置相关
  10. FastAPI 学习之路(十三)Cookie 参数,Header参数