目录
面试题 02.07. 链表相交 - 力扣(LeetCode)
前言
在上一篇中,已经讲解了链表中常用的一些技巧和操作,本篇我们继续通过相关算法题目来加深对链表的印象。
面试题 02.07. 链表相交 - 力扣(LeetCode)

算法思路
这道题是要判断两个链表是否相交,我们可以先让长的链表移动到和短的链表一样的长度,再进行判断两链表是否相交。
- 第一步:先判断两链表长度(假设两链表的长度差值为len),让长的链表先走len步
- 第二步:进行判断链表节点是否相等,如果相等,那么就返回true。若链表走到末尾都没有相交,则返回false。
算法代码
法一:
/**
* 获取两个链表的交点节点
* 如果两个链表有交点,则从交点开始,两个链表的节点是一样的
* 解决这个问题的关键在于,通过巧妙地交换两个链表的遍历顺序,使两个链表从尾部对齐
* 这样,如果两个链表有交点,它们会在到达交点前遍历相同长度的路径
*
* @param headA 第一个链表的头节点
* @param headB 第二个链表的头节点
* @return 返回两个链表的交点节点,如果没有交点则返回null
*/
public ListNode getIntersectionNode1(ListNode headA, ListNode headB) {
// 检查输入的链表头节点是否为null,如果是,则不可能有交点,直接返回null
if(headA==null||headB==null) return null;
// 初始化两个指针p和q,分别指向两个链表的头节点
ListNode p=headA,q=headB;
// 循环直到p和q指向同一个节点,即找到交点或者都为null(表示没有交点)
while(p!=q){
// 如果p遍历到链表末尾,则将其指向另一个链表的头节点;否则,继续遍历下一个节点
p=p==null?headB:p.next;
// 如果q遍历到链表末尾,则将其指向另一个链表的头节点;否则,继续遍历下一个节点
q=q==null?headA:q.next;
}
// 返回p(或者q,因为此时p和q要么同时为null,要么同时指向交点)
return p;
}
法二:
/**
* 获取两个链表的交点节点
* 如果链表相交,则从交点到链表的末尾是共有的节点
* 解决这个问题的关键在于找到第一个共有节点,即为交点
* 如果链表长度不同,直接比较对应位置的节点是不合理的
* 因此,使用哈希集合来存储一个链表的所有节点,然后遍历另一个链表,找到第一个在集合中出现的节点
*
* @param headA 第一个链表的头节点
* @param headB 第二个链表的头节点
* @return 返回两个链表的交点节点,如果没有交点则返回null
*/
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 检查任一链表为空时,直接返回null,因为不可能有交点
if(headA==null||headB==null) return null;
// 使用HashSet存储链表A的所有节点
Set<ListNode> set=new HashSet<>();
// 遍历链表A,将所有节点加入到集合中
while(headA!=null){
set.add(headA);
headA=headA.next;
}
// 遍历链表B,检查节点是否在集合中,即是否为链表A中出现过的节点
while(headB!=null){
// 如果在集合中找到了相同的节点,说明这是两个链表的交点
if(set.contains(headB)){
// 返回这个交点节点
return headB;
}
headB=headB.next;
}
// 如果没有找到交点,返回null
return null;
}
23. 合并 K 个升序链表 - 力扣(LeetCode)
算法思路
其实就是合并有序链表,但这里有K个链表。
如果采用暴力解法的话,就是先合并前两个链表,再和第3个、第4个、到第k个。这样的时间复杂度我们可以算一下:总共要合并k-1次,平均每个链表的长度为n个节点:
第一组n(k-1),第二组为n(k-2)+....+n=n*k*(k-1)/2,即为n*k^2。
但这种方法的效率明显低了不小,那么我们就需要对算法进行优化:
解法一:优先级队列
合并k个有序链表,我们可以利用小根堆的特点,将链表数组中的元素全部添加到小根堆中,每次堆顶的节点其实就是最小值,那么我们就将堆顶元素取出,接到虚拟节点dummy之后。
当然,在弹出堆顶节点之后,我们还需要判断弹出的节点的指针next是否为空,不为空说明还有下一个节点。那么我们就将下一个节点重新插入小根堆。直到小根堆为空,说明我们的链表已经合并完成。
算法代码
/**
* 合并K个升序链表的主方法
*
* @param lists 一个包含多个升序链表节点数组
* @return 合并后的单个升序链表的头节点
*/
public ListNode mergeKLists(ListNode[] lists) {
// 利用最小堆来存储链表节点
// 堆中元素通过比较器进行排序,确保堆顶元素是最小的
PriorityQueue<ListNode> queue = new PriorityQueue<>((a, b) -> a.val - b.val);
// 遍历所有链表,将非空节点加入优先队列
for (ListNode node : lists) {
if (node != null) {
queue.offer(node);
}
}
// 创建一个虚拟头节点,简化链表的合并操作
ListNode dummy = new ListNode(-1);
// cur用于遍历和链接节点
ListNode cur = dummy;
// 当队列不为空时,循环合并节点
while (!queue.isEmpty()) {
// 从队列中取出最小的节点
ListNode node = queue.poll();
// 将最小节点链接到cur后面
cur.next = node;
// 移动cur到下一个节点
cur = cur.next;
// 如果当前节点还有下一个节点,将下一个节点加入优先队列
if (node.next != null) {
queue.offer(node.next);
}
}
// 返回合并后的链表的头节点
return dummy.next;
}
解法二:分治归并
我们可以采用递归的方法来解决,将k个链表分解成1个个链表,再对两两链表中的节点进行排序合并,就可以得到排序后的链表。

