算法通关第一村-链表白银挑战(Java)
1. 两个链表第一个公共子节点
- 剑指offer52::这是经典的链表问题:输入两个链表找出它们的第一个公共子节点,例如下面的两个链表:
- 两个链表的头节点都是已知的,相交后成为一个单链表,但相交的位置未知,并且相交之前的节点数也是未知的,请设计算法找到两个链表的合并点。
1.1 哈希和集合
-
//1.1 哈希和集合 public static LinkListNode findFirstCommonNodeBySet(LinkListNode headA, LinkListNode headB){ Set<LinkListNode> setA = new HashSet<>(); while(headA != null){//先将一个链表元素全部存到Map里 setA.add(headA); headA = headA.next; } while(headB != null){//然后一边遍历第二个链表headB,一边检测Hash中是否存在当前节点 if(setA.contains(headB)){ return headB; } headB = headB.next; } return null; }
1.2 使用栈
-
//1.2 使用栈 // 直到找出最晚出栈的那一组,需要两个O(n)的空间 //stackA.peek() 是Java中Stack类的一个方法。当你调用这个方法时, // 它会返回栈顶的元素,但不从栈中移除它。这与 stackA.pop() 不同, // 后者会移除并返回栈顶的元素。 public static LinkListNode findFirstCommonNodeByStack(LinkListNode headA, LinkListNode headB){ //使用两个栈,分别将两个链表的节点入两个栈 Stack<LinkListNode> stackA = new Stack(); Stack<LinkListNode> stackB = new Stack(); while(headA != null){ stackA.push(headA); headA = headA.next; } while(headB != null){ stackB.push(headB); headB = headB.next; } LinkListNode preNode = null;//指向当前两个栈中元素相等的节点 while(stackB.size() > 0 && stackA.size() > 0){ if(stackB.peek() == stackA.peek()){//,如果两个栈的栈顶元素相等,则分别出栈 preNode = stackA.pop(); stackB.pop(); }else{//此时两个链表出现分叉 break; } } return preNode; }
1.3拼接两个字符串(换表双指针)
-
-
//1.3拼接两个字符串 public LinkListNode findFirstCommonNode(LinkListNode pHead1, LinkListNode pHead2){ if(pHead1 == null || pHead2 == null){ return null; } LinkListNode p1 = pHead1; LinkListNode p2 = pHead2; while(p1 != p2){ p1 = p1.next; p2 = p2.next; if(p1 != p2){ //一个链表访问完了,就跳到另一个链表继续访问 if(p1 == null){ p1 = pHead2; } if(p2 == null){ p2 = pHead1; } } } return p1; }
-
接下来解释一下,循环体里为什么需要加一个判断 if(p1 != p2)。简单来说,如果序列不存在交集的时候会陷入死循环,例如 list1是1 2 3,list2是 4 5,很明显,如果不加判断,list1 和 liste2 会不断循环,出不来。
1.4差和双指针
-
假如公共子节点一定存在第一轮遍历,假设 La 的长度为 L1,Lb 的长度为 L2。则 | L2-L1 | 就是两个链表的差值。第二轮遍历,链表长的先走 | L2-L1 |,然后两个链表同时向前走,节点一样的时候就是公共节点了。
-
//1.4差和双指针 public LinkListNode findFirstCommonNode2(LinkListNode pHead1, LinkListNode pHead2){ if(pHead1 == null || pHead2 == null){ return null; } LinkListNode p1 = pHead1; LinkListNode p2 = pHead2; int length1 = 0, length2 = 0; //分别计算两个链表的长度 while(pHead1 != null){ pHead1 = pHead1.next; length1++; } while(pHead2 != null){ pHead2 = pHead2.next; length2++; } //长的链表先走 sub步 if(length1 > length2){ int sub = length1 - length2; while(sub > 0){ p1 = p1.next; sub--; } } if(length1 < length2){ int sub = length2 - length1; while(sub > 0){ p2 = p2.next; sub--; } } //同时遍历两个链表 while(p1 != p2){ p1 = p1.next; p2 = p2.next; } return p1; }
2. 判断链表是否为回文序列
-
LeetCode234:判断一个链表是否为回文链表:
-
-
基本的全部压栈解法:
-
public static boolean isPalindromeSequence(LinkListNode head){ Stack<LinkListNode> stack = new Stack(); LinkListNode curNode = head; while(curNode != null){//将链表节点存放到栈中 stack.push(curNode); curNode = curNode.next; } while(head != null){//一边出栈,一边遍历链表,进行值的比较 if(stack.peek().val == head.val){ stack.pop(); head = head.next; }else{ return false; } } return true; }
3.合并有序序列
3.1合并两个有序序列
-
LeetCode21 将两个升序链表合并为一个新的升序链表并返回,新链表是通过拼接给定的两个链表的所有节点组成的。
-
-
public static LinkListNode MergeTwoLinkedLists(LinkListNode list1, LinkListNode list2){ //-1 只是一个示例值,它并不代表任何实际意义,只用于作为虚拟头节点的值 LinkListNode preHead = new LinkListNode(-1); LinkListNode prev = preHead; while(list1 != null && list2 != null){ if(list1.val <= list2.val){ prev.next = list1; list1 = list1.next; }else{ prev.next = list2; list2 = list2.next; } prev = prev.next; } //最多只有一个还未被合并完,直接接上去就行 prev.next = list1 == null ? list2 : list1; return preHead.next; }
3.2合并K个有序链表
-
合并K个链表,先将前两个合并,之后再将后面的逐步合并进来,只要将两个合并的写清楚,合并K个就容易很多。
-
//合并k个有序链表 public LinkListNode MergeTwoLinkedLists(LinkListNode[] lists){ LinkListNode res = null; for (LinkListNode list : lists) {//快捷键iter回车 res = MergeTwoLinkedLists(res, list); } return res; }
3.3一道很无聊的好题
- LeetCode1669:给你两个链表list1 和 list2 , 它们包含的元素分别是n 个 和 m 个。将 list1 中下标从 a 到 b 的节点删除, 并将 list2 接在被删除节点的位置。
-
//一道很无聊的好题 public LinkListNode mergeInBetween(LinkListNode list1, int a, int b, LinkListNode list2) { LinkListNode pre1 = list1, post1 = list1, post2 = list2; int i = 0, j = 0; while (pre1 != null && post1 != null && j < b) { if (i != a - 1) {//pre1指向下标为a的前一个节点 pre1 = pre1.next; i++; } if (j != b) { post1 = post1.next; j++; } } post1 = post1.next;//post1指向下标为b的后一个节点 //寻找1ist2的尾节点 while (post2.next != null) { post2 = post2.next; } //链1尾接链2头,链2尾接链1后半部分的头 pre1.next = list2; post2.next = post1; return list1; }
4.双指针专题
4.1寻找中间节点
-
LeetCode876:给定一个头结点为 head 的非空链表,返回链表的中间节点。如果有两个中间节点,则返回第二个中间节点。
-
示例1 输入:[1, 2, 3, 4, 5] 输出:此列表的中间结点 3 示例2 输入:[1, 2, 3, 4, 5, 6] 输出:此列表的中间结点 4 -
这个问题用经典的快慢指针,用两个指针 slow 与 fast一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast到达链表的末尾时, slow必然位于中间
-
-
//寻找中间节点 public LinkListNode FindMiddleNode(LinkListNode head){ LinkListNode fast, slow; fast = slow = head; while(fast != null && fast.next != null){ slow = slow.next; fast = fast.next.next; } return slow; }
4.2寻找倒数第K个节点
-
输入一个链表,输出该链表中倒数第K个节点。从1开始计数,即链表的尾节点是倒数第1个节点 示例 给定一个链表:1->2->3->4->5, 和 k=2 返回链表 4->5 -
这里也可以使用快慢指针,我们先将 fast 向后遍历到第 k+1 个节点,slow 仍然指向链表的第一个节点,此时指针 fast 和 slow 二者之间刚好间隔 k 个节点。之后两个指针同步向后走,当 fast 走到链表的尾部空节点时,slow 指针刚好指向链表的倒数第 K个节点。
-
需要注意的是,链表的长度可能小于 K,寻找位置的时候必须判断 fast 是否为null。
-
//寻找倒数第K个节点 public LinkListNode getKthFromEnd(LinkListNode head, int k){ LinkListNode slow = head, fast = head; while(fast != null && k > 0){ fast = fast.next; k--; } while(fast != null){ fast = fast.next; slow = slow.next; } return slow; }
4.3旋转链表
-
LeetCode61:给你一个链表的头节点head, 旋转链表,将链表每个节点向右移动 K 个位置。
-
示例1: 输入:head = [1, 2, 3, 4, 5], k = 2 输出:[4, 5, 1, 2, 3]
-
思路:因为 K 有可能大于链表长度,所以首先取一下链表长度 length,然后 k = k % length,如果k ==0,则不用旋转,直接返回头结点,否则:
-
- 让快指针 fast先走 K 步;
- 快指针和慢指针一起走;
- 快指针走到链表尾部时,慢指针所在位置刚好是要断开的地方,把快指针的结点指向原链表头部,保存一下慢指针指向的节点的下一个节点,然后慢指针指向的节点断开和下一节点的联系。
- 返回结束时慢指针指向节点的下一个节点。
-
-
//旋转链表 public LinkListNode rotateRight(LinkListNode head, int k){ if(head == null || k == 0){ return head; } LinkListNode slow = head, fast = head; LinkListNode temp = head; int length = 0; while(head != null){//得到链表的长度 head = head.next; length++; } if(k % length == 0){ return temp; } //fast 从头节点向后走 //这里使用取模运算,是为了防止 K 大于 length 的情况 //例如,如果length=5, 那么 k=2 和 k=7 效果是一样的 while((k % length) > 0){ fast = fast.next; k--; } //当快指针走到链表尾节点时 while(fast.next != null){ fast = fast.next; slow = slow.next; } LinkListNode res = slow.next; slow.next = null; fast.next = temp; return res; }
5. 删除链表元素专题
- • LeetCode 237:删除某个链表中给定的(非末尾)节点。传入函数的唯一参数为要被删除的节点。
• LeetCode 1474.删除链表 M 个节点之后的 N个节点。
•LeetCode 83 存在一个按升序排列的链表,请你删除所有重复的元素,使每个元素只出现一次。
•LeetCode 82 存在一个按升序排列的链表,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。
我们在链表基本操作部分介绍了删除的方法,至少需要考虑删除头部,删除尾部和中间位置三种情况的处理。而上面这些题目就是这个删除操作的进一步拓展。
5.1删除特定节点
-
LeetCode203:给你一个链表的头节点 head 和一个整数 val,请你删除链表中所有满足Node.val == val 的节点,并返回新的头节点。
-
示例1: 输入:head = [1, 2, 3, 4, 5, 6], val = 6 输出:[1, 2, 3, 4, 5]
-
对于删除,我们注意到首元素的处理方式与后面的不一样。为此,我们可以先创建一个虚拟节点 dummyHead,使其指向 Head,即 dummyHead.next = next,这样就不用单独处理首节点了。
-
完整步骤:
-
- 我们创建一个虚拟链表头 dummyHead, 使其指向 Head。
- 开始循环链表寻找目标元素,注意要通过 cur.next.val 来判断。
- 如果找到目标元素,就使用 cur.next = cur.next.next 来删除。
- 注意最后返回的时候要用 dummyHead.next, 而不是 dummyHead。
-
-
//删除特定节点 public LinkListNode DeleteSpecificNode(LinkListNode head, int val){ LinkListNode dummyHead = new LinkListNode(0); dummyHead.next = head; LinkListNode cur = dummyHead; while(cur.next != null){ if(cur.next.val == val){ cur.next = cur.next.next; }else{ cur = cur.next; } } return dummyHead.next; }
5.2删除倒数第n个节点
-
LeetCode 19. 删除链表的倒数第 N 个节点,并且返回链表的头节点。
-
示例1: 输入:head = [1, 2, 3, 4, 5], n = 2 输出:[1, 2, 3, 5]
-
方法1:计算链表长度
-
首先从头节点开始对链表进行一次遍历,得到链表的长度 length,随后我们再从头节点开始对链表进行一次遍历,当遍历到第 length-n+1 个节点时,它就是我们需要删除的节点。
-
//删除倒数第n个节点 //计算链表长度 public LinkListNode removeNthFromEnd(LinkListNode head, int n){ LinkListNode dummyHead = new LinkListNode(0); dummyHead.next = head; LinkListNode cur = dummyHead; int length = 0; while(head != null){ head = head.next; length ++; } for(int i = 1; i < length-n+1; ++i){ cur = cur.next; } cur.next = cur.next.next; return dummyHead.next; }
-
方法2:双指针
-
我们定义 first 和 second 两个指针,first 先走 n 步,然后 second 再开始走,当 first 走到队尾的时候,second 就是我们要的节点。
-
//双指针 public LinkListNode removeNthFromEnd2(LinkListNode head, int n){ LinkListNode dummyHead = new LinkListNode(0); dummyHead.next = head; LinkListNode first = dummyHead, second = dummyHead; for(int i = 1; i <= n; i++){ first = first.next; } while(first.next != null){ first = first.next; second = second.next; } second.next = second.next.next; return dummyHead.next; }
5.3删除重复元素
- 关于结点删除的题:
LeetCode 83 存在一个按升序排列的链表,请你删除所有重复的元素,使每个元素只出现一次。
LeetCode 82 存在一个按升序排列的链表,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。
两个题其实是一个,区别就是一个要将出现重复的保留一个,一个是只要重复都不要了,处理起来略有差别。LeetCode 1836是在82的基础上将链表改成无序的了,难度要增加不少,感兴趣的话可以自己研究一下。
5.3.1重复元素保留一个
-
LeetCode 83 存在一个按升序排列的链表,给你这个链表的头节点 head,请你删除所有重复的元素,使每个元素只出现一次。返回同样按升序排列的结果的链表。
-
示例1: 输入:head = [1, 1, 2, 3, 3] 输出:[1, 2, 3]
-
由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。具体地,我们从指针cur 指向链表的头节点,随后开始对链表进行遍历。如果当前 cur 与cur.next 对应的元素相同,那么我们就将cur.next 从链表中移除;否则说明链表中已经不存在其它与cur 对应的元素相同的节点,因此可以将 cur 指向cur.next。当遍历完整个链表之后,我们返回链表的头节点即可。另外要注意的是 当我们遍历到链表的最后一个节点时,cur.next 为空节点,此时要加以判断。
-
//删除重复元素 //重复元素保留一个 public LinkListNode deleteDuplicates(LinkListNode head){ if(head == null){ return head; } LinkListNode 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重复元素都不要
-
LeetCode 82 存在一个按升序排列的链表,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。
-
示例1: 输入:head = [1, 2, 3, 3, 4, 4, 5] 输出:[1, 2, 5] -
当一个都不要是,链表只要直接对 cur.next 以及 cur.next.next 两个node进行比较就行了,这里要注意两个node都可能为空,稍加判断就行了。
-
//重复元素都不要 public LinkListNode deleteDuplicates2(LinkListNode head){ if(head == null){ return head; } LinkListNode dummyHead = new LinkListNode(0); dummyHead = head; LinkListNode cur = dummyHead; while(cur.next != null && cur.next.next != null){ if(cur.next.val == cur.next.next.val){ int x = cur.next.val; while(cur.next != null && cur.next.val == x){ cur.next = cur.next.next; } }else{ cur = cur.next; } } return dummyHead; }
if(head == null){ return head; } LinkListNode dummyHead = new LinkListNode(0); dummyHead = head; LinkListNode cur = dummyHead; while(cur.next != null && cur.next.next != null){ if(cur.next.val == cur.next.next.val){ int x = cur.next.val; while(cur.next != null && cur.next.val == x){ cur.next = cur.next.next; } }else{ cur = cur.next; } } return dummyHead; }