算法通关村第二关——链表反转的拓展问题

算法通关村系列文章目录

  1. 第二关—终于学会链表反转了

文章目录

  • 算法通关村系列文章目录
  • 前言
  • 一、指定区间反转
    • 1.1 直接反转法
      • 1.1.1 找到 left 节点和 right 节点
      • 1.1.2 将 left 和 right 之间的节点用直接反转法进行反转
      • 1.1.3 将反转后到区间链表与原有链表链接起来
    • 1.2 头插法
      • 总结:
  • 二、两两反转节点
  • 三、链表加法
  • 总结


前言

本系列文章是针对于鱼皮知识星球——编程导航中的算法通关村中的算法进行归纳和总结。 该篇文章讲解的是第二关中的白银挑战———链表反转拓展问题

在前文我们了解了有关链表反转的两种方法,而在链表反转的基础上可以拓展出许多有关问题
如:区间反转,n组反转,链表加法等等。在这些拓展问题中,核心内容还是我们在链表反转的思想,再加入一些题目的限定条件即可。
下面将以 LeetCode 中的一些题目为背景进行研究


一、指定区间反转

LeetCode 92 题:
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]

算法通关村第二关——链表反转的拓展问题_第1张图片
这道题让我们反转 left-right 之间的节点,思想跟我们之前反转整个链表一致,额外需要注意一下边界范围
上一次我们使用了两种方式进行链表反转,这次的区间反转我们也紧接着使用上次的两种方式进行区间反转

1.1 直接反转法

在直接反转法中,步骤为

  1. 先找到 left 节点和 right 节点
  2. 将 left 和 right 之间的节点用直接反转法进行反转
  3. 将反转后到区间链表与原有链表链接起来

听起来就向把大象塞进冰箱的三个步骤一样
其实需要注意的内容还是挺多的,我们下面进行详细的介绍
算法通关村第二关——链表反转的拓展问题_第2张图片

1.1.1 找到 left 节点和 right 节点

  1. pre —— left 节点前一个节点,也是原链表的左切分点
  2. leftNode ——反转区间的左端节点
  3. rightNode —— 反转区间的右端节点
  4. end —— right 节点后一个节点,也是原链表的右切分点
        ListNode virtual = new ListNode(-1);
        virtual.next = head;
        ListNode pre = virtual;

        // 找到left节点的前一个节点
        for (int i = 1; i <= left - 1; i++) {
            pre = pre.next;
        }
        ListNode leftNode = pre.next;
        ListNode rightNode = leftNode;
        // 找到right节点
        for (int i = left; i <= right - 1; i++) {
            rightNode = rightNode.next;
        }
        ListNode end = rightNode.next;
        // 这里一定要rightNode.next = null,如果
        rightNode.next = null

算法通关村第二关——链表反转的拓展问题_第3张图片

1.1.2 将 left 和 right 之间的节点用直接反转法进行反转

这里我们直接将 leftNode 当做是反转区间的头节点传给上次我们写的直接反转法中进行反转

 ReverseListII.reverseListNormal(leftNode);

直接反转法进行链表反转

public static ListNode reverseListNormal(ListNode head){
        ListNode current=head.next;
        ListNode pre=head;
        head.next=null;
        while (current!=null){
            // 将要处理的节点后面的链表存起来
            ListNode temp=current.next;
            // 还是将要处理的节点连接到反转的链表的头部
            current.next=pre;
            pre=current;
            current=temp;
        }
        return pre;
    }

算法通关村第二关——链表反转的拓展问题_第4张图片

1.1.3 将反转后到区间链表与原有链表链接起来

这里我们将 pre(原链表的左切分点)链接到反转区间的右端点,将反转区间的左端的链接到 end(原链表的右切分点)
这样我们就将原链表和反转后的链表进行链接了

        pre.next = rightNode;
        leftNode.next = end;

算法通关村第二关——链表反转的拓展问题_第5张图片
完整的代码

public static ListNode reverseListBetweenByNail(ListNode head, int left, int right) {
        ListNode virtual = new ListNode(-1);
        virtual.next = head;
        ListNode pre = virtual;

        // 找到left节点的前一个节点
        for (int i = 1; i <= left - 1; i++) {
            pre = pre.next;
        }
        ListNode leftNode = pre.next;
        ListNode rightNode = leftNode;
        // 找到right节点
        for (int i = left; i <= right - 1; i++) {
            rightNode = rightNode.next;
        }
        ListNode end = rightNode.next;
        rightNode.next = null;
        //获得反转后的区间链表
        ReverseListII.reverseListNormal(leftNode);
        pre.next = rightNode;
        leftNode.next = end;
        return virtual.next;

    }

