文章目录
链表
链表是我们的老朋友的,我们也许对链表的操作聊熟于心,但是对于链表的套路题,我们还真不一定了解
一、基本操作
206-反转链表(必须牢记)
题目
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
思考
这是链表的经典题目了
我们尝试用递归和迭代两种解法去解
- 迭代 gif
只要想起这张图,就一定能把代码写出来
- 递归
递归的实现要稍微复杂一点
1、假设原本的链表长这个样子
2、在某个中间过程中,已经有部分被倒转了
3、部分代码解释,在图上标明了
题解
- 递归
在写递归的代码时候,一定要牢记递归解释的2、3张图
public ListNode reverseList(ListNode head) {
/**
* 方法1 递归
*/
if (head==null || head.next==null) return head;
ListNode newHead = reverseList(head.next);
head.next.next=head;
head.next=null;
return newHead;
}
- 迭代
public ListNode reverseList(ListNode head) {
/**
* 方法2 迭代
* 在做这道题的时候,去想那个 gif
*/
ListNode pre=null;
ListNode curr=head;
while (curr!=null) {
ListNode tmp = curr.next;
curr.next=pre;
pre=curr;
curr=tmp;
}
return pre;
}
21-合并有序链表
题目
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
提示:
- 两个链表的节点数目范围是
[0, 50]
-100 <= Node.val <= 100
l1
和l2
均按 非递减顺序 排列
思考
一样,有递归和非递归两种写法
- 递归
递归的过程,可以想成成,将两根橡皮泥,搓成一根橡皮泥的过程
- 非递归
迭代要注意几点
1、记得最后的收尾工作
2、头部可以在一开始设置一个假头部,这样,可以避开每次都要判断头部是否为空的问题
题解
- 递归
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1==null) return l2;
if (l2==null) return l1;
if (l1.val<=l2.val) {
l1.next=mergeTwoLists(l1.next,l2);
return l1;
}
if (l2.val<l1.val) {
l2.next=mergeTwoLists(l1,l2.next);
return l2;
}
return null;
}
- 非递归
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head=new ListNode(-1);
ListNode tail=head;
while (l1!=null && l2!=null) {
if(l1.val<=l2.val) {
tail.next=l1;
l1=l1.next;
} else {
tail.next=l2;
l2=l2.next;
}
tail=tail.next;
}
// 收尾工作
tail.next=l1==null?l2:l1;
return head.next;
}
24-两两交换链表节点
题目
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
思考
我的第一反应就是递归(后来发现超越 100%,还是很 nice 的)
这里要注意,要定义一个 fake 节点,用来指向头结点
因为 head 指针在交换的过程中,会变化位置,无法用它来指向链表第一个位置
这也是链表题中的一个小技巧
题解
public ListNode swapPairs(ListNode head) {
// 用一个 fake 节点,指向头结点,这样,头结点永远不会丢失
ListNode fake = new ListNode(-1);
fake.next=head;
swapRecursion(fake,head);
return fake.next;
}
public void swapRecursion(ListNode pre,ListNode node) {
if (node==null || node.next==null) return;
// 交换节点
ListNode next = node.next;
node.next=next.next;
next.next=node;
pre.next=next;
// 递归
swapRecursion(node,node.next);
}
二、其他操作
160-相交链表
题目
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
图示两个链表在节点 c1
开始相交**:**
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
提示:
listA
中节点数目为m
listB
中节点数目为n
0 <= m, n <= 3 * 104
1 <= Node.val <= 105
0 <= skipA <= m
0 <= skipB <= n
- 如果
listA
和listB
没有交点,intersectVal
为0
- 如果
listA
和listB
有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]
思考
**方法一:**使用集合
判断两个链表是否相交,可以使用哈希集合存储链表节点。
首先遍历链表 headA,并将链表 headA 中的每个节点加入哈希集合中。然后遍历链表 headB,对于遍历到的每个节点,判断该节点是否在哈希集合中:
-
如果当前节点不在哈希集合中,则继续遍历下一个节点;
-
如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都在两个链表的相交部分,因此在链表 headB 中遍历到的第一个在哈希集合中的节点就是两个链表相交的节点,返回该节点。
如果链表 headB 中的所有节点都不在哈希集合中,则两个链表不相交,返回 null。
**方法二:**双指针
假设链表 A 的头节点到相交点的距离是 a,链表 B 的头节点到相交点的距离是 b,相交点 到链表终点的距离为 c。我们使用两个指针,分别指向两个链表的头节点,并以相同的速度前进, 若到达链表结尾,则移动到另一条链表的头节点继续前进。按照这种前进方法,两个指针会在 a + b + c 次前进后同时到达相交节点。
我们的中心思想,是让两个节点,在同一时间,走完 a+b+c 长度的路程,这时候,他们一定是在交汇点
题解
- 使用 集合
public ListNode getIntersectionNode(ListNode headA, ListNode headB) { Set<ListNode> set = new HashSet<>(); while (headA!=null) { set.add(headA); headA=headA.next; } while (headB!=null) { if (set.contains(headB)) return headB; headB=headB.next; } return null; }
- 双指针
public ListNode getIntersectionNode(ListNode headA, ListNode headB) { ListNode h1 = headA; ListNode h2 = headB; while (h1!=h2) { h1=h1==null?headB:h1.next; h2=h2==null?headA:h2.next; } return h1; }
这里可能会有个疑问:
代码这么写,如果是两个链表不相交的情况,不会出现死循环吗?
其实不会
链表 headA 和 headB 的长度分别是 mm 和 nn。考虑当 m=n 和 m!=n 时,两个指针分别会如何移动:
如果 m=n,则两个指针会同时到达两个链表的尾节点,然后同时变成空值 null,此时返回 null;
如果 m!=n,则由于两个链表没有公共节点,两个指针也不会同时到达两个链表的尾节点,因此两个指针都会遍历完两个链表,在指针 pA 移动了 m+n 次、指针 pB 移动了 n+m 次之后,两个指针会同时变成空值 null,此时返回 null。
234-回文链表
题目
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2输出: false
示例 2:
输入: 1->2->2->1输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
思考
**方法一:**将链表转换为数组,然后判断数组是否回文
**方法二:**先使用快慢指针找到链表中点,再把链表切成两半;然后把后半段翻转,最后比较两半是否相等。
题解
方法一:
public boolean isPalindrome(ListNode head) { /** * 方法一: 链表转数组 */ if (head==null) return true; List<Integer> list = new ArrayList<>(); while (head!=null) { list.add(head.val); head=head.next; } int i=0,j=list.size()-1; while (i<=j) { if (list.get(i)!=list.get(j)) return false; i++; j--; } return true; }
方法二:
public boolean isPalindrome(ListNode head) { /** * 方法二:折半翻转,再判断 */ if (head==null || head.next==null) return true; // 使用快慢指针,找链表中点 ListNode fast = head; ListNode slow = head; while (fast.next!=null && fast.next.next!=null) { fast=fast.next.next; slow=slow.next; } slow.next=reverse(slow.next); slow=slow.next; while (slow!=null) { if (head.val!=slow.val) return false; head=head.next; slow=slow.next; } return true; } /** * 翻转链表 * @param head * @return */ ListNode reverse(ListNode head) { if (head==null || head.next==null) return head; ListNode pre=null; ListNode curr=head; while (curr!=null) { ListNode tmp = curr; curr=curr.next; tmp.next=pre; pre=tmp; } return pre; }
小结
- 担心头是空的?无法判断头的位置?在 new 一个 fake 节点,指向头吧
用到该技巧的题目有 21 160 题
- 链表判环,固定使用快慢指针法!