算法通关村第一关——链表白银挑战笔记
前言:
通过白银挑战的学习,我也了解到了很多知识和解题套路,并且进一步增加了自己对算法学习的兴趣。
以下代码均为自己后期手写,所以可能与教程不太一致。因此也可能会有一部分错误,大家在浏览时发现错误可以积极评论指出,我也会第一时间进行更正,谢谢大家。
以下代码通用初始化ListNode类
public class ListNode { public int val; public ListNode next; public ListNode(int val) { this.val = val; } }
1. 两个链表第一个公共子节点
题目:输入两个链表,找出它们的第一个公共节点。例如下面的两个链表:
两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数也是未知的,请设计算法找到两个链表的合并点。
public ListNode findFirstCommonNode(ListNode headA, ListNode headB) { Stack<ListNode> stackA = new Stack(); Stack<ListNode> stackB = new Stack(); while(headA != null){ stackA.push(headA); headA = headA.next; } while(headB != null){ stackB.push(headB); headB = headB.next; } ListNode preNode = null; while(stackB.size() > 0 && stackA.size() > 0){ if(stackA.peek() == stackB.peek()){ preNode = stackA.pop(); stackB.pop(); }else{ break; } } return preNode; }
注:本题用hashmap或者hashset或者集合也均可
2. 判断链表是否为回文序列
题目:判断一个链表是否为回文链表。
示例1:
输入: 1->2->2->1
输出: true
进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
public static boolean IsPalindromic(ListNode headA) { Stack<Integer> stack = new Stack<>(); ListNode node = headA; while (headA != null){ stack.push(headA.val); headA = headA.next; } while (node != null){ if (stack.pop() != node.val){ return false; } node = node.next; } return true; }
上述代码可以进行改进:压栈完毕的时候再次遍历只需 遍历一般元素即可
还可以使用双指针的办法,slow 、fast fast每次走的步数是slow的二倍,当fast到尾部的时候slow正好到中间部分,然后继续遍历进行比较即可
3. 合并有序链表
3.1:合并两个有序链表
题目:将两个升序链表合并为一个新的升序链表并返回,新链表是通过拼接给定的两个链表的所有节点组成的。
public static ListNode MergeList(ListNode list1, ListNode list2) { //充当哨兵结点的作用 ListNode node = new ListNode(0); //扮演尾指针 ListNode res = node; while(list1 != null || list2 != null){ //情况1:都不为空的情况 if(list1 != null && list2 != null){ if(list1.val < list2.val){ res.next = list1; list1 = list1.next; }else if(list1.val > list2.val){ res.next = list2; list2 = list2.next; }else{ //相等的情况,分别接两个链 res.next = list2; list2 = list2.next; res = res.next; res.next = list1; list1 = list1.next; } res = res.next; //情况2:假如还有链表一个不为空 }else if(list1 != null && list2 == null){ //后面一次性全链接上 res.next = list1; }else if(list1 == null && list2 != null){ //后面一次性全链接上 res.next = list2; } } return node.next; }
3.2:合并K个链表
题目:合并k个链表,有多种方式,例如堆、归并等等。
先将前两个合并,之后再将后面的逐步合并进来,这样的的好处是只要将两个合并的写清楚,合并K个就容易很多:
public ListNode mergeKLists(ListNode[] lists) { ListNode node = null; for (ListNode list: lists) { node = MergeList(node, list); } return node; }
3.3:一道很无聊的好题
题目:给你两个链表 list1 和 list2 ,它们包含的元素分别为 n 个和 m 个。请你将 list1 中下标从a到b的节点删除,并将list2 接在被删除节点的位置。
public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) { ListNode pre1 = list1, post1 = list1, post2 = list2; int i = 0, j = 0; while(pre1 != null && post1 != null && j < b){ if(i != a - 1){ pre1 = pre1.next; i++; } if(j != b){ post1 = post1.next; j++; } } post1 = post1.next; //寻找list2的尾节点 while(post2.next != null){ post2 = post2.next; } //链1尾接链2头,链2尾接链1后半部分的头 pre1.next = list2; post2.next = post1; return list1; }
4. 双指针专题
4.1:寻找中间结点
题目:给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
示例1
输入:[1,2,3,4,5]
输出:此列表中的结点 3
示例2:
输入:[1,2,3,4,5,6]
输出:此列表中的结点 4
public ListNode middleNode(ListNode head) { ListNode slow = head, fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } return slow; }
直接用双指针,标准的双指针会直接显示偶数个中间靠后面的那个,如果要显示前面的那个,可以具体该一下代码即可
4.2:寻找倒数第K个元素
题目:寻找倒数第K个元素
输入一个链表,输出该链表中倒数第k个节点。本题从1开始计数,即链表的尾节点是倒数第1个节点。
示例
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
public static ListNode GetKthFromEnd(ListNode node,int k) { ListNode fast = node; ListNode slow = node; ListNode head = node; int length = 0; while (node != null){ node = node.next; length++; } if (length < k){ return head; } while (fast != null && k > 0){ fast = fast.next; k--; } while (fast != null){ slow = slow.next; fast = fast.next; } return slow; }
4.3:旋转链表
题目:给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
示例1:
输入:head = [1,2,3, 4,5], k = 2
输出:[4,5,1,2,3]
public ListNode rotateRight(ListNode head, int k) { if(head == null || k == 0){ return head; } //这里三个变量都指向链表头结点 ListNode temp = head; ListNode fast = head; ListNode slow = head; int len = 0; //这里head先走一遍,统计出链表的元素个数,完成之后head就变成null了 while(head != null){ head = head.next; len++; } if(k % len == 0){ return temp; } // 从这里开始fast从头结点开始向后走 //这里使用取模,是为了防止k大于len的情况 //例如,如果len=5,那么k=2和7,效果是一样的 while((k % len) > 0){ k--; fast = fast.next; } // 快指针走了k步了,然后快慢指针一起向后执行 // 当fast到尾结点的时候,slow刚好在倒数第K个位置上 while(fast.next != null){ fast = fast.next; slow = slow.next; } ListNode res = slow.next; slow.next = null; fast.next = temp; return res; }
5. 删除链表元素专题
5.1:删除特定结点
题目:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点 。
示例1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
public static ListNode removeElements(ListNode node,int val) { ListNode head = node; while (node.next != null){ if (node.next.val == val){ node.next = node.next.next; node = node.next; } node = node.next; } return head; }
5.2:删除倒数第n个节点
题目:给你一个链表,删除链表的倒数第n个结点,并且返回链表的头结点。进阶:你能尝试使用一趟扫描实现吗?
示例1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
public static ListNode removeNthFromEnd(ListNode node, int n) { ListNode head = node; ListNode fast = node; ListNode slow = node; int length = 0; while (node != null){ node = node.next; length++; } //避免n大于链表长度,导致发生错误 int k = n % length; if(k == 0){ return head; } while (k > 0){ fast = fast.next; k--; } while (fast.next !=null){ slow = slow.next; fast = fast.next; } slow.next = slow.next.next; return head; }
其实这道题我觉得用栈比较好,将链表压入栈,直接弹栈,弹到n的时候,即可找到我们所需的结点(主要是双指针格调高,感觉比较装...)
5.3:删除重复元素
5.3.1:重复元素保留一个
题目:存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素只出现一次 。返回同样按升序排列的结果链表。
示例1:
输入:head = [1,1,2,3,3]
输出:[1,2,3]
public ListNode deleteDuplicates(ListNode head) { if (head == null) { return head; } ListNode cur = head; while (cur.next != null) { if (cur.val == cur.next.val) { cur.next = cur.next.next; } else { cur = cur.next; } } return head; }
5.3.2:重复元素都不要
例如:
示例1:
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
public static ListNode deleteDuplicates(ListNode node) { if (node == null) { return node; } //直接对node.next 以及 node.next.next 两个node进行比较就行了,所以加个哨兵节点会好很多 ListNode res = new ListNode(0); res.next = node; while (res.next != null && res.next.next != null) { if (res.next.val == res.next.next.val) { //标记值 int a = res.next.val; while (res.next.val == a && res.next != null) { res.next = res.next.next; } } else { res = res.next; } } return node; }
6. 再论第一个公共子节点问题
6.1:拼接两个字符串
先看下面的链表A和B: A: 0-1-2-3-4-5 B: a-b-4-5 如果分别拼接成AB和BA会怎么样呢? AB:0-1-2-3-4-5-a-b-4-5 BA:a-b-4-5-0-1-2-3-4-5
发现拼接后从最后的4开始,两个链表是一样的了,自然4就是要找的节点,所以可以通过拼接的方式来寻找交点。
6.2:差和双指针
假如公共子节点一定存在第一轮遍历,假设La长度为L1,Lb长度为L2.则|L2-L1|就是两个的差值。第二轮遍历,长的先走|L2-L1|,然后两个链表同时向前走,结点一样的时候就是公共结点了。