1.2 头插法

如果当 left 和 right直接的区域很大时,我们需要先找到左端点和右端点,然后再依次反转,相当于遍历了两次完整的链表,而头插法可以使我们只遍历一遍就可以反转

在头插法的步骤中

  1. 找到 left 节点
  2. 在反转区间中,将每一个节点都插入到区间左端点前方

寻找 left 节点的代码:

        ListNode virtual = new ListNode(-1);
        virtual.next = head;
        ListNode leftNode = virtual;
        // 找到left节点的前一个节点
        for (int i = 1; i <= left - 1; i++) {
            leftNode = leftNode.next;
        }
        // 反转区间的最左侧节点,同时也是反转过后最右侧的节点--------简称为起始节点
        ListNode reverseSetLeftNode = leftNode.next;

区间反转的代码

在头插法反转链表时,我们使用了一个虚拟头节点,让每新加入的节点都通过它将已反转的链表进行链接
在区间反转中,我们可以将原链表的左切分点当作是区间反转的虚拟头节点,因为左切分点携带的数值对于反转区间来说,也是无意义的

        // 下面就是反转链表的过程,还是考虑指针的前后指向问题
        for (int i = left; i <= right - 1; i++) {
            //存起来这个最左侧节点的下一个节点
            ListNode temp = reverseSetLeftNode.next;
            // 相当于把这个节点的后一个节点隔过去,连接到下下一个节点
            reverseSetLeftNode.next = temp.next;
            // 让后一个节点链接到现在反转区间链表的头部,因为我们要把这个后一个节点移到前面,
            // 那操作次序就是先连接现在反转区间的头节点,左侧断口处 连接到 新的反转链表
            temp.next = leftNode.next;
            //让后一个节点连接到这个左侧断口处,相当于每次都将起始节点的后一个都移动到最前面,那可不就是反转了嘛
            leftNode.next = temp;
        }
        return virtual.next;

算法通关村第二关——链表反转的拓展问题_第6张图片

总结:

在区间反转的头插法中,我们可以更加深入了解为什么要加一个虚拟头节点可以规避很多种其他的情况
如果不加虚拟头节点:那开始就是只有leftNode
如果没有从第一个节点开始反转,只是从中间开始反转的话,那最后返回head也可以
如果是要从第一个节点开始反转的话,那leftNode和head就同时指向一个节点,那最后就不能再返回head了。
因为这时候相当于把链表全反转了,head就相当于最后一个节点了,所以不能返回head,可以说整个处理逻辑可能都需要变化
所以加一个虚拟头节点,每时每刻都指向链表头节点,就可以解决这个问题

二、两两反转节点

LeetCode 24 题:
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)

输入:head = [1,2,3,4]
输出:[2,1,4,3]

算法通关村第二关——链表反转的拓展问题_第7张图片
这道题其实就是将我们链表反转的情况给简化成长度为2的链表进行反转,并将这些反转后的链表再链接起来即可
所以,核心思想还是链表反转,注意一下链表之间的边界问题。

  1. current1 ——两个节点中的第一个
  2. current2——两个节点中的第二个
  3. pre——已反转链表的尾部,也是要反转节点的前方节点

步骤为:

  1. 将current2链接的节点用temp进行存储
  2. 将current1链接 temp,current2链接current1 进行节点链接的反转
  3. 重置pre,使之指向现在已反转链表的尾部
  4. 重置current1,将current1往后移动一个节点
    public static ListNode reverseListTwoGroup(ListNode head){
        ListNode virtual=new ListNode(-1);
        virtual.next=head.next;
        ListNode current1=head;
        ListNode pre=virtual;
        while (current1!=null&&current1.next!=null){
            current2=current1.next;
            ListNode temp=current2.next;
            current1.next=temp;
            current2.next=current1;
            pre.next=current2
            pre=current1;
            current1=temp;
        }
        return virtual.next;
    }

算法通关村第二关——链表反转的拓展问题_第8张图片


三、链表加法

LeetCode455题:
给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。

你可以假设除了数字 0 之外,这两个数字都不会以零开头。

输入:l1 = [7,2,4,3], l2 = [5,6,4]
输出:[7,8,0,7]

算法通关村第二关——链表反转的拓展问题_第9张图片