// 法二、分支归并
/**
* 合并K个排序链表
* 使用分支归并的方法,两两合并链表,直到所有链表合并成一个
*
* @param lists 一个包含多个排序链表的数组
* @return 合并后的单一排序链表
*/
public ListNode mergeKLists1(ListNode[] lists) {
return merge(lists, 0, lists.length - 1);
}
/**
* 递归地合并排序链表数组中的链表
*
* @param lists 排序链表数组
* @param left 合并的起始索引
* @param right 合并的结束索引
* @return 合并后的链表头节点
*/
private ListNode merge(ListNode[] lists, int left, int right) {
// 当左索引大于等于右索引时,返回该索引位置的链表,作为递归的终止条件
if (left >= right) return lists[left];
// 计算中间索引,用于将数组分为两部分
int mid = left + (right - left) / 2;
// 递归合并左半部分链表
ListNode leftHead = merge(lists, left, mid);
// 递归合并右半部分链表
ListNode rightHead = merge(lists, mid + 1, right);
// 创建一个哑节点作为初始节点,便于后续操作
ListNode dummy = new ListNode(-1);
// 当前操作节点指针
ListNode cur = dummy;
// 合并左右两部分链表,每次取较小值节点
while (leftHead != null && rightHead != null) {
if (leftHead.val < rightHead.val) {
cur.next = leftHead;
leftHead = leftHead.next;
} else {
cur.next = rightHead;
rightHead = rightHead.next;
}
cur = cur.next;
}
// 如果左半部分链表还有剩余,直接连接到合并链表的末尾
while (leftHead != null) {
cur.next = leftHead;
cur = cur.next;
leftHead = leftHead.next;
}
// 如果右半部分链表还有剩余,直接连接到合并链表的末尾
while (rightHead != null) {
cur.next = rightHead;
cur = cur.next;
rightHead = rightHead.next;
}
// 返回合并后的链表头节点
return dummy.next;
}
25. K 个一组翻转链表

