白银挑战-链表高频面试算法题
题目大纲
链表经典题目合集:
题目 | 链接 |
---|---|
第一个公共节点 | 剑指 Offer 52. 两个链表的第一个公共节点 |
判断链表是否为回文 | 234. 回文链表 |
合并两个有序链表 | 21. 合并两个有序链表 |
寻找链表的中间节点 | 876. 链表的中间结点 |
寻找倒数第K个元素 | 剑指 Offer 22. 链表中倒数第k个节点 |
旋转链表 | 61. 旋转链表 |
删除特定节点 | 237. 删除链表中的节点 |
删除倒数第n个节点 | 19. 删除链表的倒数第 N 个结点 |
删除链表中的重复元素 | 83. 删除排序链表中的重复元素 |
删除链表中的重复元素Ⅱ | 82. 删除排序链表中的重复元素 II |
链表定义与 leetcode 中一致
class ListNode {
public int val;
public ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
1. 两链表第一个公共子节点
链接:https://leetcode.cn/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/
解法1:哈希与集合
public ListNode findFirstCommonNodeNySet(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;
}
- 总体思路:将第一个链表中的元素存在哈希集合中,之后逐步遍历第二个链表,同时查找有没有重复的元素。如果能够查找到,说明有公共链表。这里空间复杂度为
O(n)
解法2:使用栈
public ListNode findFirstCommonNodeByStack(ListNode headA, ListNode headB) {
Stack<ListNode> stackA = new Stack();
Stack<ListNode> stackB = new Stack();
while(headA != null) {
stackA.push(headA);
headA = headA.next;
}
while(headA != null) {
stackA.push(headA);
headA = headA.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;
}
- 总体思路:不推荐,因为这里会用到两个栈,需要用到两个
O(n)
的空间。主要的目的是训练使用栈的一些api
解法3:双指针法
链表A:4-1-8-4-5
链表B:5-0-1-8-4-5
首先,这道题的前提是两个链表必有公共的部分,那么我们可以说他们必有重合的点。我们可以把链表分成四个部分,left-A
,right-A
,left-B
,right-B
。其中,right-A = right-B
。
那么,我们就可以将两个链表交叉进行拼接,得到这样的结构:
他们的长度相同,且最后一部分相等,我们就可以得出前面的部分也相等这样一个结论。那我们就可以重复遍历AB链表。之后如果得到了相同的节点的话,那么我们就可以说明,他们已经在重合的部分了。
public ListNode findFirstCommonNodeByStack(ListNode headA, ListNode headB) {
if(headA == null || headB == null) {
return null;
}
ListNode p1 = headA;
ListNode p2 = headB;
while(p1 != p2) {
p1 = p1.next;
p2 = p2.next;
// 防止下个节点相同的情况出现
if(p1 != p2) {
// 一个链表访问完了就跳到另一个节点访问
if(p1 == null) {
p1 = headB;
}
if(p2 == null) {
p2 = headB;
}
}
}
return p1;
}
- 总结:这里主要的是空间复杂度比较低,不需要创造额外的空间即可完成重复节点的查找。
2. 判断链表是否为回文序列
链接:https://leetcode.cn/problems/palindrome-linked-list/
这里可以想到运用栈结构先进后出这样一个特点来进行遍历,而且注意到,如果是回文链表的话,那么只需要遍历到他长度的一半即可,一半进行压栈,另外一半进行比较。
public boolean isPalindrome(ListNode head) {
// 准备两个快慢指针
ListNode pre = head;
ListNode post = head;
Stack<Integer> stack = new Stack<>();
// 这里用.next作为循环结束的条件是为了防止空指针异常
while(post != null) {
// 对应只有单数的情况,跳掉最中间的节点
if(post.next == null) {
pre = pre.next;
break;
}
stack.push(pre.val);
pre = pre.next;
post = post.next.next;
}
// 此时 栈中进入了一半的链表,开始比较
while(!stack.isEmpty() && pre != null) {
if(stack.pop() != pre.val) {
return false;
}
pre = pre.next;
}
// 如果有多余的节点也不行
return true;
}
- 这里要做到清晰认知双指针情况下求一半长度奇数和偶数分别的情况。
- 长度为奇数,那么
post
指针的next就直接为null了; - 长度为偶数,
post.next.next
指针为null了;
- 长度为奇数,那么
运用这个性质,我们区分了奇偶数数组,这里判断出奇数数组之后,我们需要将中间元素给忽略掉。
3. 合并有序链表
3.1 合并两个有序链表
链接:https://leetcode.cn/problems/merge-two-sorted-lists/
这道题的思路其实很简单,就是不断的去遍历节点,之后将小的节点放在新的链表中。
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode prehead = new ListNode(-1);
// 临时节点,用于遍历赋值
ListNode 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;
}
要注意的就是链表的遍历手段,这里如何每次只拼接上一个节点呢?
prev.next = list1; list1 = list1.next;
将我们要实现的数组指向要拼的节点,节点在走向下一个节点,那么这时候,我们就能够得到一个我们想要的节点了。
3.2 合并多个有序链表
public ListNode mergeKLists(ListNode[] lists) {
ListNode res = null;
for(ListNode list : lists) {
res = mergeTwoLists(res, list);
}
return res;
}
4. 双指针
4.1 寻找中间节点
链接:https://leetcode.cn/problems/middle-of-the-linked-list/
这里我们要用到双指针法,用一快一慢两个指针去遍历链表,慢指针一次走一步,快指针一次走两步。那么快指针达到链表的末尾的时候,慢指针就会到达中间。分为奇偶数两种情况,但是奇偶数只会影响到快指针的情况。而不会影响到慢指针走到中间节点。
- 长度为奇数,那么
post
指针的 next 就直接为null了; - 长度为偶数,
post.next.next
指针为null了;
public ListNode middleNode(ListNode head) {
ListNode pre = head, post = head;
while(post != null && post.next != null) {
pre = pre.next;
post = post.next.next;
}
return slow;
}
4.2 寻找倒数第K个元素
链接:https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/
这道题也可以用双指针法,快指针先走K步,那么当快指针走完的时候,慢指针就能够走到倒数第K个节点。
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode pre = head; ListNode post = head;
for(int i = 0; i < k; i++) {
// 题目有说明不会超过,所以可以不用判断越界
post = post.next;
}
while(post != null) {
pre = pre.next;
post = post.next;
}
return pre;
}
4.3 旋转链表
链接:https://leetcode.cn/problems/rotate-list/
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++;
}
// 如果翻转的k刚好是长度的倍数的话,说明没有进行操作
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 删除特定节点
链接:https://leetcode.cn/problems/delete-node-in-a-linked-list/
步骤总结:
- 创建一个虚拟链表头
dummyHead
,让其 next 指向 head 。 - 循环链表查找目标元素,这里是通过
cur.next.val
进行判断。 - 找到目标元素,则进行删除,运用
cur.next = cur.next.next
来删除。 - 最后返回
dummyHead.next
public ListNode removeElements(ListNode head, int val) {
// 1. 创建一个虚拟链表头 `dummyHead`
ListNode dummyHead = new ListNode(0);
// 其 next 指向 head
dummyHead.next = head;
// 临时变量进行遍历
ListNode cur = dummyHead;
while(cur.next != null) {
// 2. 找出符合条件需要删除的元素
if(cur.next.val == val) {
// 3. 进行删除
cur.next = cur.next.next;
} else {
// 直接跳过
cur = cur.next;
}
}
// 返回虚拟头节点
return dummyHead.next;
}
5.2 删除倒数第n个节点
链接:https://leetcode.cn/problems/remove-nth-node-from-end-of-list/
只遍历一次:
快慢指针进行定位 + 普通的删除
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(-1);
ListNode fast = head;
ListNode slow = dummy;
// 删除的条件变成了指定的节点位置
for(int i= 0; i < n - 1; i++) {
fast = fast.next;
}
while(fast != null && fast.next != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
}
5.3 删除链表中的重复元素
只留下不同的数字链接:https://leetcode.cn/problems/remove-duplicates-from-sorted-list/
重复的都不留下链接:https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/
留下一个重复的:
public ListNode deleteDuplicates(ListNode head) {
ListNode node = head;
while(node != null && node.next != null) {
if(node.val == node.next.val) {
node.next = node.next.next;
} else {
node = node.next;
}
}
return head;
}
不留下重复的:
这里和上面不同的只有一个是需要先把重复的元素提取出来,进行判断,因为原链表不能有这个数字。
public ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return head;
}
ListNode dummy = new ListNode(0, head);
ListNode cur = dummy;
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 dummy.next;
}