在链表加法中如果正序相加的话,是跟我们平常计算顺序是相反的,所以这道题如果要写就必须要用到链表反转,但是这道题不仅要注意链表反转,还要注意加法还有各种进位等各种东西

变量

  1. reList1 —— 反转后的链表1
  2. reList2 —— 反转后的链表2
  3. val —— 每一对节点相加后的值
  4. carry低位向高位的进位

步骤

  1. 先将两个链表分别反转,两种方法用哪一个反转都可以
  2. 新创建一个虚拟头节点,链接一个链表来存储两个链表的和
  3. 先将存储的进位加上 val,随后判断 reList1 和 reList2现在还有无节点,有节点就将节点的值加上
  4. 判断 val 是否会产生进位
  5. 最后可能全部为对应加完后,还产生了一个进位,也就是由原来的n位数因为相加变为了n+1位数
  6. 因为我们是反着加的,所以最后的结果也是反着的,最后再把相加的结果反转一下就可以了
    public static ListNode addInListByReverse(ListNode list1, ListNode list2) {
        //1. 先将两个链表反转
        ListNode reList1 = reverseListByHead(list1);
        ListNode reList2 = reverseListByHead(list2);
        //2. 将两个链表反转
        ListNode resultVirtual = new ListNode(-1);
        ListNode p = resultVirtual;
//        int[] flag = new int[2];
//        int i = 1;
        int carry=0;



        // 将3个while循环整合成一个,在循环中进行判断
        while(reList1!=null||reList2!=null){
            // 先将进位加上
            int val=carry;
            if(reList1!=null){
                val+= reList1.val;;
                reList1=reList1.next;
            }
            if(reList2!=null){
                val+= reList2.val;;
                reList2=reList2.next;
            }
            ListNode listNode = new ListNode(val % 10);
            p.next=listNode;
            carry=val/10;
            p=p.next;
        }

//       将情况分开写的while循环
//        while (reList1 != null && reList2 != null) {
//            int r1 = reList1.val;
//            int r2 = reList2.val;
//            // 一开始的和
//            int add = r1 + r2;
//            // 看前一位有没有进位
//            if (flag[0] == i - 1) {
//                add = add + flag[1];
//                flag[0] = 0;
//                flag[1] = 0;
//            }
//            // 产生的进位
//            int carry = add / 10;
//            // 进位后的和
//            int fAdd = carry == 0 ? add : add % 10;
//            // 其实找的是上一位的进位
//            ListNode node = new ListNode(fAdd);
//            // 给下一位的进位
//            flag[0] = i;
//            flag[1] = carry;
//            p.next = node;
//            p = p.next;
//            reList1 = reList1.next;
//            reList2 = reList2.next;
//            i++;
//        }
//
//        while (reList1 != null) {
//            int add = reList1.val;
//            // 看前一位有没有进位
//            if (flag[0] == i - 1) {
//                add = add + flag[1];
//                flag[0] = 0;
//                flag[1] = 0;
//            }
//            // 产生的进位
//            int carry = add / 10;
//
//            int fAdd = carry == 0 ? add : add % 10;
//            ListNode node = new ListNode(fAdd);
//            // 给下一位的进位
//            flag[0] = i;
//            flag[1] = carry;
//            p.next = node;
//            p = p.next;
//            reList1 = reList1.next;
//            i++;
//        }
//        while (reList2 != null) {
//            int add = reList2.val;
//            // 看前一位有没有进位
//            if (flag[0] == i - 1) {
//                add = add + flag[1];
//                flag[0] = 0;
//                flag[1] = 0;
//            }
//            // 产生的进位
//            int carry = add / 10;
//            int fAdd = carry == 0 ? add : add % 10;
//            ListNode node = new ListNode(fAdd);
//            // 给下一位的进位
//            flag[0] = i;
//            flag[1] = carry;
//            p.next = node;
//            p = p.next;
//            reList2 = reList2.next;
//            i++;
//        }
        if(carry==1){
            ListNode node = new ListNode(1);
            p.next = node;
        }
        ListNode listNode = reverseListByHead(resultVirtual.next);
        return listNode;
    }

总结

研究完这三道题,可以看出链表反转是可以出相当多的题目的,同时也是比较基础和重要的。对于链表反转还有一些比较难的题,比如K个一组反转等,也就是第二关的黄金挑战。单由于只要写青铜和白银的对应挑战就算过关,黄金挑战的解析就有时间再发布吧

你可能感兴趣的:(算法通关村,算法,链表,数据结构)