链表相关面试题
- 876. 链表的中间结点
- 206. 反转链表
- 86. 分隔链表
- 160. 相交链表
- 141. 环形链表
- 问题:快慢指针为什么一定会相遇
- 142. 环形链表 II
- 问题:如何确认入口
- 237. 删除链表中的节点
- 19. 删除链表的倒数第 N 个结点
- 21. 合并两个有序链表
- 23. 合并K个升序链表(两种解法)
- 扩展:[PriorityQueue](https://blog.csdn.net/yzx3105/article/details/128442507)
- 2. 两数相加
876. 链表的中间结点
题目描述:
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例:1
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例:2
输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
代码:
static ListNode middleNode(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
}
//如果是奇数个节点,fast最后指向的是最后一个节点,如果是偶数个节点,fast最后指向的是倒数第二个节点
//因此,如果fast.next==null,证明是奇数个节点,直接返回slow
//如果fast.next.next==null,证明是偶数个节点,返回slow.next(返回第二个中间节点)
if(fast.next == null){
return slow;
}else{
return slow.next;
}
}
206. 反转链表
题目描述:
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例:1
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例:2
输入:head = [1,2]
输出:[2,1]
示例:3
输入:head = []
输出:[]
代码:
public ListNode reverseList(ListNode head) {
ListNode dummy = new ListNode(0);
while (head != null) {
ListNode next = head.next;
head.next = dummy.next;
dummy.next = head;
head = next;
}
return dummy.next;
}
86. 分隔链表
题目描述:
给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
示例:1
输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]
示例:2
输入:head = [2,1], x = 2
输出:[1,2]
代码:
/**
* 利用两个链表,将小于num的节点放到minNodeHead链表,大于等于num的节点放到maxNodeHead链表,最后,将minNodeHead拼接maxNodeHead返回
* @param head
* @param num
* @return
*/
static ListNode partition(ListNode head, int num) {
ListNode minNodeHead = new ListNode(0);
ListNode maxNodeHead = new ListNode(0);
ListNode minPre = minNodeHead;
ListNode maxPre = maxNodeHead;
while (head != null) {
if (head.val < num) {
minPre.next = head;
minPre = minPre.next;
} else {
maxPre.next = head;
maxPre = maxPre.next;
}
head = head.next;
}
maxPre.next=null;
minPre.next = maxNodeHead.next;
return minNodeHead.next;
}
160. 相交链表
题目描述:
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
图示两个链表在节点 c1 开始相交:
示例:1
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
— 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
示例:2
输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [1,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 。
代码:
static ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode a = headA;
ListNode b = headB;
while (a != b) {
a = a != null ? a.next : headB;
b = b != null ? b.next : headA;
}
return a;
}
141. 环形链表
题目描述:
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
代码:
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null) {
return false;
}
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
break;
}
}
return fast != null && fast.next != null;
}
}
问题:快慢指针为什么一定会相遇
慢指针每次移动一格,快指针每次移动两格,在有环的链表里,他们一定会相遇
1、当快指针就在慢指针后面,那么下一次慢指针移动一位,快指针移动两位,相遇
2、当快指针和慢指针差一个位置,那么下一次慢指针移动一位,快指针移动两位,他们会变成第一种情况
3、当快指针和慢指针差两个位置,那么下一次慢指针移动一位,快指针移动两位,他们会变成第二种情况
我知道你也在纠结为什么会没有快指针跳过慢指针他们没有相遇的这种情况发生,但是这种情况只会发生在他们相遇后的下一次移动
原因:其实从上面的三步不难看出:快指针是一格一格追赶慢指针的,即他们的距离是…4->3->2->1->0这样缩短的。所以一定会相遇
快慢指针为什么一定会相遇(文章参考):https://blog.csdn.net/Leslie5205912/article/details/89386769
142. 环形链表 II
题目描述:
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
**说明:**不允许修改给定的链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:no cycle
解释:链表中没有环。
代码:
public class Solution {
static ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
break;
}
}
//无环
if (fast == null || fast.next == null) {
return null;
}
//有环 这时候起点至入口的距离等于相交点至入口的距离,因此两个指针用同样的速度直到相遇就是入口
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
}
问题:如何确认入口
如图:
慢指针走了 x+y 的距离
快指针走了 x+y+n(y+z)的距离
由于快指针是慢指针的二倍速度,因此得出公式
2(x+y) = x+y+n(y+z)
x = n(y+z)-y //放出去一个x+y
x = (n-1)(y+z)+z //n至少等于1,当n=1的时候可以推导出 x=z
因此,当两个指针用同样的速度走,相遇点就是入口
具体详解视频:https://www.bilibili.com/video/BV1if4y1d7ob
237. 删除链表中的节点
题目描述:
有一个单链表的 head,我们想删除它其中的一个节点 node。
给你一个需要删除的节点 node 。你将 无法访问 第一个节点 head。
链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。
删除给定的节点。注意,删除节点并不是指从内存中删除它。这里的意思是:
给定节点的值不应该存在于链表中。
链表中的节点数应该减少 1。
node 前面的所有值顺序相同。
node 后面的所有值顺序相同。
示例:1
输入:head = [4,5,1,9], node = 5
输出:[4,1,9]
解释:指定链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9
示例:2
输入:head = [4,5,1,9], node = 1
输出:[4,5,9]
解释:指定链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9
代码:
void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
19. 删除链表的倒数第 N 个结点
题目描述:
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例:1
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例:2
输入:head = [1], n = 1
输出:[]
示例:3
输入:head = [1,2], n = 1
输出:[1]
代码:
static ListNode removeNthFromEnd(ListNode head, int n) {
//给出一个虚拟头指针,方便后期操作
ListNode dummy = new ListNode(0);
dummy.next = head;
//fast先往前走n步
ListNode fast = dummy;
for (int i = 0; i < n; i++) {
fast = fast.next;
}
//fast slow同步往前走,直到fast走到了null,这时候slow指向被删除节点的前一个节点
ListNode slow = dummy;
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
}
21. 合并两个有序链表
题目描述:
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:1
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
代码:
static ListNode merge(ListNode l1, ListNode l2) {
ListNode result = new ListNode(0);
ListNode pre = result;
while (l1 != null && l2 != null) {
if (l1.val > l2.val) {
pre.next = l2;
l2 = l2.next;
} else {
pre.next = l1;
l1 = l1.next;
}
pre = pre.next;
}
//如果两边长度不一致,剩下的则可以直接追加
pre.next = l1 == null ? l2 : l1;
return result.next;
}
23. 合并K个升序链表(两种解法)
题目描述:
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
示例:1
输入:
[
1->4->5,
1->3->4,
2->6
]
输出: 1->1->2->3->4->4->5->6
代码:(利用数组遍历找到最小值)
static ListNode merge(ListNode[] listNodeArr) {
ListNode result = new ListNode(0);
ListNode cur = result;
while (true) {
int minIndex = -1;
//找到最小索引
for (int i = 0; i < listNodeArr.length; i++) {
if (listNodeArr[i] != null) {
if (minIndex == -1) {
minIndex = i;
} else {
if (listNodeArr[i].val < listNodeArr[minIndex].val) {
minIndex = i;
}
}
}
}
//如果minIndex:-1,证明数组中没有数据了
if (minIndex == -1) {
return result.next;
}
//result拼接当前最小节点
cur.next = listNodeArr[minIndex];
//cur后移
cur = cur.next;
//listNodeArr[minIndex]后移
listNodeArr[minIndex] = listNodeArr[minIndex].next;
}
}
代码2:(利用小顶堆)
static ListNode merge2(ListNode[] listNodeArr) {
//使用小顶堆,每次取出的都是最小的节点
Queue<ListNode> minHeap = new PriorityQueue<>(Comparator.comparingInt(node -> node.val));
ListNode result = new ListNode(0);
ListNode cur = result;
//将节点放进去
for (ListNode list : listNodeArr) {
if (list != null) {
minHeap.offer(list);
}
}
while (!minHeap.isEmpty()) {
//弹出最小值
ListNode tempNode = minHeap.poll();
cur.next = tempNode;
cur = cur.next;
//往堆里放入当前节点的next
if (tempNode.next != null) {
minHeap.offer(tempNode.next);
}
}
return result.next;
}
扩展:PriorityQueue
2. 两数相加
问题描述
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
代码
static ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = new ListNode(0);//先给出一个节点,避免下面判断空,return head.next就好
ListNode pre = head;
int more = 0; //进位
int sum = 0;//两个数字相加之和
while (l1 != null || l2 != null || more != 0) {
sum = (l1 != null ? l1.val : 0) + (l2 != null ? l2.val : 0) + more;
more = sum / 10; //加出来的数字超过10,进一位
sum = sum % 10; //加出来的数字值需要个位数
//链表增加节点
ListNode newNode = new ListNode(sum);
pre.next = newNode;
//pre指向当前最新节点,方便后面增加节点
pre = newNode;
//l1 l2都指向下一个节点,继续相加
l1 = l1.next != null ? l1.next : null;
l2 = l2.next != null ? l2.next : null;
}
return head.next;
}
- 环形链表 II(详解视频):https://www.bilibili.com/video/BV1if4y1d7ob
- 相交链表(详解视频):https://www.bilibili.com/video/BV1aK411N7fW
- 删除链表的倒数第 N 个结点(详解视频): https://www.bilibili.com/video/BV1fq4y1y7z3
- 合并两个有序链表(视频讲解): https://www.bilibili.com/video/BV1vo4y1Q7Ew
- 合并K个有序链表(视频讲解):https://www.bilibili.com/video/BV1QK4y1N7ww
- 两数相加(视频讲解):https://www.bilibili.com/video/BV11C4y1t7iY
- 分隔链表(视频讲解):https://www.bilibili.com/video/BV1Gq4y1e7GH
- 链表的中间结点(视频讲解):https://www.bilibili.com/video/BV1UK411n7RN