链表反转专题 | 新手村

1 基础

链表反转总共有两种,一种是带头节点反转,一种是不带头节点反转。都需要好好掌握。

1.1 建立虚拟头节点辅助反转

206. 反转链表 - 力扣(LeetCode)

很多时候一张图就能说明一切。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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

因为如果headNULL,那么curNULL,循环直接不会进入,直接最后返回prev = NULL,结果正确。

1.3 递归(Fantastic!)

搞清楚关于函数体的两个事:

  1. 代码输入是什么?
  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* 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 指定区间反转

92. 反转链表 II - 力扣(LeetCode)

与上面不同的是,这里需要做指定区间的链表反转

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 两两交换链表中的节点

24. 两两交换链表中的节点 - 力扣(LeetCode)

首先看图理解题意

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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 链表加法

445. 两数相加 II - 力扣(LeetCode)

区分于上面那道单一加法的题目

这个就是两个链表直接相加,难度差不多

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接上去,基础操作。

每次都是这样。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值