文章目录
手把手刷链表算法
递归秒杀反转链表–但效率上不如迭代
1、反转整个链表
先直接看代码实现
// 定义:输入一个单链表头结点,将该链表反转,返回新的头结点
ListNode* reverse(ListNode* head) {
// base case---找到最最开始的情况,从此情况开始往回返
if (head == nullptr || head->next == null) {
return head;
}//如果链表为空或者只有一个节点的时候,反转结果就是它自己,直接返回即可
ListNode* last = reverse(head->next);//1先找到翻转后的头节点
head->next->next = head;//2将翻转后的头节点接到原先的头节点上
head->next = nullptr;//3将原先的头节点指向空null
return last;
}
1、
2、
3、
2、反转链表前N个节点
【题目】
ListNode* successor = nullptr; // 后驱节点
// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode* reverseN(ListNode* head, int n) {
// base case---找到最最开始的情况,从此情况开始往回返
if (n == 1) {
// 记录第 n + 1 个节点
successor = head->next;
return head;
}
// 以 head->next 为起点,需要反转前 n - 1 个节点
ListNode* last = reverseN(head->next, n - 1);
head->next->next = head;
// 让反转之后的 head 节点和后面的节点连起来
head->next = successor;
return last;
}
其与第一个具体的区别:
1、base case 变为 n == 1,反转一个元素,就是它本身,同时要记录后驱节点。
2、刚才我们直接把 head.next 设置为 null,因为整个链表反转后原来的 head 变成了整个链表的最后一个节点。但现在 head 节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 successor(第 n + 1 个节点),反转之后将 head 连接上。
3、反转链表的一部分—重点理解!
现在解决我们最开始提出的问题,给一个索引区间 [m, n](索引从 1 开始),仅仅反转区间中的链表元素。
首先,如果 m == 1,就相当于反转链表开头的 n 个元素嘛,也就是我们刚才实现的功能。
如果 m != 1 怎么办?如果我们把 head 的索引视为 1,那么我们是想从第 m 个元素开始反转对吧;如果把 head.next 的索引视为 1 呢?那么相对于 head.next,反转的区间应该是从第 m - 1 个元素开始的;
【总结】就是递归进去,因为反转开头的n个元素会帮我们把后面都处理好,因此我们就当作后面已经处理好了,返回处理好的,连接起来,最后把head再返回就可以了。递归的好处就完全体现出来了,都不用记录head。
ListNode* reverseBetween(ListNode* head, int m, int n) {
// base case---找到最最开始的情况,从此情况开始往回返
if (m == 1) {
return reverseN(head, n);
}
// 前进到反转的起点触发 base case
//当左边的坐标减1时,右边勿忘也需要减1
head->next = reverseBetween(head->next, m - 1, n - 1);
return head;
}
值得一提的是,递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)。所以递归操作链表可以作为对递归算法的练习或者拿去和小伙伴装逼,但是考虑效率的话还是使用迭代算法更好。
如何K个一组反转链表–迭代反转链表
先简化问题,如果给定链表头结点,那么如何反转整个链表呢?
其实有了上面的递归操作后,这个很好解决!
// 反转以 head 为头结点的链表
ListNode* reverse(ListNode* head) {
ListNode* pre, cur, next;
pre = nullptr; cur = head;
while (cur != nullptr) {
next = cur->next;
// 逐个结点反转
cur->next = pre;
// 更新指针位置
pre = cur;
cur = next;
}
// 返回反转后的头结点
return pre;
}
若是反转从head到b的之间的节点,只需要把while里面的条件改成b和pre里的初始值改为end就可以了。
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* end = head;
for(int i=0;i<k;++i)
{
//如果凑不够n个了,就别去递归了
if(end==nullptr) return head;
end = end->next;
}
//最终返回新头
ListNode* new_head = reverse(head,end);
//每轮调换完了别忘记反转后的也要指向后面调换好的新头
head->next = reverseKGroup(end,k);
return new_head;
}
//下面使是反转[head,end)之间链表的反转步骤,四步轻松秒杀
ListNode* reverse(ListNode* head,ListNode* end)
{
ListNode* cur = head;
ListNode* pre = end;
while(cur!=end)
{
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
};
如何判断回文链表–链表的前序后序!
首先,判断一个字符串是不是回文串就简单很多,不需要考虑奇偶情况,只需要双指针技巧,从两端向中间逼近即可。
bool isPalindrome(String s) {
// 一左一右两个指针相向而行
int left = 0, right = s.size() - 1;
while (left < right) {
if (s[left] != s[right]) {
return false;
}
left++;
right--;
}
return true;
}
而这道题的关键在于,单链表无法倒着遍历,无法使用双指针技巧。常理,可以靠反转链表来判断,但是这里我们借助二叉树后序遍历的思路,不需要显式反转原始链表也可以倒序遍历链表:
void traverse(ListNode* head) {
// 前序遍历代码
traverse(head->next);
// 后序遍历代码
}
代码如下所示,就是利用递归。
class Solution {
public:
ListNode* front;
bool isPalindrome(ListNode* head) {
front = head;
return cheak(head);
}
bool cheak(ListNode* back){
if(back==nullptr) return true;
bool ans = cheak(back->next);
// 后序遍历代码
ans = ans && (front->val == back->val);
front = front->next;
return ans;
}
};
还有一个不错的算法,直接看代码吧,中点+翻转+双指针判断
class Solution {
public:
bool isPalindrome(ListNode* head) {
//第一步,快慢指针找链表中点
ListNode* slow=head,*fast =head;
while(fast!=nullptr && fast->next!=nullptr)
{
slow = slow->next;
fast = fast->next->next;
}
//这一步主要是因为链表节点的奇偶个数
if(fast!=nullptr) slow = slow->next;
//第二步,反转后面的部分
ListNode* new_head = reverse(slow);
//第三步,前后对比
while(new_head!=nullptr)
{
if(new_head->val != head->val) return false;
head = head->next;
new_head = new_head->next;
}
return true;
}
ListNode* reverse(ListNode* head)
{
ListNode* pre = nullptr;
ListNode* cur = head;
while(cur != nullptr)
{
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
};