链表问题
前言
链表问题因为涉及指针,出现错误时通常为段错误(由于访问越界引起的错误)如果没有外用编译器或者GDB是很难debug的,由于野指针,悬浮指针等问题,导致在C/C++中尤其难以debug。而如果对于链表操作不熟练,如节点连接,节点删除那就更难以处理出错。
链表基本操作
以单链表为例
链表创建
链表创建有头插法和尾插法两种方法,题做多了,就能根据不同的情境使用不同的方法。
下述操作基于此结构体节点:
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
头插法
/* head开始有n个节点
*
* 头插法 每新建一个节点都接到头节点head之后
* 连接的方法为 把头节点head之后的节点接到新节点后面即 node->next=head->next;
* 把新节点接到头节点head之后即 head->next=node;
*/
ListNode *headInsert(int n)
{
ListNode *head=new ListNode(1);
for(int i=2;i<=n;++i){
ListNode *node=new ListNode(i);
node->next=head->next;
head->next=node;
}
return head;
}
尾插法
/*head开始有n个节点
*
* 尾插法 每新建一个节点都接到尾部tail之后
* 连接的方法为 把新节点接到尾部tail即 tail->next=node;
* 修改尾部tail为新节点node即 tail=node;
*/
ListNode *tailInsert(int n)
{
ListNode *head=new ListNode(1);
ListNode *tail=head;
for(int i=2;i<=n;++i){
ListNode *node=new ListNode(i);
tail->next=node;
tail=node;
}
return head;
}
节点遍历
/*传入一个头节点执行遍历
* cur为遍历时的当前节点
* 访问完一个节点就指针cur后移即 cur=cur->next;
* 边界条件为尾节点为空 跳出
*/
void travel(ListNode *head){
ListNode *cur =head;
while(cur){
cout<<cur->val<<" ";
cur=cur->next;
}
cout<<endl;
}
节点删除
/*传入一个头节点和要删除的节点序号
* 遍历找到该节点位置
* 删除方法为:找到待删除节点的前驱,接上其后继节点
* 假设删除节点为 3 则找到2接上4。
*
* 特别的 n<=0的的情况应该排除
* 超出链表范围的也应该排除
* 头节点没有前驱,只需 head=head->next;
*/
ListNode* del(ListNode *head , int n){
if(n<=0) return head;
ListNode *cur=head;
if(n==1){
head=head->next;
delete cur;
}
else {
for (int i=2;i<=n-1&&cur;++i) {
cur=cur->next;
}
if(!cur||!cur->next)return head;//超出链表长度
ListNode *p=cur->next;
cur->next=cur->next->next;
delete p;
}
return head;
}
双指针法
这个方法是以快慢指针的方法,达到定序的作用。如:
链表中的倒数k个结点
解题思路
让快指针提前走k步,慢指针才开始走。
解题代码
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
ListNode *front=pListHead,*back=pListHead;
unsigned i;
for(i=0;i<k&&front;++i){
front=front->next;
}
if(i<k)return nullptr;
while(front){
front=front->next;
back=back->next;
}
return back;
}
};
分割法
类快排的分割法,对链表进行分割,序列重排。
链表分割
解题思路
新起两个指针,分别连接 <x 和 >=x 的两条链表,最后把大链表接在小链表后面。
特别要注意的是:由于采用在原链表的节点,通过调整其连接序列来新建两个链表,不需要额外空间开销,但需要注意bigHead的尾部可能连接着 <x 的节点所以需要将其置空否则将导致链表成环。
解题代码
//这里没有先建头节点,采用尾插法创建链表
//需要先判断头节点是否建立
//且需要注意边界条件 如:没有 <x或者没有 >=x 的节点
class Partition {
public:
ListNode* partition(ListNode* pHead, int x) {
// write code here
ListNode *smallHead=nullptr,*bigHead=nullptr;
ListNode *p=pHead,*smallp=nullptr,*bigp=nullptr;
while(p){
if(p->val<x){
if(!smallHead){
smallHead=p;
smallp=smallHead;
}else {
smallp->next=p;
smallp=smallp->next;
}
}
else {
if(!bigHead){
bigHead=p;
bigp=bigHead;
}
else{
bigp->next=p;
bigp=bigp->next;
}
}
p=p->next;
}
if(bigp)
bigp->next=nullptr;
if(smallp)
smallp->next=bigHead;
return smallHead?smallHead:bigHead ;
}
};
遍历与创建链表结合
链式A+B
解题思路
1) 首先需要遍历A,B两个链表获得两个值,可采用遍历累加,每一位乘上数位权重
2) 将其和值sum转换为链表。循环(对10)取余、求商,获得每一位的值。
每次获得的值都是sum的低位,且题目要求数位反向存储。那么采用头插法即可。
解题代码
class Plus {
public:
ListNode* plusAB(ListNode* a, ListNode* b) {
// write code here
int A=0,B=0;
int n=0;
while(a){
A+=a->val*pow(10,n);
++n;
a=a->next;
}
n=0;
while(b){
B+=b->val*pow(10,n);
++n;
b=b->next;
}
int sum=A+B;
ListNode *res=new ListNode(-1);
ListNode *p=res;
while(sum){
ListNode *node=new ListNode(sum%10);
p->next=node;
p=node;
sum/=10;
}
return res->next;
}
};
链表翻转 双指针结合
以链表翻转结合双指针来解决经典问题: 回文链表
回文链表
解题思路
如果不看进阶要求,那么只需要用一个栈,来存储数据再比较即可或者转换为回文数组。
进阶:
如何判断回文链表:
找到中间结点:快慢指针,快指针速度为慢指针速度的一半。
翻转其中一半链表,则得到两段一样的链表,遍历判断即可
解题代码
1、简单模拟下链表长度为单数或者双数的情况。
得知以head为起点,如果为双数,快指针最后为空,慢指针位于中点的next。
如果为单数,快指针位于尾节点,慢指针位于中点。
2、翻转链表需要:当前节点的上一个即pre
,而继续往遍历则不能丢掉当前节点的尾巴。
即以pre节点迭代记住前一个节点,同时需要个临时节点记住当前节点的尾巴,以便后移。
3、最后得到pre节点为前半部分翻转链表头,slow为后半链表头
class Solution {
public:
bool isPalindrome(ListNode* head) {
ListNode *fast,*slow;
fast=slow=head;
ListNode *pre=nullptr;
//快慢指针遍历,同时翻转前半部分
while(fast&&fast->next){
fast=fast->next->next;
ListNode *temp=slow->next;
slow->next=pre;
pre=slow;
slow=temp;
}
//如果为单数的情况,需要后移一位
if(fast) slow=slow->next;
//开始判断两段链表是否一致。
while(slow&&pre){
if(slow->val!=pre->val)return false;
else slow=slow->next,pre=pre->next;
}
return (!slow||!pre)?true:false;
}
};
转换路径问题
链表可以看成一条路径,在某些链表问题如:链表相交、链表成环等等 ,我们可以把链表看成路径,问题则可以变成简单的数学问题如:追及问题。
链表相交
解题思路
1、如果通过暴力法写两个循环,比较A链的每个节点与B链的每个节点,那么复杂度将是O(n2)。
换个思路
2、如果把链表看成路径,在假设有两人在路径上竞走,但两人起始地点不一定相同,求其相遇的地点。这个问题怎么解?
评论区有个特别浪漫的说法:我变成你,走过你的路;你变成我,走过我的路,最后我们相遇。即:让先各自遍历链表,到达一方结尾,走另一方的路,最后路程相等:A+B=B+A
只要让两个点遍历链表,判断条件为节点指针是否相等,如果达到一方末端则走另一方的路。
解题代码
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *A=headA, *B=headB;
while(A!=B){
A!=NULL?A=A->next:A=headB;
B!=NULL?B=B->next:B=headA;
}
return A;
}
};
链表环路检测
解题思路
要求:原题需要我们找到环路的入口,如果没有环则返回nullptr。
设环路起点距离head为X,环一圈为Y长。如果求链表的中点我们可以用快慢指针,快指针为慢指针的两倍步长。
1、这里我们如果用快慢指针,如果存在环,他们必在环上相遇。设相遇点距离入口为k,此时快指针已在环上走了n
圈:存在关系2(x+k)=x+k+nY
画简得X+k=nY
得X=(n-1)Y+Y-k
这里Y-k
也是相遇点和入口的距离。
2、那么答案已经呼之欲出了;让快指针回到head按照慢指针的速度走,下次相遇的节点就是环的入口。
3、如果无环节点,快慢指针将在空指针相遇。
解题代码
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if(!head||!head->next)return nullptr;
ListNode *fast,*slow;
fast=slow=head;
while(fast&&fast->next){
fast=fast->next->next;
slow=slow->next;
if(fast==slow) break;
}
if(fast!=slow)return nullptr;//无环
fast=head;
while(fast!=slow){
fast=fast->next;
slow=slow->next;
}
return fast;
}
};