2.7 链表的相关算法题及解题模板
对比数组题,链表题可以说是简单不少,主要是花样少,无法考察以下几种:
1.单指针遍历
2.双指针
3.快慢指针:双指针的进化版,快指针每次走两步,慢指针每次走一步,可用于找链表中间节点,找环等。
4.反转链表:建议递归迭代头插法三种方法都掌握。
5.链表节点的删除和连接:牢记口诀,先连后,再连前,(如有删除)最后释放删除节点。
https://leetcode.cn/tag/linked-list/problemset/力扣链表题集
- 408历年考题都是带有头结点的链表,如果是不带头结点的链表就自己创建一个
dummy
结点指向第一个结点,让dummy
结点当作头结点之后再用一样的思路处理即可;当然也可以视情况另作处理,画个图就好做了。- 在做链表题时,要舍得用变量,千万别想着节省变量,否则极有可能把自己绕晕。
2.7.1 删除结点问题
删除结点只要找到待删除结点的直接前驱结点,然后用删除结点的基本操作将待删除结点删除即可;
这部分推荐看一下灵神的视频:删除链表重复节点【基础算法精讲 08】
删除结点问题如果算上倒数第 n n n个结点的话,真题已经考察过2次,我觉得可以重点关注一下后面两道.
- 在带有头结点的链表中删除值为
x
的所有结点
- 【示例】:
- 题源:203. 移除链表元素
- 示意代码:
法一:设置两个指针,一个用于遍历链表,另一个用来标记值为x的结点的直接前驱的位置;
void delete_x(LNode *&L,int x) {
LNode *pre = L, *p = L->next, *q;
//用pre记录当前节点的前驱结点,始终保持在p结点的前一位,然后用p指针来遍历链表;*q指针在后面删除值为x结点时用到;
while(p != NULL) {
if(p -> data != x) { //如果遍历到的当前结点值不为x;
pre = p; //更新pre指针;
p = p -> next; //更新p指针,然后继续遍历链表,注意顺序不可以颠倒哦
}else{
q = p; //如果遍历到的当前结点值为x,用q指针标记待删除元素位置;
pre -> next = p -> next; // 删除当前结点;
p = p -> next; //更新p指针;
free(q); //释放掉删除结点所占用的空间,c++中可以用delete(q);
}
}
}
法二:直接用一个指针来遍历链表,当下一个结点值为x时即可执行删除操作;如果该链表是不带头结点的话,可以设置一个dummy结点指向第一个结点然后充当头结点即可;
void delete_x(LNode *&L,int x) {
LNode *p = L, *q;
while(p -> next) {
if(p -> next ->data = x) {
q = p -> next;
p -> next = q -> next;
free(q);
}else{
p = p -> next;
}
}
}
法三: 递归写法(这里是不带头结点的链表,如果是带头结点的话再写一个函数head->next = delete_x即可)
void delete_x(LNode *&L, int x) {
LNode *p;
if(L -> data == x) {
p = L;
L = L -> next;
detele_x(L,x);
}else{
delete_x(L -> next,x)
}
}
如果要删除的元素是值为位于某个区间的话,那么只要修改判断条件语句即可;
- 删除带头结点的链表中值为
x
的第一个结点;
- 【示例】:
- 解题思路:由于本题只要删除值为x的第一个结点,因此如果找到了待删除结点的直接前驱或者已经遍历完整条链表也没有找到时,跳出循环即可;然后根据这两种情况执行相应操作即可;
- 示意代码:
bool delete_first_x(LNode *&L, int x) {
LNode *p = L;
while(p->next->data != x && p -> next != NULL){
p = p -> next;
}
//由判断条件可知,跳出while循环有两种情况;
if(p-> next == NULL) {
return false; //第一种情况,该链表中不含值为x的结点,返回false;
}else{
LNode *q = p -> next;
p -> next = q -> next;
free(q);
return true; //第二种情况,成功找到并删除,返回true;
}
}
- 在带有头结点的链表中删除值最小的那个结点(假设这样的结点存在且唯一)
- 【示例】:
- 解题思路:设置两个指针
p
和minNode
,p
用于遍历链表,minNode
用来寻找值最小的那个结点的直接前驱,如果p->next
结点值比minNode->next
值小,则更新minNode
指针位置,让其移动到当前p
指针所处位置,然后p
指针继续向后遍历比较;如果p->next
结点值比minNode->next
值大的话,则p
指针继续向后遍历,minNode
指针不动;这样的话当p
指针遍历结束时,minNode
指针的直接后继就是值最小的那个结点,然后执行删除操作就可以啦! - 示意代码:
void delete_min(LNode *&L) {
LNode *p = L -> next, minNode = L;
while(p -> next !=NULL) {
if(p -> next -> data > minNode -> next -> data) {
p = p -> next;
}else{
minNode = p; //如果当前遍历结点的直接后继结点值比minNode的直接后继结点值小的话,更新minNode指针;
p = p -> next;
}
}
LNode *q = minNode -> next;
minNode -> next = q -> next;
free(q);
}
若要删除值最大的那个结点,也可以这样用一个
maxNode
指向最大值结点的直接前驱结点;如果题目要求按递增排序顺序输出单链表各节点的数据元素,并释放结点空间,可以考虑用该方法在外面再套一个
while(L -> next)
循环;每次输出值最小的一个并删除即可,因为有两层遍历,因此时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1);当然也可以将链表中的元素依次输入到数组中,再对数组排序输出,这样的话时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),但是空间复杂度为 O ( n ) O(n) O(n),因为用到了额外空间。
- 在有头节点的
有序
链表中删除重复元素;,使每个重复元素只出现一次;(leetcode第83题)
- 示例:
- 解题思路:
- 示意代码:
void delete_same(LNode *&L) {
LNode *p = L -> next, *q; //初始化p指向链表第一个结点,q指针用来在后面指向p的直接后继结点;
while(p->next) {
q = p -> next;
if(q -> data == p -> data) { //发现重复元素
p -> next = q -> next; //执行删除操作;
free(q); //释放结点空间;
}else{ //没有发现重复元素
p = p -> next; //遍历下一个节点;
}
}
}
- 删除排序链表的重复元素,注意是删除所有含有重复数字的结点,只保留下不同的数字。
-
【示例】:
-
解题思路:我们让指针 c u r cur cur指向链表的头结点,随后开始对链表进行遍历。如果当前 c u r . n e x t cur.next cur.next与 c u r . n e x t . n e x t cur.next.next cur.next.next对应的元素相同,那么我们就需要将 c u r . n e x t cur.next cur.next以及所有后面拥有相同元素值的链表节点全部删除。我们记下这个元素值 x x x,随后不断将 c u r . n e x t cur.next cur.next从链表中移除,直到 c u r . n e x t cur.next cur.next为空节点或者其元素值不等于 x x x为止。此时,我们将链表中所有元素值为的 X X X节点全部删除。
如果当前 c u r . n e x t cur.next cur.next与 c u r . n e x t . n e x t cur.next.next cur.next.next对应的元素不相同,那么说明链表中只有一个元素值为 c u r . n e x t cur.next cur.next的节点,那么我们就可以将 c u r cur cur指向 c u r . n e x t cur.next cur.next。
当遍历完整个链表之后,我们返回链表的头结点即可。
需要注意 c u r . n e x t cur.next cur.next以及 c u r . n e x t . n e x t cur.next.next cur.next.next可能为空节点,如果不加以判断,可能会产生运行错误。
-
示意代码:
//带有头结点的版本 LNode *deleteDuplicates(LNode *&L) { LNode *cur = L; //用于遍历链表,将其下下个结点值与其下个结点值比较,这样如果相等的话执行删除操作,那么cur就是待删除结点的直接前驱结点,从而方便了后续的删除操作; while(cur -> next && cur -> next -> next) { int x = cur -> next -> data; //先记录下一个节点的值 if(cur -> next -> next -> data == x) { //如果下下个结点的值和记录值相等时,则进入循环把与记录值相等的结点通通删除; while(cur -> next && cur -> next -> data == x) { //只要cur->next值与记录值x相等,就一直循环执行删除操作; LNode *q = cur -> next; cur -> next = q -> next; delete(q); } }else { //如果下下个结点的值和下个结点的值不相等,则更新cur结点 cur = cur -> next; } } return L; }
虽然看起来函数内部有两重循环,但是注意每一次循环要么删除一个结点,要么cur向右移动一位,也相当于处理了一个结点,因此总的执行次数为 O ( n ) O(n) O(n),因此时间复杂度是 O ( n ) O(n) O(n)的,空间复杂度为 O ( 1 ) O(1) O(1)
暴力方法,先遍历一遍链表记录链表长度,再遍历一次找到其前驱结点,执行删除操作;
最优解:双指针写法;前后指针
示意代码:
//不带头结点
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0,head);
ListNode *left = dummy, *right = dummy;
while(n -- ) {
right = right -> next;
}
while(right -> next) {
left = left -> next;
right = right -> next;
}
left -> next = left -> next -> next;
return dummy -> next;
}
//带头结点:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *left = head , *right = head;
while(n -- ) {
right = right -> next;
}
while(right -> next) {
left = left -> next;
right = right -> next;
}
left -> next = left -> next -> next;
return head;
}
给你一个链表的头节点 head,请你编写代码,反复删去链表中由 总和 值为 0 的连续节点组成的序列,直到不存在这样的序列为止。
删除完毕后,请你返回最终结果链表的头节点。
示例1:
输入:head = [1,2,-3,3,1]
输出:[3,1]
提示:答案 [1,2,1] 也是正确的。
示例2:
输入:head = [1,2,3,-3,4]
输出:[1,2,4]