算法思路
这道题我们可以用模拟的思想去解决。
- 首先遍历链表节点,判断一共有多少组需要逆序;
- 利用双for循环,外循环表示要逆序的组数,内循环重复k次长度进行逆序。
此外,利用头插的方法。需要用一个pre节点来记录记录前驱节点,用tmp来记录每次逆序的第一个节点,方便我们更新pre。(每次逆序的第一个节点在逆序后是最后一个数)
算法代码
/**
* 将链表中的节点每k个进行反转
*
* @param head 链表的头节点
* @param k 每组的长度,即每k个节点进行反转
* @return 反转后的链表的头节点
*/
public ListNode reverseKGroup(ListNode head, int k) {
//先判断用多少组需要翻转
int n=0;
ListNode cur=head;
//遍历链表,统计链表长度
while (cur!=null){
n++;
cur=cur.next;
}
//计算可以反转的组数
n/=k;
//创建一个虚拟头节点,方便处理反转后的链表连接
ListNode dummy=new ListNode(-1);
cur=head;
//pre用于保存上一组的最后一个节点
ListNode pre=dummy;
//根据计算出的组数进行反转操作
for(int i=0;i<n;i++){
//每次保存上一组最后一个节点
ListNode tmp=cur;
//对当前组内的k个节点进行反转
for(int j=0;j<k;j++){
//保存当前节点的下一个节点
ListNode next=cur.next;
//将当前节点插入到上一组最后一个节点的后面
cur.next=pre.next;
pre.next=cur;
cur=next;
}
//更新pre为当前组的最后一个节点,为下一组的反转做准备
pre=tmp;
}
//将反转后的链表的尾部与剩余链表连接
pre.next=cur;
//返回反转后的链表的头节点
return dummy.next;
}
61. 旋转链表 - 力扣(LeetCode)

算法思路
根据题目,我们需要将链表的节点往后移动k个位置,后面的k个节点要按照原来的顺序放到前面。
解法一
我们可以先将链表分为两部分:
- 前n-k个节点进行逆转,再将后面的k个节点进行逆转
- 将逆转完的两部分进行合并
- 将合并完的链表再进行翻转
算法代码
public ListNode rotateRight(ListNode head, int k) {
if (head == null || head.next == null || k == 0) {
return head;
}
// 计算链表长度
int count = 1;
ListNode tail = head;
while (tail.next != null) {
tail = tail.next;
count++;
}
// 如果 k 是链表长度的倍数,直接返回原链表
k = k % count;
if (k == 0) {
return head;
}
//找到第n-k个节点
ListNode firstTail=head;
for (int i = 0; i < count - k - 1; i++) {
firstTail = firstTail.next;
}
//分割链表
ListNode secondHead=firstTail.next;
firstTail.next=null;
// 再次反转前 k 个节点
ListNode reversedFirstPart = reverse(head);
// 反转后 k 个节点
ListNode reversedSecondPart = reverse(secondHead);
//合并两部分
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
cur.next = reversedFirstPart;
while(cur.next!=null){
cur=cur.next;
}
cur.next=reversedSecondPart;
// 再次反转整个链表
return reverse(dummy.next);
}
// 反转整个链表
private ListNode reverse(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}

算法代码
解法二
先找到要新的头结点,新的头结点就是倒数第k个节点,以例1为例,我们可以先找到4的位置,将3连4的地方断开,接着1、2、3连接到5之后。
算法代码
public ListNode rotateRight(ListNode head, int k) {
if (head == null || head.next == null || k == 0) {
return head;
}
// 计算链表长度
int count = 1;
ListNode tail = head;
while (tail.next != null) {
tail = tail.next;
count++;
}
// k 可能大于链表长度,需要取模
k = k % count;
if (k == 0) {
return head; // 如果 k 是链表长度的倍数,无需旋转
}
// 找到倒数第 k+1 个节点
ListNode newTail = head;
for (int i = 0; i < count - k - 1; i++) {
newTail = newTail.next;
}
// 新的头节点是倒数第 k 个节点
ListNode newHead = newTail.next;
// 断开链表
tail.next = head; // 将尾节点连接到原头节点,形成环
newTail.next = null; // 断开环,形成新的链表
return newHead;
}
第一种解法比较容易出错,推荐第二种。

以上就是链表篇的所有内容~当然,有关链表的题目还有很多,还需要多加练习。
若有不足,欢迎指正~

1167





