算法小技巧 -- 链表
一、快慢指针
1、核心思想
【核心思想:】 采用双指针完成,一个指针永远比另一个指针稍快一点。 【常见案例:】 找到单链表的中间节点
环形链表 【单链表结构:】 class ListNode { int val; ListNode next; ListNode() {} ListNode(int val) { this.val = val; } ListNode(int val, ListNode next) { this.val = val; this.next = next; } }
2、案例实现(找到单链表的中间节点)
【LeetCode 题目:】 标题: 876. 链表的中间结点 题目描述: 给定一个头结点为 head 的非空单链表,返回链表的中间结点。 如果有两个中间结点,则返回第二个中间结点。 限制: 给定链表的结点数介于 1 和 100 之间。 测试用例: 输入:[1,2,3,4,5] 输出:3 输入:[1,2,3,4,5,6] 输出:4 【案例分析:】 假设两个指针 slow、fast。 每次指向下一个节点时,fast 都比 slow 快一个节点, 即 fast 每次跳过两个节点,slow 每次跳过一个节点。 当 fast 遍历到链表末尾时,slow 恰好处于链表中间节点处。 即 slow = slow.next; fast = fast.next.next; 注: 链表节点为偶数时,存在两个中间节点,此处以第二个节点作为中间节点。 【伪代码实现:】 public ListNode middleNode(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } return slow; }
3、案例实现(环形链表)
(1)LeetCode 题目
【LeetCode 题目:】 标题: 141. 环形链表 题目描述: 给定一个链表,判断链表中是否有环。 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 如果链表中存在环,则返回 true。 否则,返回 false 。 注: 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。 pos 不作为参数进行传递,仅仅是为了标识链表的实际情况(用于表示哪个位置开始会出现环)。 限制: 链表中节点的数目范围是 [0, 104] -105 <= Node.val <= 105 pos 为 -1 或者链表中的一个 有效索引 。 测试用例: 输入:head = [3,2,0,-4], pos = 1 输出:true 解释:链表中有一个环,其尾部连接到第二个节点。 输入:head = [1,2], pos = 0 输出:true 解释:链表中有一个环,其尾部连接到第一个节点。 输入:head = [1], pos = -1 输出:false 解释:链表中没有环。
(2)案例分析
【案例分析:】
链表存在环,则进行链表遍历时将会进入 死循环,需要设置结束遍历的条件。
若不存在环,即使链表足够长,总能遍历结束,无需设置结束遍历的条件。
联想到一个场景:
晚上寂寞无聊,约哥们去体育馆操场跑圈。假设两个人为 A、B。
A、B 同时出发,由于身体素质问题,A 跑的比 B 快点。
经过一段时间后,A 甩了 B 半圈,再经过一段时间后,A 追上了 B。
只要操场跑道是个圈,A 总能追上 B,只是时间长短的问题。
即:
A、B 的跑步频率只要设置的不太离谱,总有重合的一天。
此处设置 A 为快指针,B 为慢指针。
A 跑两米,B 跑一米,当 A 追上了 B,则表示是个环。
(3)伪代码
【伪代码:】 public boolean hasCycle(ListNode head) { if (head == null) { return false; } ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) { return true; } } return false; }
二、单链表的遍历
1、核心思想
【核心思想:】 head 与 head.next 一定要区分开。 每次经过一个节点后,需要跳到下一个节点, 即 head = head.next。 【常见实现:】 使用 while 进行遍历。 使用 递归 进行遍历。(类似于树的 前序、后序 遍历的实现) 【常见案例:】 正序输出链表(可使用 while、递归 实现) 反序输出链表(可使用 递归 实现)
2、案例实现(正序输出链表 -- while 实现)
【案例分析:】 temp 与 temp.next 一定要区分开。 temp 表示当前所在链表节点位置,temp.next 表示当前链表节点的下一个位置。 注: 判断条件可以为 temp != null 或者 temp.next != null,灵活使用。 【伪代码:】 public void printList(ListNode head) { ListNode temp = head; if (temp == null) { System.out.println("Empty"); return; } while(temp != null) { System.out.println(temp.val); temp = temp.next; } }
3、案例实现(遍历输出链表 -- 递归实现)
【案例分析:】 遍历输出链表,正序、反序 的代码非常相似,可等同于 树结构的 前序、后序遍历进行理解。 递归 类似于 栈结构,先进后出。入栈的顺序就是 正序的,出栈的顺序就是 反序的。 【伪代码:】 public static void printList(ListNode head) { if (head == null) { System.out.println("Empty"); return; } nextNode(head); } public static void nextNode(ListNode head) { if (head == null) { return; } // System.out.println(head.val); // 正序遍历输出 nextNode(head.next); System.out.println(head.val); // 反序遍历输出 }
三、单链表反转
1、核心思想
【核心思想:】 head 与 head.next 一定要区分开。 每经过一个节点,确保当前节点的下一个节点指向自己。 即: head.next.next = head 若从头开始反转,可以使用 多指针 实现。 若从尾开始反转,可以使用 递归 实现。 【常见实现:】 使用 栈 作为中转站,遍历链表,将值入栈,出栈时构建为一个新的链表。 使用多个指针,从头开始,逐个节点进行反转。 使用 递归 进行反转,从尾开始,逐个节点进行反转。 【常见案例:】 反转链表 反转部分链表(指定范围进行反转) K 个一组翻转链表 回文链表
2、案例实现(反转链表 -- 栈实现)
【LeetCode 题目:】 标题: 206. 反转链表 题目描述: 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 限制: 链表中节点的数目范围是 [0, 5000] -5000 <= Node.val <= 5000 测试用例: 输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1] 输入:head = [1,2] 输出:[2,1] 输入:head = [] 输出:[] 【案例分析:】 使用 栈 作为中转站,遍历链表,将值入栈,出栈时构建为一个新的链表。 【伪代码:】 public ListNode reverseList(ListNode head) { if (head == null) { return head; } ListNode temp = head; Stack<Integer> stack = new Stack<>(); while(temp != null) { stack.push(temp.val); temp = temp.next; } temp = head; while(!stack.isEmpty()) { temp.next = new ListNode(stack.pop()); temp = temp.next; } return head.next; }
3、案例实现(反转链表 -- 多指针实现)
【LeetCode 题目:】 标题: 206. 反转链表 【案例分析:】 此方式从头向尾执行。 使用多个指针,假设当前指针为 current,其上一个指针为 pre,下一个指针为 next。 其中: pre 用于存储 当前指针 需要指向的 上一个节点。 next 用于存储 当前指针 需要跳到的 下一个节点。 每次经过一个节点时,保证当前节点指向上一个节点,然后跳转到下一个节点继续操作。 即: next = current.next; current.next = pre; pre = current; current = next; 【伪代码:】 public ListNode reverseList(ListNode head) { ListNode pre = null; ListNode next = null; ListNode current = head; while(current != null) { next = current.next; current.next = pre; pre = current; current = next; } return pre; }
4、案例实现(反转链表 -- 递归实现)
【LeetCode 题目:】 标题: 206. 反转链表 【案例分析:】 此方式从尾向头进行。 每次经过一个节点时,保证当前节点的下一个节点指向当前节点,移除当前节点指向的下一个节点。 然后跳转到上一个节点继续操作(此过程由递归的逻辑实现,返回上一层)。 即 head.next.next = head; head.next = null; 【伪代码:】 public static ListNode reverseList(ListNode head) { if (head == null) { return head; } return nextNode(head); } public static ListNode nextNode(ListNode head) { if (head.next == null) { return head; } // lastNode 表示最后一个节点位置,没有被修改,不断的向递归的上层传递 ListNode lastNode = nextNode(head.next); head.next.next = head; head.next = null; return lastNode; }
5、案例实现(反转部分链表 -- 多指针)
【LeetCode 题目:】 标题: 92. 反转链表 II 题目描述: 给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。 请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。 限制: 链表中节点数目为 n 1 <= n <= 500 -500 <= Node.val <= 500 1 <= left <= right <= n 测试用例: 输入:head = [1,2,3,4,5], left = 2, right = 4 输出:[1,4,3,2,5] 输入:head = [5], left = 1, right = 1 输出:[5] 【案例分析:】 反转指定范围的链表。关键就在于 反转范围边界 的四个节点是如何连接的。 假设反转范围边界节点分别为 a、b、c、d,需要反转的是 b -> ... -> c 这部分节点。 开始顺序为 head -> ... a -> b -> ... -> c -> d -> ... 最终需要得到的顺序为 head -> ... a -> c -> ... -> b -> d -> ... 这么看上去就很直观了,先令 b、c 之间的链表节点反转,然后让 a 指向 c,b 指向 d 即可完成。 注: 范围为 0 时,此时只反转一个节点,即 b、c 为同一个节点,无需反转。 若从头开始反转,即没有 a 节点,此时 c 直接作为新的头结点即可。 【伪代码:】 public ListNode reverseBetween(ListNode head, int left, int right) { ListNode first, temp; first = temp = head; int count = right - left; if (count != 0) { if (left == 1) { // 从头开始反转,没有 a 节点,c 节点即为新的头结点 return reverseList(head, count); } while (temp != null && left != 1) { left--; // 找到开始反转的节点的上一个节点,即 a 节点 first = temp; // 找到开始反转的节点,即 b 节点 temp = temp.next; } // 从中间位置开始反转,需要连接上反转范围的链表的新头节点,即 a -> c first.next = reverseList(temp, count); } return head; } public ListNode reverseList(ListNode head, int count) { ListNode first, pre, next, current; pre = next = null; first = current = head; // 反转指定范围的链表 while(current != null && count != 0) { next = current.next; current.next = pre; pre = current; current = next; count--; } // 指向反转范围的新尾节点的下一个节点,即 b -> d first.next = current.next; // 返回反转范围链表的新头结点,即 c 节点 current.next = pre; return current; }
6、案例实现(K 个一组翻转链表)
【LeetCode 题目:】 标题: 25. K 个一组翻转链表 题目描述: 给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。 k 是一个正整数,它的值小于或等于链表的长度。 如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。 限制: 列表中节点的数量在范围 sz 内 1 <= sz <= 5000 0 <= Node.val <= 1000 1 <= k <= sz 测试用例: 输入:head = [1,2,3,4,5], k = 2 输出:[2,1,4,3,5] 输入:head = [1,2,3,4,5], k = 3 输出:[3,2,1,4,5] 输入:head = [1,2,3,4,5], k = 1 输出:[1,2,3,4,5] 输入:head = [1], k = 1 输出:[1] 【案例分析:】 k 个节点一组去反转,原理与 部分节点反转 类似,只是多了重复调用的过程。 关注点同样是 a、b、c、d 四个节点指向的问题。 注: 此处从头开始反转,即第一次没有 a 节点,直接使用 c 节点作为新的头节点。 【伪代码:】 public ListNode reverseKGroup(ListNode head, int k) { if (head == null) { return head; } ListNode temp = head; // 找到需要反转的节点范围 for (int i = 0; i < k; i++) { // 若反转节点范围不足,则不进行反转 if (temp == null) { return head; } temp = temp.next; } // 获取反转节点后的新头结点 ListNode newHead = reverseList(head, k - 1); // 进行下一次反转,用于获取下一部分的新头节点,并令当前尾节点指向下一次获取的头结点 head.next = reverseKGroup(temp, k); return newHead; } public ListNode reverseList(ListNode head, int count) { //ListNode first, pre, next, current; ListNode pre, next, current; pre = next = null; //first = current = head; current = head; while (current != null && count != 0) { next = current.next; current.next = pre; pre = current; current = next; count--; } //first.next = current.next; current.next = pre; return current; }
7、案例实现(回文链表)
【LeetCode 题目:】 标题: 剑指 Offer II 027. 回文链表 题目描述: 给定一个链表的 头节点 head ,请判断其是否为回文链表。 如果一个链表是回文,那么链表节点序列从前往后看和从后往前看是相同的。 能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题? 限制: 链表 L 的长度范围为 [1, 105] 0 <= node.val <= 9 测试用例: 输入: head = [1,2,3,3,2,1] 输出: true 输入: head = [1,2] 输出: fasle 【案例分析:】 判断回文,常用方法就是:从两侧向中间逼近 或者 从中间向两侧展开 或者 暴力反转构建新的链表,然后逐个比较。 从两侧向中间逼近(效率低): 由于链表的结构特殊,无法直接从 尾节点向头结点遍历, 但可以使用 递归的方式,模拟 栈 的使用,先递归到最后一个节点,然后逐级回退到上层节点。 从中间向两侧逼近(效率稍高): 先找到链表的中点,可以使用快慢指针。 然后对 前半段链表进行 反转,这样就可以从 中间节点 向两侧展开并比较。 当然,也可以反转后半段链表,然后从头结点开始比较。 暴力反转构建新链表(需要额外空间): 直接对链表进行反转,构建一个新链表。 然后 新链表、旧链表 逐个进行值比较。 【伪代码:(从两侧向中间逼近)】 public static ListNode first = null; public static boolean isPalindrome(ListNode head) { first = head; return nextNode(head); } public static boolean nextNode(ListNode head) { if (head == null) { return true; } boolean flag = nextNode(head.next); if (flag && head.val == first.val) { first = first.next; return flag; } return false; } 【伪代码:(从中间向两侧展开,反转后半段链表)】 public static boolean isPalindrome(ListNode head) { // 找中间节点 ListNode middleNode = middleNode(head); // 反转后半段链表 ListNode newNode = reverseList(middleNode); // 从头结点 开始比较 新的后半段链表 while(newNode != null && head != null) { if (newNode.val != head.val) { return false; } newNode = newNode.next; head = head.next; } return true; } public static ListNode middleNode(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } return slow; } public static ListNode reverseList(ListNode head) { ListNode pre, next, current; pre = next = null; current = head; while (current != null) { next = current.next; current.next = pre; pre = current; current = next; } return pre; }
别把自己太当回事,也别太把自己不当回事!Life is Fantastic!!!