算法4: LeetCode_K个节点的组内逆序调整
- 最近一直都是链表的算法练习,今天刷的是LeetCode原题,还是关于链表的节点逆转,难度等级:Hard.
- 首先看题目:给定一个单聊表的头节点head和一个正整数k, 要求实现k个节点的小组内部逆序,如果最后一组不够k个就不调整。 如果给定的链表为1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8,k = 3,调整完以后链表变成 3 -> 2 -> 1 -> 6 -> 5 -> 4 -> 7 -> 8
- 解题思路:
- 既然是给定长度的组内局部逆转,首先肯定是要对链表进行分组,符合条件的小组内部进行链表的逆转。不符合条件的小组,则保持不变
- 如果符合条件,每个小组的内部其实都设计到头节点和尾结点的对调,并且前一个小组的尾结点需要持有下一小组你转完以后的新的头节点。此处可能涉及到多次逆转。比如第一次逆转完以后链表应当变成 3 -> 2 -> 1 -> 4 -> 5 -> 6 -> 7 -> 8. 第二次逆转完变成 3 -> 2 -> 1 -> 6 -> 5 -> 4 -> 7 -> 8。第三次逆转,发现只有7和8两个节点,不符合逆转条件,则不逆转。
- 通过思路2我们可知,第一小组你转完以后,头节点即锁定为3,后面不会发生改变。而值为1的节点首先是持有下一个小组的头结点,即值为4的节点。但是,当下一小组节点发生逆转以后,会更改持有的头节点,会变更为持有值为6的新头节点。最后一次只有2个节点,不满足条件,不需要逆转,直接返回即可。
- 总结:通过以上分享,我们大体得出结论:第一次逆转锁定整个链表的头节点; 每一次逆转后,前一小组的尾结点都需要变更持有下一小组的逆转后的头结点引用对象;如果不满足条件,则直接返回,不做任何变更
package code1.code_02; /** * LeetCode https://leetcode.cn/problems/reverse-nodes-in-k-group/ * K个节点的组内逆序调整 * 给定一个单链表的头节点head,和一个正数k * 实现k个节点的小组内部逆序,如果最后一组不够k个就不调整 * 调整前:1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8,k = 3 * 调整后:3 -> 2 -> 1 -> 6 -> 5 -> 4 -> 7 -> 8 * * 解题思路: * * */ public class ReverseKGroupTest07_extension { private static class ListNode<V> { public V data; public ListNode next; ListNode (V _data){ this.data = _data; } } // 先设计打印方法,方便检查逆转后结果 public void printNode (ListNode node) { if (node == null) { System.out.println("链表不存在"); } System.out.println("当前节点的值为: " + node.data); //递归的方式逐层打印Node的子节点 if(node.next != null) { printNode(node.next); } } //设计构造单链表的函数 public ListNode initSingleNodeList (int length) { if (length <= 0) { return null; } ListNode head = null; ListNode<Integer> cur; ListNode<Integer> pre = null; for (int i = 1; i <= length; i++) { cur = new ListNode<>(i); //头结点一旦确认,将会固定不变 if (head == null) { head = cur; } if (pre != null) { pre.next = cur; } pre = cur; } return head; } public ListNode getEndNodeInKGroup (ListNode node, int k) { /* while (k > 0 && node != null) { node = node.next; k--; }*/ //以上注释的代码是有bug的写法,边边角角需要注意 while (--k > 0 && node != null) { node = node.next; } return node; } public ListNode reverseKGroup (ListNode node, int k) { ListNode start = node; ListNode end = this.getEndNodeInKGroup(start, k); //第一次进入都没有尾节点,说明此链表不符合局部逆转 //k个节点的要求,直接返回 if (end == null) { return node; } //逆转前的第一个分组尾节点,就是新的头结点 ListNode head = end; /** * 第一次逆转。为什么没有返回值呢? * 因为head已经指向了新的头结点, 此方法做到逆转, * 使链表并没有断. * 此时的head是第一次局部逆转后的值。比如: * 逆转前是 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 * 逆转后是 3 -> 2 -> 1 -> 4 -> 5 -> 6 -> 7 */ System.out.println("========外部调用前hash值为: " + end.hashCode() + "======"); reverseNode(start, end); System.out.println("========外部调用后hash值为: " + end.hashCode() + "======"); System.out.println("+++++++++++++++++++++++++++一次完整的调用结束++++++++++++++++++++++++++++++++"); //上一组group你转完以后的尾节点 ListNode lastEnd = start; while (lastEnd.next != null) { /** * 接着逆转 * 因为reverseNode(start, end)并没有返回值,因此原有的连 * 表并没有断。由于在 reverseNode(start, end)方法中的逆 * 转。 此时的start已经持有了下一组待逆转group的头节点处 * 可以参考reverseNode(start, end)方法的最后一行代码 */ start = lastEnd.next; end = this.getEndNodeInKGroup(start, k); //如果接下来的group不符合算法要求,也就是不满足 //k个节点,直接返回 if (end == null) { //因为没有发生逆转,前一组已经逆转的尾结点本来就 //持有这一组待你转接节点的默认头节点,也就不需要更新了 return head; } System.out.println("========外部调用前hash值为: " + end.hashCode() + "======"); reverseNode(start, end); System.out.println("========外部调用后hash值为: " + end.hashCode() + "======"); System.out.println("+++++++++++++++++++++++++++一次完整的调用结束++++++++++++++++++++++++++++++++"); //上一组的尾结点,需要持有本组你转完以后的头结点 lastEnd.next = end; //lastEnd来到分组你转完以后的尾节点,为下轮循环做准备 lastEnd = start; } //直接返回的是head, 也解释了为什么reverseNode(start, end) //可以没有返回值了. //如果有兴趣,可以去改写的我的算法2中逆转整个链表的代码。将有返回值的逆转 //方法也改成没有返回值的方法https://blog.csdn.net/chen_yao_kerr/article/details/127935045?spm=1001.2014.3001.5501 return head; } //无参数函数,因此之前的连边不能断 private void reverseNode (ListNode start, ListNode end) { //我们需要提前直到逆转前的end节点的下一个节点 //逆转完成以后,新的尾节点指向此节点,保证链表不断 //此处有一个问题,此处的end指向了下一个节点,那么end应该是变化了的才对。那么为什么 //在代码的122行lastEnd.next = end; 依旧持有值为6的节点,而不是持有值为7的节点呢? //其实,此处的end经过一次赋值,相当于在另一个方法栈中生成了一个新的局部变量,只是 //他们的名称相同而已。其实他们根本就不是一个东西 >> 此处我们通过hash值来确认 System.out.println("========方法体内部调用前的hash值为: " + end.hashCode() + "======"); end = end.next; System.out.println("========方法体内部调用后的hash值为: " + end.hashCode() + "======"); ListNode next = null; ListNode cur = start; //start节点是新的尾节点,后面还有用。此时定义一个新的临时节点 ListNode pre = null; //已经逆转的节点 //开始逆转 //此时的end节点是下一组待逆转的头结点 while (cur != end) { next = cur.next; cur.next = pre; pre = cur; cur = next; } /** * 此处有2个问题需要解释一下 * 问题1: 为什么是start.next 而不是cur.next呢 ? * 原因: 因为cur已经来到了第一个待逆转group的尾结点,也就是 * 你转完以后group的头结点处 * * 问题2:为什么start.next = pre 而不是start.next = cur ? * 原因: 因为我们在方法的开始就设置了end = end.next; 这样可以 * 保证我们当前group的节点都可以在87行的while方法中遍历到。 * 而cur最后一次是来到了end.next的位置,这已经超出了当前group的 * 节点范畴,因此需要往前找一个,也就是上一次逆转的节点 */ start.next = end; } public static void main(String[] args) { ReverseKGroupTest07_extension test = new ReverseKGroupTest07_extension(); //此参数可以设计成从UI传递的值 int length = 8; //初始化固定长度的链表 ListNode node = test.initSingleNodeList(length); System.out.println("===============测试构造的链表是否正常==================="); test.printNode(node); //链表进行按照k个数组进行划分,并进行局部逆转的过程 ListNode n = test.reverseKGroup(node, 3); System.out.println("===============打印局部逆转后的结果==================="); test.printNode(n); } }
打印结果如下:
===============测试构造的链表是否正常=================== 当前节点的值为: 1 当前节点的值为: 2 当前节点的值为: 3 当前节点的值为: 4 当前节点的值为: 5 当前节点的值为: 6 当前节点的值为: 7 当前节点的值为: 8 ========外部调用前hash值为: 1163157884====== ========方法体内部调用前的hash值为: 1163157884====== ========方法体内部调用后的hash值为: 1956725890====== ========外部调用后hash值为: 1163157884====== +++++++++++++++++++++++++++一次完整的调用结束++++++++++++++++++++++++++++++++ ========外部调用前hash值为: 356573597====== ========方法体内部调用前的hash值为: 356573597====== ========方法体内部调用后的hash值为: 1735600054====== ========外部调用后hash值为: 356573597====== +++++++++++++++++++++++++++一次完整的调用结束++++++++++++++++++++++++++++++++ ===============打印局部逆转后的结果=================== 当前节点的值为: 3 当前节点的值为: 2 当前节点的值为: 1 当前节点的值为: 6 当前节点的值为: 5 当前节点的值为: 4 当前节点的值为: 7 当前节点的值为: 8 Process finished with exit code 0
实测结果:
其实,逐步拆分算法,组成每一个小例子,然后将小例子都理解了,再组合在一起就是一个完整的算法了。先理解这个算法想要干什么,对照自己的例子去实现就可以了