1. 基本的解题套路和方法
1.1 反转单链表
链表问题最经典的模版代码
设置三个额外的变量,遍历结束时,pre指向最后一个节点,cur和next都指向null
public ListNode reverseList(ListNode head) { // 设置额外两个指针 ListNode pre = null; ListNode cur = head; ListNode next = null; //从头反转到尾 while(cur!=null){ // 先记录下一个 next = cur.next; // 当前的反转 cur.next = pre; // 更新到下一个, 下面两行代码顺序不可调换! pre = cur; cur = next; } //指定次数的反转 while(reverseLength-- > 0){ nextNode = curNode.next; curNode.next = preNode; // 更新 preNode = curNode; curNode = nextNode; } return pre; }
1.2 增加dummy节点
在一些删除问题和遍历问题中,为了防止出现单独处理头节点的情况或者头节点被删除,需要换头,可以增加一个虚拟节点,让头节点指向它。
// 设置一个虚拟节点 ListNode dummyNode = new ListNode(-1); dummyNode.next = head; ListNode pre = dummyNode; while(pre != null){ //对pre.next的一些列操作 pre = pre.next; } return dummyNode.next;
1.3 快慢指针
设置两个指针,通常一个快一个慢,一前一后,且他们保持一定的间距,一起遍历链表,结束条件是快指针决定的。
1.4 链表的长度
单链表如果需要统计它的长度可以用如下模版
ListNode cur = head; int len = 0; while(cur!=null){ cur = cur.next; ++len; }
2. 题目
2.1 反转
反转链表
Nr. 206
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
1.迭代方法
略
2.递归方法
比较难以理解,配合动图实用更佳
还有递归方法的详细拆解
public ListNode reverseList(ListNode head) { //递归终止条件是当前为空,或者下一个节点为空 if(head==null || head.next==null) { return head; } //这里的cur就是最后一个节点 ListNode cur = reverseList(head.next); //这里请配合动画演示理解 //如果链表是 1->2->3->4->5,那么此时的cur就是5 //而head是4,head的下一个是5,下下一个是空 //所以head.next.next 就是5->4 head.next.next = head; //防止链表循环,需要将head.next设置为空 head.next = null; //每层递归函数都返回cur,也就是最后一个节点 return cur; }
反转链表II
Nr. 92
题目描述:
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
保证1<=m<=n<=链表长度
输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL
解题思路:
设置一个beforeLeft节点,记录前一个反转区域,设置一个afterRight节点,记录最后一个反转的下一个节点。
先找到leftNode节点,从这里进入,开始按照普通翻转方法开始,到结束时,preNode指向rightNode,cur指向afterRight节点。
再根据L是否等于1,决定是否处理头节点。
1.链表原始情况
2.不换头
beforeLeftNode.next = preNode; leftNode.next = curNode; return head;
3.换头
head = preNode; leftNode.next = curNode; return head;
4.整体代码
public static ListNode reverseList(ListNode head, int L, int R){ // 1. 找到反转区域的前一个节点 beforeLeftNode ListNode beforeLeftNode = null; // 找到开始反转的右节点 ListNode leftNode = head; if(L == 1){ beforeLeftNode = null; leftNode = head; }else { // L >= 2 beforeLeftNode = head; int temp = m; //注意这里的次数,若temp为2直接跳过while循环 while(--temp>1){ beforeLeftNode = beforeLeftNode.next; } leftNode = beforeLeftNode.next; } // 2. 先确定反转区域 int reverseLength = R-L+1; // 从curNode开始是入口 ListNode curNode = leftNode; ListNode nextNode = null; ListNode preNode = null; // R-L+1 次 while(reverseLength-- > 0){ nextNode = curNode.next; curNode.next = preNode; // 更新 preNode = curNode; curNode = nextNode; } // 反转完毕,处理头节点 和 两个端节点 // 反转结束时, 和普通反转链表一样,preNode指向反转的最后一个节点,cur和next都指向下一个节点 if (L == 1){//考虑要换头节点 head = preNode; System.out.println("换头了"); }else { beforeLeftNode.next = preNode; } leftNode.next = curNode; return head; }
链表k个一组反转
Nr.25
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
给你这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
解题思路:
每个k组反转,都要找到他们的前驱节点,但是以头节点开始的那一组没有前驱节点,因此定义一个假的节点,假节点的next指向head。
dummy->1->2->3->4->5初始化pre和end都指向dummy。pre指每次要翻转的链表的头结点的上一个节点。end指每次要翻转的链表的尾节点
当end不为null, end先遍历k次到指定位置,如果剩下的链表长度不足K,立刻退出,不用反转;
找到本次链表的开始节点start, 它也是反转结束后的最后一个节点,也会是本次更新的pre和end;
再记录下次要反转的部分的开头, next;
断开本次k长度的部分,方便反转链表的子函数;
执行局部反转, 返回反转后的头节点;
start.next会因为反转函数变为null,通过.next把断开的链表重新链接。
public static ListNode reverseKGroup(ListNode head, int k){ if (head == null || head.next == null){ return head; } // 需要两个节点 pre 和 end,分别记录反转区域的前缀和最后一个反转区域 // 建立一个虚拟节点dummy, 记录头节点的前缀 ListNode dummy = new ListNode(0); dummy.next = head; ListNode pre = dummy; ListNode end = dummy; // 建立两个节点 start 和 next, 分别记录开始反转的节点 和 下一批要反转的节点 while(end!= null){ // end 遍历k次到指定位置 for(int i=0; i<k && end!=null; i++){ end = end.next; } if (end == null){ // 剩下的链表长度不足K,立刻退出,不用反转 break; } // 找到本次链表的开始节点, 它也是反转结束后的最后一个节点,也会是本次更新的pre和end ListNode start = pre.next; // 记录下次要反转的部分的开头 ListNode next = end.next; // 断开本次k长度的部分 end.next = null; // 执行局部反转, 返回反转后的头节点 pre.next = reverseList(start); // start.next会因为反转函数变为null,通过.next把断开的链表重新链接。 start.next = next; pre = start; end = start; } return dummy.next; }
2.2 删除
删除链表节点(力扣版)
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
注意:此题对比原题有改动
示例 1:
请在这里输入引用内容
输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
解题思路:
设置一个虚拟节点,防止删除的节点是head,这样分情况讨论很麻烦。
public ListNode deleteNode(ListNode head, int val) { if(head == null){ return null; } // 设置一个虚拟节点 ListNode dummyNode = new ListNode(-1); dummyNode.next = head; ListNode pre = dummyNode; while(pre!=null){ // 发现待删除的点 if(pre.next.val == val){ // pre.next -> cur, pre指向下一个节点,绕过待删除节点 pre.next = pre.next.next; // 结束循环 break; } pre = pre.next; } return dummyNode.next; }
删除链表节点(剑指版)
待删除节点不是以val给出,而是以ListNode的形式给出的。如何做到时间复杂度O(1)。
解题思路:
类似于乾坤大挪移,把待删除节点的后一个元素赋值(val和next)给待删除节点,也就相当于删除了当前元素。
如果删除的节点是尾节点,就需要从头遍历到尾节点的前一个节点,把它删除。这个复杂度是O(n)。但最后的平均复杂度是O(1)。
class deleteNode {public static ListNode deleteNode(ListNode head, ListNode val){ if (head == null || val == null){ return null; } if (val.next != null){ // 待删除节点不是尾节点 ListNode next = val.next; val.val = next.val; val.next = next.next; } else if (head == val){ // 待删除节点只有一个节点,此节点为头节点 head = null; } else { // 待删除节点为尾节点 ListNode cur = head; while (cur.next != val){ cur = cur.next; } cur.next = null; } return head; } }
掌握了这个思想,这道题就可以秒了, 删除中间节点
删除排序链表中的重复元素 II
Nr.82
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中没有重复出现的数字。
示例 1:
请在这里输入引用内容
输入: 1->2->3->3->4->4->5
输出: 1->2->5
解题思路:
链表可能会删除头节点,因此设置一个虚拟节点dummy。并设置快慢指针,其中当慢指针和快指针相等时,说明相邻节点是相等的,让快指针一直遍历到不相等的位置,把它们一并删除。
public ListNode deleteDuplicates(ListNode head) { // 边界条件: 空链表,只有一个节点的链表,不会发生删除 if(head == null || head.next == null){ return head; } // 先建立虚拟节点 ListNode dummy = new ListNode(-1); dummy.next = head; ListNode left = dummy; ListNode right = head; // 开始遍历,直到right到达最后一个节点, 这里的第一个条件是兼顾到内层的while循环 // 因为比较的是right.next,所以它不能为null while(right!=null && right.next!=null){ // 如果不相等,则向下遍历 if(left.next.val != right.next.val){ left = left.next; right = right.next; }else{ // 有相同节点 // 逻辑判断的短路效应: 条件的顺序是有关系的 // 遍历到不相等的位置,退出循环时left在相等节点的前一个,right在不相等节点 while(right!=null && left.next.val==right.val){ right = right.next; } // 让left指向right,跳过所有的相等节点 left.next = right; } } return dummy.next; }
删除排序链表中的重复元素
Nr. 83
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例 1:
请在这里输入引用内容
输入: 1->1->2
输出: 1->2
解题思路:
和82相比,这道题我们可以推断出,头节点一定不会被删除,因为是升序链表,因此不用设置虚拟节点,直接用快慢指针解决。因为没有虚拟节点,不用像82那样比较快慢指针的next,而是直接比较。
public ListNode deleteDuplicates(ListNode head) { if(head == null){ return null; } ListNode left = head; ListNode right = head.next; while(right!=null){ // 不相等时,遍历到下一个,始终保持快慢间距 if(left.val != right.val){ left = left.next; right = right.next; }else{ // 找到不相等的点 while(right!=null && left.val == right.val){ right = right.next; } // 退出循环时,right为不相等的第一个数, left为第一个相等的数 left.next = right; } } return head; }
2.3 交点/环/合并
两个链表的第一个公共节点
Nr. 剑指 Offer 52.
输入两个链表,找出它们的第一个公共节点。
解题思路:
设置两个指针,一起遍历,若有公共交点一定是两个指针同时到达,所有第一种思路是统计两个链表的长度,让长链表的指针先走,走到短链表的头,然后再一起走。
另一种就是,两个指针从头一起遍历,当到达末尾时,让两个指针分别指向对方的头节点,若有交点,一定会相遇。若没有交点,最后一起为null,退出循环。原理就是短链表的快指针走得快,让他再去长链表遍历,最后会一起同步。
原理是, 两个链表长度分别为L1+C、L2+C, C为公共部分的长度,第一个人走了L1+C步后,回到第二个人起点走L2步;第2个人走了L2+C步后,回到第一个人起点走L1步。 当两个人走的步数都为L1+L2+C时相交。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) { if(headA == null || headB == null){ return null; } // 虐狗法 ListNode p1 = headA; ListNode p2 = headB; while(p1 != p2){ p1 = p1 != null ? p1.next : headB; p2 = p2 != null ? p2.next : headA; } return p1; }
合并两个排序的链表
Nr. 剑指 Offer 25
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
请在这里输入引用内容
示例1:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
解题思路:
类似于,归并排序的思想,双指针,谁小移动谁,合并。设置一个虚拟节点模拟排序后链表的头部的前一个节点,依次添加节点,最后返回dummy.next。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) { // 合并到第一个元素较小的链表中 if(l1 == null && l2 == null){ return null; } if(l1 == null){ return l2; } if(l2 == null){ return l1; } // 用虚拟节点来记录,最后要返回的头节点信息 ListNode dummyNode = new ListNode(-1); ListNode cur = dummyNode; while(l1!=null && l2!=null){ if(l1.val <= l2.val){ cur.next = l1; l1 = l1.next; }else{ cur.next = l2; l2 = l2.next; } cur = cur.next; } cur.next = l1 == null ? l2 : l1; return dummyNode.next; }
环形链表
Nr. 141
给定一个链表,判断链表中是否有环。
解题思路:
我们遍历所有结点并在哈希表中存储每个结点的引用(或内存地址)。如果当前结点为空结点 null(即已检测到链表尾部的下一个结点),那么我们已经遍历完整个链表,并且该链表不是环形链表。如果当前结点的引用已经存在于哈希表中,那么返回 true(即该链表为环形链表)。但是这种方法的空间复杂度是O(n)。
public boolean hasCycle(ListNode head) { Set<ListNode> nodesSeen = new HashSet<>(); while (head != null) { if (nodesSeen.contains(head)) { return true; } else { nodesSeen.add(head); } head = head.next; } return false; }
进阶方法,借助快慢指针,让快指针每次走两步,慢指针走一步,如果链表有环,二者一定会相遇,可以想像在环形赛道内,一个运动员跑得慢一个跑得快,最后一定会套圈相遇。定量分析就是:二者的距离/速度差。
public boolean hasCycle(ListNode head) { if(head == null){ return false; } // 采用快慢指针的方法 ListNode f = head; ListNode s = head; // 在环内追逐 boolean res = false; // 如果没有环,一定是fast先出条件 while(f!=null && f.next!=null){ f = f.next.next; s = s.next; if(f == s){ return true; } } return res; }
环形链表 II
Nr. 142
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
解题思路:
和141类似可以用哈希表,找到第一个出现过的节点就是入环节点,但缺点就是空间复杂度高。现在介绍进阶方法。详细参考
设置两个指针,一快一慢,快的(f)每次走两步,慢的(s)每次走一步。如果链表没有环,f指针会先到达null退出循环,表示无环。
如果有环,f和s一定会在环内某个位置相遇,假设环外节点a步,环内节点b步,接下来计算他们相遇时走了多少步,f每次走两步都统计进去。f指针比s多走二倍,所以f=2s,并且它们都走过了环外节点a步,且在环内转圈后相遇,因此f=s+nb,表示f走过s的步数,且在圈内多转了n次。
f = 2s 和 f = s + nb可以得出重要结论一: s=nb,就是说慢指针在相遇时已经走了nb步。
现在想要到达入环节点,每个从头节点出发走过a+nb步后都会在入环节点,因为nb是整数圈,最后还是会在入环节点处。第二个重要结论就是:慢指针只要走过a+nb步就会在入环节点处,它已经走了nb步,就差a步,借助f指针帮助它,让f在第一次相遇后,回到头部,和s指针一起走,当相遇时,一定是在入环节点。
public ListNode detectCycle(ListNode head) { if(head == null){ return null; } ListNode fast = head; ListNode slow = head; // 快慢指针第一次相遇,慢指针走了 nb步 while(true){ // 可以遍历到null,链表没有环 if(fast==null || fast.next==null){ return null; } // 有环 fast = fast.next.next; slow = slow.next; if(fast == slow){ // 相遇时,跳出循环 break; } } // 此时快指针移动到头部,再走a步,慢指针也走a步,走a+nb步,一定到入环节点 fast = head; while(fast != slow){ fast = fast.next; slow = slow.next; } return fast; }
2.4 链表长度
倒序打印链表
Nr. 剑指 Offer 06. 从尾到头打印链表
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
请在这里输入引用内容
示例 1:
输入:head = [1,3,2]
输出:[2,3,1]
解题思路:
用模版统计出链表的长度,然后分配一个数组,再从头遍历链表,把值从尾到头的填入数组中。
public int[] reversePrint(ListNode head) { if(head == null){ return new int[0]; } // 第一次遍历,希望得到链表的长度 ListNode cur = head; int len = 0; while(cur!=null){ cur = cur.next; ++len; } // 新建数组 int[] res = new int[len]; cur = head; int index = len-1; while(cur!=null){ res[index] = cur.val; cur = cur.next; --index; } return res; }
链表中倒数第k个节点
Nr. 剑指 Offer 22
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。
请在这里输入引用内容
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
解题思路:
一种思路是统计处链表的长度,再去遍历到len-k的节点,这样不能一次遍历完。而快慢指针可以做到一次遍历完,且不用处理一些数值关系。
思路就是,让快指针先走k步,慢指针在头节点,一起遍历,当快指针到达null时,慢指针此刻就在倒数k的位置。
public ListNode getKthFromEnd(ListNode head, int k) { if(head == null || k==0){ return null; } // 设置快慢指针 ListNode former = head; ListNode latter = head; // 快指针先走k步 while(k-- >0){ // k 设置的超过链表长度,不合理 if(former == null){ return null; } // 假设是倒数第k节点,最后一次,fomer=null former = former.next; } // 一起移动,直到快指针先到达尾部 while(former!=null){ former = former.next; latter = latter.next; } return latter; }
2.5 复制链表
复制一种复杂链表
Nr. 剑指 Offer 35.
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
解题思路:
这个是链表的深度拷贝,每个节点都是新建立,关键在于如果复制random的关系。可以用哈希表记录一种映射,老节点对应新节点。
遍历新节点,同时也是遍历老节点,给新节点建立他们自己的next和random关系。方法是,找到老节点的next,再通过哈希表找到对应的新节点的,同理random也是。空间复杂度是O(n),时间复杂度O(n)
// 完成一种深度拷贝 public Node copyRandomList(Node head) { // 哈希表的方法 // 用哈希表记录,老节点 和 新节点 HashMap<Node, Node> map = new HashMap<Node, Node>(); Node cur = head; while(cur!=null){ map.put(cur, new Node(cur.val)); cur = cur.next; } // 接下来为新节点,设置它们的 next 和 random cur = head; while(cur!=null){ map.get(cur).next = map.get(cur.next); map.get(cur).random = cur.random == null ? null : map.get(cur.random); cur = cur.next; } return map.get(head); }
另一种方法可以做到空间复杂度O(1),方法就是把新链表的节点都挂在老链表的后表,这样我们也仿照哈希表,建立起新老节点的映射关系,老得Node.next 是 新的Node。
public Node copyRandomList(Node head) { if(head == null){ return null; } // 空间复杂度为O(1)的方法,用老节点后面跟着新节点,代替了hash表 Node cur = head; Node next = null; // 每次把新产生的节点放在老节点的后面 while(cur!=null){ // 记录老节点的下一个 next = cur.next; cur.next = new Node(cur.val); cur.next.next = next; cur = next; } // 设置新节点的 random // 先找到老节点的random,新节点的random就是它的next cur = head; Node curCopy = null; while(cur!=null){ // 记录老节点的下一个, 这次加倍了 next = cur.next.next; // 新节点 curCopy = cur.next; curCopy.random = cur.random == null ? null : cur.random.next; cur = next; } // 把新老节点拆分开,恢复成原有的样子 // 重新设置每个节点的next cur = head; Node res = head.next; while(cur!=null){ // 记录老节点的下一个, 这次加倍了 next = cur.next.next; curCopy = cur.next; curCopy.next = next==null ? null : next.next; cur.next = next; cur = next; } return res; }