【算法手记03】反转链表

反转链表很重要,是链表题中的高频考点,部分题目可能还需要将反转链表抽出来作为一个函数使用。这几种方式都必须熟练手搓。

反转整个链表

lc206.给定单链表头结点,给出反转后的链表。

使用dummy虚拟头结点实现

反转后的新链表带一个dummy结点,将原链表的结点依次插入dummy结点之后,即可完成反转。过程大致如下:
image.png
image.png

下图中,cur作为遍历链表的指针,next指向cur的下一个节点。
image.png

以第一个结点为例,cur结点尾插dummy节点同样遵守"先接管后继,再接入前驱"的准则,先令cur.next指向dummy.next,也就是null;随后令dummy.next指向cur,插入完成。

从上图可以看到,在进行插入之前,我们令cur的后一个结点为next节点,这就便于我们在将前一个节点插入完成后,令cur指针指向next指针。
image.png
最后,考虑边界。
由于引入了dummy节点,因此头节点不需要特殊处理。现在需要考虑循环跳出的条件,即什么时候反转完了。

当cur指针指向最后一个节点,next指针指向null。cur节点插入新链表后,新的cur指针指向原来next指针,也就是null,此时退出循环。也就是循环进入条件应该是cur!=null.
image.png

public ListNode reverseList(ListNode head) {  
    ListNode cur = head;  
    ListNode dummy = new ListNode();  
    while (cur!=null){  
        ListNode next = cur.next;  
        //先接管后继  
        cur.next = dummy.next;  
        //再接入前驱  
        dummy.next = cur;  
        cur = next;  
    }  
    return dummy.next;  
}

直接操作原链表实现

此方法不引入新链表,空间复杂度更低,但也更复杂。
先画出初始和结束的状态:
image.png

下图中,cur作为遍历链表指针,同上。
随后,cur的next指向它的前一个结点prev。初始时,prev为null。随后,prev、cur依次向后移1位。
image.png
当cur指向null时,prev指向链表末尾,此时反转完毕,循环跳出。
image.png
代码如下:

ListNode prev = null;  
ListNode cur = head;  
while (cur!=null){  
    ListNode next = cur.next;  
    cur.next = prev;  
    prev = cur;  
    cur = next;  
}  
return prev;

指定区间反转

lc92
image.png
image.png

本题与lc206反转整个链表的实现方法是类似的。
lc206中,我们将遍历到的原链表的结点依次插到dummy结点的后面,那么本题我们只需要将遍历到的区间节点依次插入到反转区间的第一个位置,也就是反转区间的前驱结点之后即可。

本题指针较多,写题的时候一定要画图,不然很容易把自己搞乱。

过程如下:
image.png

下面来考虑具体细节。我们选取反转区间中间的一部分来讨论实现细节。
如下图。由上面可以知道,cur指针每往前走一步,都要把自身插到pre结点之后。
类似这种更换链表中指定结点的位置的需求,我们一定要确定好结点的前驱和后继,以便它变换位置后,其他顺序不变。
image.png
明白这点之后,开始操作,将cur插入pre之后。

  1. 插入结点操作遵循 “先接管后继,再接入前驱” 原则,cur先将其next指针指向pre的next,随后pre的next指针指向cur。
  2. front的next指针指向next,cur指针移向next,大功告成。
    image.png
    image.png

随后考虑初始化条件。front最开始应该和cur同时指向pre.next。
这是因为,按照上面的方法,第一个节点cur.next指向pre.next时,出现自环。
front指向next时,链表恢复正常。由此可见front最开始应该和cur指向同一个位置。第二次移动之后,cur都会移向next,而front不会变。
实际上,front指针永远指向原区间的第一个结点。
image.png
image.png

再考虑边界条件。

  1. 由题中所给的变量范围可知,链表至少会有一个结点、left一定不会大于right,因此非法输入不需要考虑。
  2. 考虑指针越界。当right=n也就是区间到链表末尾时,当cur移动到right处,其next为null,不会触发空指针异常。
    综上,本题不需要额外考虑边界条件。

想好后,代码就很好写了。

public ListNode reverseBetween(ListNode head, int left, int right) {  
    ListNode dummy = new ListNode();  
    dummy.next = head;  
    int cnt = 1;  
    ListNode pre = dummy,cur,front;  
    while (cnt<left){  
        cnt++;  
        pre = pre.next;  
    }  
    cur = pre.next;  
    front = cur;  
  
    while (cnt<=right){  
        ListNode next = cur.next;  
        cur.next = pre.next;  
        pre.next = cur;  
        front.next = next;  
        cur = next;  
        cnt++;  
    }  
    return dummy.next;  
  
}

K个一组反转(hard)

lc25.
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

image.png
提示:

  • 链表中的节点数目为 n
  • 1 <= k <= n <= 5000
  • 0 <= Node.val <= 1000

