1 基础
链表反转总共有两种,一种是带头节点反转,一种是不带头节点反转。都需要好好掌握。
1.1 建立虚拟头节点辅助反转
很多时候一张图就能说明一切。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J4fU9ttA-1690014664153)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/aaff52dc-e791-439e-9de1-ec9b2c089a25/Untitled.png)]
具体实现的过程中,要注意虚拟节点的头一开始创建的时候不需要连接head!
(虽然我也不知道为什么我会这样做,但我的的确确就是这么做了)
下面给出代码
/**
* 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* ans = new ListNode(-1);
ListNode* cur = head;
while(cur!=NULL)
{
ListNode* next = cur->next;
cur->next = ans->next;
ans->next = cur;
cur = next;
}
return ans->next;
}
};
1.2 直接操作链表实现反转
看图就懂了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DvzG8SBl-1690014664154)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/718bbf46-efee-4c1a-9544-0a40bb9b4f16/Untitled.png)]
对图进行代码实现
/**
* 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* prev = NULL;
ListNode* cur = head;
while(cur != NULL)
{
ListNode* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
};
为什么不需要在开头判断head
是否为NULL
?
因为如果head
为NULL
,那么cur
为NULL
,循环直接不会进入,直接最后返回prev = NULL
,结果正确。
1.3 递归(Fantastic!)
搞清楚关于函数体的两个事:
- 代码输入是什么?
- 代码返回结果是什么?
从链表反转这个具体例子来看,现在的代码是这样的:
/**
* 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) {
}
};
对这个函数输入链表头,这个函数的返回值是反转后的链表的表头。
那么我们尝试构建一个链表如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BTVv0ujY-1690014664155)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b0c96593-3e2e-472a-9d60-edaaa954e859/Untitled.png)]
要反转后6个节点,必须先反转后5个节点,然后拼接head和新表;
要反转后5个节点,必须先反转后4个节点,然后拼接head和新表;
以此类推
用代码写出来就是 ListNode* new_head = reverselist(head→next);
返回值用new_head
保存下来,每次函数操作head
的下一个节点,将下一个节点作为表头的链表反转然后返回新的表头。
那么现在图解变成了下面这样:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-crtEgldl-1690014664155)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b068e919-b636-4e37-9806-cc12a24d9622/Untitled.png)]
怎么把这两个链表连接起来呢?
首先要明白一个事情,就是head
其实后面链表都是正常的,只是没有画出来,通过head
依旧可以遍历整个链表。因为这个函数本身就没有对head
进行操作什么,链表当然不会莫名其妙在head
处断掉了。
head->next->next = head->next;
把节点(2)的下一个接入head(1)
head-next = NULL
把head(1)的下一个指向NULL
return new_head;
返回答案
最后考虑base情况(链表为空的情况)||(链表只有一个节点,不需要反转的情况)
if(head == NULL || head->next == NULL) return NULL;
最后整合为完整代码
/**
* 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) {
if(head == NULL || head->next == NULL)
return head;
ListNode* new_head = reverseList(head->next);
head->next->next = head;
head->next = NULL;
return new_head;
}
};
毕竟,你的脑袋能压几个栈?
2 拓展问题
2.1 指定区间反转
与上面不同的是,这里需要做指定区间的链表反转
2.1.1 头插法
一句话说明头插法就:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8vgLPNxO-1690014664155)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fa5e4419-8dd9-40e9-9444-95c76a1b8219/Untitled.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ULM5Cpu3-1690014664156)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9e5a7f31-b2d5-4570-9044-383bc96fba35/Untitled.png)]
为什么这个方法叫做头插法呢?
因为必须要创建一个虚拟节点:首先如果是第一个节点就在反转区间内,找不到其前一个节点。所以必须要创建一个新的虚拟头。
同时注意操作次数,因为已经明确了区间范围,所以直接用for循环计数进行操作。
下面给出代码:
/**
* 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* reverseBetween(ListNode* head, int left, int right) {
ListNode* dummy = new ListNode(-1, head);
ListNode* prev = dummy;
for(int i=0; i<left-1; i++)
prev = prev->next;
ListNode* cur = prev->next;
ListNode* next;
for(int i = 0; i<right-left; i++)
{
next = cur->next;
cur->next = next->next;
next->next = prev->next;
prev->next = next;
}
return dummy->next;
}
};
2.1.2 穿针引线法
简单来说就是:先剪断,再把剪断的部分反转之后连接上去。
既然要完成拼接,那么需要的四个指针不能少
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ssUdouhK-1690014664156)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a12cb0b3-4031-4404-8b6e-d14b3a88ae64/Untitled.png)]
反转可以写最简单的递归
下面给出代码就好
/**
* 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* reverseBetween(ListNode* head, int left, int right) {
ListNode* dummy = new ListNode(-1, head);
ListNode* pre = dummy;
ListNode* succ = dummy;
ListNode* old_head = dummy;
ListNode* new_head =dummy;
for(int i=0; i<left-1; i++)
pre = pre->next;
for(int i=0; i<right; i++)
new_head = new_head->next;
old_head = pre->next;
succ = new_head->next;
pre->next = NULL;
new_head->next = NULL;
new_head = reverseList(old_head);
pre->next = new_head;
old_head->next = succ;
return dummy->next;
}
ListNode* reverseList(ListNode* head)
{
if( head == NULL || head->next == NULL)
return head;
ListNode* new_head = reverseList(head->next);
head->next->next = head;
head->next = NULL;
return new_head;
}
};
2.2 两两交换链表中的节点
首先看图理解题意
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yDpR6iXG-1690014664156)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6802508b-4921-42d9-bf09-108564c87c5a/Untitled.png)]
显然是需要创建虚拟头节点的,然后想到了之前做的指定区间反转的题目,这里区间长度就是2;
难度不大,都是基础操作。
每次取出一个节点来往回连接,然后进入下一个节点就好。
/**
* 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* swapPairs(ListNode* head) {
ListNode* dummy = new ListNode(-1, head);
ListNode* pre = dummy;
ListNode* cur = dummy->next;
while(cur!=NULL && cur->next!=NULL)
{
ListNode* next = cur->next;
cur->next = next->next;
pre->next = next;
next->next = cur;
pre = cur;
cur = cur->next;
}
return dummy->next;
}
};
需要注意的就是while
的循环条件判断。
首先cur
不能为空,考虑空链表的情况,其次cur→next
不能为空,因为while
内涉及到cur→next→next;
2.2 链表加一
2.2.1 stack
因为链表是从高位到低位连接的,而加法是从低位到高位,所以想到栈的数据结构
先把所有的节点的val值压入栈,每次弹出的就是从末尾开始了,然后逐渐构建出一个新的链表。
ListNode* ListAddofStack(ListNode* head)
{
stack<int> st;
while(head!=NULL)
{
st.push(head->val);
head = head->next;
}
int carry = 0;
ListNode* dummy = new ListNode(-1);
int adder = 1;
while(!st.empty() || adder!=0 || carry>0;)
{
int digit;
if(st.empty()) digit = 0;
else {digit = st.top(); st.pop();}
int sum = digit + carry + adder;
carry = sum>=10? 1 : 0;
sum = sum>=10? sum-10 : sum;
ListNode* cur = new ListNode(sum, dummy->next);
dummy->next = cur;
adder = 0;
}
return dummy.next;
}
2.2.2 reverse
基于链表反转实现。先将链表反转,然后从头加1,得到新的链表,再反转回来就好。思路很简单
等会有一个链表加法的题目,这里就不过多赘述了~
2.3 链表加法
区分于上面那道单一加法的题目
这个就是两个链表直接相加,难度差不多
2.3.1 stack
利用栈这个数据结构的特性进行操作,每一次存储的是一个节点
如果有的节点已经操作完了,那么默认创建的就是val = 0 的节点
while循环判断的终点就是,只要有东西可以加,就不离开循环,直到两个栈都空了并且进位为0。
然后整体采取的是头插法,不需要进行链表反转
头插法的意思就是每一次插入在头节点之前,尾插法就是每一次插入在节点的末尾。
/**
* 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) {}
* };
*/
#include<stack>
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
stack<ListNode*> st1;
stack<ListNode*> st2;
while(l1!=NULL)
{
st1.push(l1);
l1 = l1->next;
}
while(l2!=NULL)
{
st2.push(l2);
l2 = l2->next;
}
ListNode* new_head = new ListNode(0);
int carry = 0;
while(!st1.empty() || !st2.empty() || carry != 0)
{
ListNode* tmp1 = new ListNode(0);
ListNode* tmp2 = new ListNode(0);
if(!st1.empty())
{
tmp1 = st1.top();
st1.pop();
}
if(!st2.empty())
{
tmp2 = st2.top();
st2.pop();
}
int sum = tmp1->val + tmp2->val + carry;
carry = sum/10;
int ans = sum%10;
ListNode* cur = new ListNode(ans);
cur->next = new_head->next;
new_head->next = cur;
}
return new_head->next;
}
};
2.3.2 reverse
通过链表反转操作,先把两个链表都反转一遍,然后再加起来,用头插法,最后就不用再反转一次了。
/**
* 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* head1 = reverseList(l1);
ListNode* head2 = reverseList(l2);
ListNode* dummy = new ListNode(-1);
int carry = 0;
while(head1!=NULL || head2!=NULL || carry!=0)
{
int sum = 0;
if(head1!=NULL)
{
sum+=head1->val;
head1 = head1->next;
}
if(head2!=NULL)
{
sum+=head2->val;
head2 = head2->next;
}
sum+=carry;
carry = sum/10;
ListNode* new_node = new ListNode(sum%10);
new_node->next = dummy->next;
dummy->next = new_node;
}
return dummy->next;
}
ListNode* reverseList(ListNode* head)
{
ListNode* dummy = new ListNode(-1);
while(head!=NULL)
{
ListNode* next = head->next;
head->next = dummy->next;
dummy->next = head;
head = next;
}
return dummy->next;
}
};
2.4 回文链表的反转问题(拓展)
剑指 Offer II 027. 回文链表 - 力扣(LeetCode)
核心思想就是一边遍历,一边反转。
而遍历和反转都是已经学过的内容,相当于做一个整合,难度不大
/**
* 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:
bool isPalindrome(ListNode* head) {
if(head == NULL || head->next == NULL)
return true;
ListNode* prepre = NULL;
ListNode* pre = head;
ListNode* slow = head;
ListNode* fast = head;
while(fast!=NULL && fast->next !=NULL)
{
pre = slow;
slow = slow->next;
fast = fast->next->next;
//下面对前面这段链表进行反转
pre->next = prepre;
prepre = pre;
}
if(fast!=NULL) //@1
{
slow = slow->next;
//这里是处理链表节点数为奇数的情况
}
//下面开始比较节点
while(slow!=NULL)
{
if(slow->val != pre->val)
return false;
slow = slow->next;
pre = pre->next;
}
return true;
}
};
@1 : 这个位置需要注意一下,如果是奇数个,需要避开中间那个对称的唯一节点。
其实把图画出来就很简单了。
slow和fast分别一次一步和一次两步
pre指向slow的前一个节点,prepre指向pre的前一个节点,pre又是反转后的链表的头节点。
每次循环开始前先让pre指向slow,然后slow和fast遍历。
遍历结束之后因为知道pre和prepre的位置。此时prepre是旧的头节点,要把pre接上去,基础操作。
每次都是这样。