链表专题

链表题目的一般做法

  • 对于链表类的题目,一般我们遇到的是单链表或者双链表,对于跳跃表、环状链表或者其他的一些链表我们遇到的不多。对于链表的题目我们画图来进行找思路、找解法都是比较有用的,第一,一定要画图;
  • 当链表的头节点可能被修改或者被删除的时候,我们可以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;
}
}
}
}
}
};

LeetCode刷题 链表专题的更多相关文章

  1. C#LeetCode刷题-链表

    链表篇 # 题名 刷题 通过率 难度 2 两数相加   29.0% 中等 19 删除链表的倒数第N个节点   29.4% 中等 21 合并两个有序链表 C#LeetCode刷题之#21-合并两个有序链 ...

  2. LeetCode刷题 树专题

    树专题 关于树的几个基本概念 1 树的节点定义 2 关于二叉树的遍历方法 2.1 前序遍历 2.2 中序遍历 2.3 后序遍历 2.4 层序遍历 3 几种常见的树介绍 3.1 完全二叉树 3.2 二叉 ...

  3. [Leetcode刷题]——链表

    一.找出两个链表的交点 160.相交链表(easy)2021-01-05 编写一个程序,找到两个单链表相交的起始节点     如下面的两个链表,在c1 处相交: public class Soluti ...

  4. LeetCode刷题 二分专题

    二分专题 二分的题目类型 对于满足二段性的题目的两套模板 模板一 模板如下 模板二 模板如下 解决二分题目的一般流程 LeeCode实战 LC69.x的平方根 解法思路 LC35.搜索插入位置 解法思 ...

  5. LeetCode刷题总结-链表

    LeetCode刷题总结-链表 一.链表     链表分为单向链表.单向循环链表和双向链表,一下以单向链表为例实现单向链表的节点实现和单链表的基本操作. 单向链表 单向链表也叫单链表,是链表中最简单的 ...

  6. LeetCode刷题专栏第一篇--思维导图&时间安排

    昨天是元宵节,过完元宵节相当于这个年正式过完了.不知道大家有没有投入继续投入紧张的学习工作中.年前我想开一个Leetcode刷题专栏,于是发了一个投票想了解大家的需求征集意见.投票于2019年2月1日 ...

  7. LeetCode刷题总结之双指针法

    Leetcode刷题总结 目前已经刷了50道题,从零开始刷题学到了很多精妙的解法和深刻的思想,因此想按方法对写过的题做一个总结 双指针法 双指针法有时也叫快慢指针,在数组里是用两个整型值代表下标,在链 ...

  8. LeetCode刷题总结-数组篇(上)

    数组是算法中最常用的一种数据结构,也是面试中最常考的考点.在LeetCode题库中,标记为数组类型的习题到目前为止,已累计到了202题.然而,这202道习题并不是每道题只标记为数组一个考点,大部分习题 ...

  9. LeetCode刷题总结-树篇(下)

    本文讲解有关树的习题中子树问题和新概念定义问题,也是有关树习题的最后一篇总结.前两篇请参考: LeetCode刷题总结-树篇(上) LeetCode刷题总结-树篇(中) 本文共收录9道题,7道中等题, ...

随机推荐

  1. str.strip(chars)

    strip会去除给定字符串的指定字符,指定字符可以是一个或多个,去除从左右分别进行,没有则忽略,如果需要去除某个中间的字符,必须先去除外围的字符 看几个例子,以s为例,故意设置为非对称结构, s = ...

  2. P6672-[清华集训2016]你的生命已如风中残烛【结论】

    正题 题目链接:https://www.luogu.com.cn/problem/P6672 题目大意 长度为\(m\)的序列\(a\),有\(n\)个数字不是\(0\),其他\(m-n\)个是\(0 ...

  3. Java——多线程编程学习/01

    原文章:http://www.cnblogs.com/QG-whz/p/8351298.html  注:建议去看原博主的文章,单就这个知识点而论,比书本讲的透彻,如有违规,联系必删! 并发环境下进行编 ...

  4. 在昨天夜黑风高的晚上,我偷了隔壁老王的Python入门课件,由浅入深堪称完美!

    隔壁老王是一个资深码农,就业教育事业的秃顶之才昨天我下楼打酱油,看他迎面走来,满目春光我好奇的问道:老王,有什么好事,隔壁小花叫你上门了吗?老王:秘密!!我心想:哎呦~不错啊半晚之时,连猫狗都睡着了, ...

  5. JVM学习笔记——栈区

    栈区 Stack Area 栈是运行时的单位,堆是存储单位,栈解决程序的运行问题,即程序如何执行,如何处理数据. 每个线程在创建时都创建一个该线程私有的虚拟机栈,每个栈里有许多栈帧,一个栈帧对应一个 ...

  6. 洛谷2805 [NOI2009]植物大战僵尸 (拓扑排序+最小割)

    坚决抵制长题面的题目! 首先观察到这个题目中,我们会发现,我们对于原图中的保护关系(一个点右边的点对于这个点也算是保护) 相当于一种依赖. 那么不难看出这个题实际上是一个最大权闭合子图模型. 我们直接 ...

  7. Miller-Rabin学习笔记

    首先给出两个定理: 1.费马小定理 设p是一个素数,a是一个整数,且不是p的倍数,那么 \(a^{p−1} \equiv\ 1 \pmod p\) 2.二次探测定理 若\(p\)是素数,\(x\)是一 ...

  8. 2020.4.6--UCF Local Programming Contest 2017的正式赛

    Problem A : Electric Bill 题目大意:进行电量分级制收费,1000kwh及以下一档收费,1000kwh以上按另一档收费,给出每个人的电量总额,问每人应支付多少钱. 思路:基础i ...

  9. hadoop学习笔记:运行wordcount对文件字符串进行统计案例

    文/朱季谦 我最近使用四台Centos虚拟机搭建了一套分布式hadoop环境,简单模拟了线上上的hadoop真实分布式集群,主要用于业余学习大数据相关体系. 其中,一台服务器作为NameNode,一台 ...

  10. WEB安全指南

    说明:本文是Mozilla Web应用部署文档,对运维或者后端开发团队的部署行为进行指导.该部署安全规范内容充实,对于部署有很大意义.同时也涉及到了许多web前端应用安全的基本知识,如CSP, TOK ...