思路与指定区间反转类似。需要注意以下几个点:

  1. 使用for循环而不是while循环会更好
  2. 需要先计算出要反转几个区间。
  3. 区间反转完毕后,新的pre、front、cur指针指向一定要弄清楚。

针对第二点,我们可以先画出k=2时,第一个区间已经反转完毕各指针的指向情况。
image.png
当开始反转第二个区间时,各指针指向应该变成下面这样:
可以得出结论。当一个区间反转完毕时,需要移动pre、front指针,而cur指针无需移动。
image.png
代码如下:

public ListNode reverseKGroup(ListNode head, int k) {  
    ListNode dummy = new ListNode();  
    dummy.next = head;  
    ListNode cur = head;  
    int len = 0;  
    while (cur!=null){  
        len++;  
        cur = cur.next;  
    }  
    int groupCnt = len/k;  
    ListNode pre = dummy;  
    cur = pre.next;  
    ListNode front = pre.next;  
    for(int i = 0;i < groupCnt;i++){  
        for(int j = 0;j < k;j++) {  
            ListNode next = cur.next;  
            cur.next = pre.next;  
            pre.next = cur;  
            front.next = next;  
            cur = next;  
        }  
        pre = front;  
        front = cur;  
    }  
    return dummy.next;  
  
}

两两交换链表节点

lc24.image.png
如下图所示,遍历指针cur应在交换节点区间前一位,方便我们将cur后面的第二个节点插入到cur之后,随后node1指向next,cur移动到node1的位置。
image.png

考虑边界条件。这里会出现node1、node2、next,其中node1、2都必须非空,因为它们要进行交换。next空不空就无所谓了。
整体而言思路还是比较简单的,代码如下:

public ListNode swapPairs(ListNode head) {  
    ListNode dummy = new ListNode();  
    dummy.next = head;  
    ListNode cur = dummy;  
    while (cur.next!=null && cur.next.next!=null){  
        ListNode node1 = cur.next;  
        ListNode node2 = node1.next;  
        ListNode next = node2.next;  
        node2.next = node1;  
        cur.next = node2;  
        node1.next = next;  
        cur = node1;  
    }  
    return dummy.next;   
}

链表加法

lc445
image.png

加法是从后加到前,而链表是从前往后。

栈实现

首先可以想到用栈先进后出的性质,把链表压栈然后依次出栈,对应相加。如下:
其中,需要有一个变量carry记录其进位情况。
另外还需注意一点的是,为了保证结果链表的顺序是从数字高位到低位的,我们在往结果链表添节点时需要使用头插法。
接下来考虑循环进入条件。当两个栈有一个非空时,进入循环;如果某一个栈结点已空,而另一栈某结点非空,则让空结点值为0即可。
还要考虑结果比原来最大位数还大一位的情况,如5+5.此时也要说明如果进位不为0时,还需要进行一次循环。
image.png
代码如下:

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {  
    Stack<ListNode> s1 = new Stack<>();  
    Stack<ListNode> s2 = new Stack<>();  
    while (l1!=null){  
        s1.push(l1);  
        l1 = l1.next;  
    }  
    while (l2!=null){  
        s2.push(l2);  
        l2= l2.next;  
    }  
    int carry = 0;  
    ListNode dummy = new ListNode();  
    while (!s1.isEmpty() || !s2.isEmpty()||carry!=0){  
        int num1 = s1.isEmpty()?0:s1.pop().val;  
        int num2 = s2.isEmpty()?0:s2.pop().val;  
        //计算结果  
        int res = num1+num2+carry;  
        //计算进位  
        carry = res>=10?1:0;  
        res %=10;  
        //头插结果链表  
        ListNode resNode = new ListNode(res);  
        ListNode next = dummy.next;  
        dummy.next = resNode;  
        resNode.next = next;  
    }  
    return dummy.next;  
  
}

反转链表实现

逻辑类似。注意两点:

  1. 先抽象出链表反转子函数。
  2. 每加完一次,记得移动链表指针至next。
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {  
    l1 = reverse(l1);  
    l2 = reverse(l2);  
    int carry = 0;  
    ListNode dummy = new ListNode();  
    while (l1!=null || l2!=null || carry!=0){  
        int num1 ,num2;  
        if(l1!=null){  
            num1 = l1.val;  
            l1 = l1.next;  
        }else {  
            num1 = 0;  
        }  
        if(l2!=null){  
            num2 = l2.val;  
            l2 = l2.next;  
        }else {  
            num2 = 0;  
        }  
        int res = num1+num2+carry;  
        carry = res>=10?1:0;  
        res %= 10;  
        ListNode resNode = new ListNode(res);  
        ListNode next = dummy.next;  
        dummy.next = resNode;  
        resNode.next = next;  
    }  
    return dummy.next;  
  
}  
private ListNode reverse(ListNode head){  
    ListNode prev = null;  
    ListNode cur = head;  
    while (cur!=null){  
        ListNode next = cur.next;  
        cur.next = prev;  
        prev = cur;  
        cur = next;  
    }  
    return prev;  
}

  • 19
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值