[Leetcode] 高频题汇总 —— 链表

本文深入解析链表数据结构,涵盖单向与双向链表特性,重点介绍链表反转、K个一组翻转、合并K个排序链表及环形链表等问题的高效解法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

链表是一种常见的线性数据结构。对于单向链表,每个节点有一个 n e x t next next 指针指向后一个节点,还有一个成员变量用以存储数值;对于双向链表,还有一个 p r e v prev prev 指针指向前一个节点。与数组类似,搜索链表需要 O ( n ) O(n) O(n) 的时间复杂度,但是链表不能通过常数时间读取第 k k k 个数据。链表的优势在于能够以较高的效率在任意位置插入或删除一个节点。

解题思路
  • 当涉及可能改变头结点的操作时(插入、删除、反转等),应该考虑在头结点前建立哑结点(dummy)。
  • 当需要在链表中寻找特定位置时,可以用快慢指针实现 O ( n ) O(n) O(n) 时间内完成遍历。
具体问题
结点定义
struct ListNode {
	int val;
	ListNode *next;
	ListNode(int x) : val(x), next(NULL) {}
};
反转链表 II

反转从位置 m m m n n n 的链表。请使用一趟扫描完成反转。 1 ≤ m ≤ n ≤ 1 ≤ m ≤ n ≤ 1mn 链表长度。

示例
输入:1->2->3->4->5->NULL, m = 2, n = 4
输出:1->4->3->2->5->NULL
解题思路

由于头结点可能发生改变,因此需要一个哑结点指向头结点。首先需要找到第一个开始反转结点的前一个位置。因为题目中 m , n m,n m,n 是从 1 开始计数的,所以 p r e pre pre 只需要向后移动 m − 1 m-1 m1 个位置, p r e pre pre 始终指向位置 m m m 的前一个结点。然后要开始交换位置,由于一次只能交换两个结点,示例中交换过程如下:
在这里插入图片描述

ListNode* reverseBetween(ListNode* head, int m, int n) {
	ListNode *dummy = new ListNode(-1), pre = dummy;
	dummy->next = head;
	for (int i = 0; i < m - 1; ++i) {
		pre = pre->next;
	}
	ListNode *cur = pre->next;
	for (int i = m; i < n; ++i) {
		ListNode *t = cur->next;
		cur->next = t->next;
		t->next = pre->next;
		pre->next = t;
	}
	return dummy->next;
}

类似题目 反转链表

K 个一组翻转链表

给你一个链表,每 k k k 个结点一组进行翻转,请你返回翻转后的链表。 k k k 是一个正整数,它的值小于或等于链表的长度。如果结点总数不是 k k k 的整数倍,那么请将最后剩余的结点保持原有顺序。

示例
给定这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
解题思路

类似于上一题,只不过需要把原链表分成若干段,然后分别对每段进行反转。其中,反转每段链表的过程和上一题一样,注意反转之后 reverseKGroup 函数中 c u r cur cur p r e pre pre 的位置发生了改变,cur = pre->next;不需要反转的话 cur = cur->next

/* 迭代 */
ListNode* reverseKGroup(ListNode* head, int k) {
    if (!head || k < 2) return head;
    ListNode *dummy = new ListNode(-1), *pre = dummy, *cur = head;
    dummy->next = head;
    for (int i = 1; cur; ++i) {
        if (i % k == 0) {
            pre = reverse(pre, cur->next);
            cur = pre->next;
        } else {
            cur = cur->next;
        }
    }
    return dummy->next;
}

ListNode* reverse(ListNode* start, ListNode* end) {
    ListNode* pre = start->next, *cur = pre->next;
    while (cur != end) {
        pre->next = cur->next;
        cur->next = start->next;
        start->next = cur;
        cur = pre->next;
    }
    return pre;
}

/* 递归 */
ListNode* reverseKGroup(ListNode* head, int k) {   
    ListNode* cur = head;
    for (int i = 0; i < k; ++i) {
        if (!cur) return head;
        cur = cur->next;
    }
    ListNode* newHead = reverse(head, cur);
    head->next = reverseKGroup(cur, k);
    return newHead;
}    

ListNode* reverse(ListNode* start, ListNode* end) {
    ListNode* pre = end;
    while (start != end) {
        ListNode* t = start->next;
        start->next = pre;
        pre = start;
        start = t;
    }
    return pre;
}

类似题目 两两交换链表中的节点

合并K个排序链表

合并 k k k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

示例
输入:
[
  1->4->5,
  1->3->4,
  2->6
]
输出:1->1->2->3->4->4->5->6
解题思路

利用归并排序的思想,将 k k k 个链表不断分成两份,然后递归调用分段函数,最终问题变成了合并两个有序链表。然后再写一个函数对两个链表进行合并即可。时间复杂度 O ( N l o g k ) O(Nlogk) O(Nlogk),其中 N N N 为链表总结点数;空间复杂度 O ( 1 ) O(1) O(1)。合并两个链表时,这里采用了递归的写法,迭代也可以,思想是“穿针引线”。当 l1->val > l2->val 时,说明要先从 l2 拿出一个结点排序,因此对 l1 和 l2 剩余部分(即 l2->next)进行递归,最后将递归部分连在 l2 拿出的结点后面,最后返回 l2。反之亦然。

ListNode* mergeKLists(vector<ListNode*>& lists) {
	return merge(lists, 0, (int) lists.size() - 1);
}

ListNode* merge(vector<ListNode*> &lists, int start, int end) {
	if (start > end) return NULL;
	if (start == end) return lists[start];
	int mid = start + (end - start) / 2;
	ListNode *left = merge(lists, start, mid);
	ListNode *right = merge(lists, mid + 1, end);
	return mergeTwoLists(left, right);
}

ListNode* mergeTwoLists(ListNode *l1, ListNode *l2) {
	if (!l1) return l2;
	if (!l2) return l1;
	if (l1->val > l2->val) {
		l2->next = mergeTwoLists(l1, l2->next);
		return l2;
	} else {
		l1->next = mergeTwoLists(l1->next, l2);
		return l1;
	}
}

类似题目 合并两个有序链表排序链表

删除排序链表中的重复元素 II

给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。

示例
输入:1->2->3->3->4->4->5
输出:1->2->5
解题思路

由于链表开头可能会有重复项,被删掉的话头指针会改变,而最终却还需要返回链表的头指针,所以需要定义一个新的节点,然后链上原链表。定义一个前驱指针和一个现指针,每当前驱指针指向新建的节点,现指针从下一个位置开始往下遍历,遇到相同的则继续往下,直到遇到不同项时,把前驱指针的 n e x t next next 指向下面那个不同的元素。如果现指针遍历的第一个元素就不相同,则把前驱指针向下移一位。

ListNode* deleteDuplicates(ListNode* head) {
	if (!head || !head.next) return head;
	ListNode *dummy = new ListNode(-1), *pre = dummy;
	dummy->next = head;
	while (pre->next) {
		ListNode *cur = pre->next;
		while (cur->next && cur->next->val == cur->val) {
			cur = cur->next;
		}
		if (cur != pre->next) {
			pre->next = cur->next;
		} else {
			pre = pre->next;
		}
	}
	return dummy->next;
}

类似题目 删除排序链表中的重复元素移除链表元素

环形链表 II

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

示例

在这里插入图片描述

输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。
解题思路

只需要设两个指针,一个每次走一步的慢指针和一个每次走两步的快指针,如果链表里有环的话,两个指针最终肯定会相遇。当两个指针相遇了后,让其中一个指针再次从链表头开始,两个指针一起每次走一步,等到它们再相遇的位置就是链表中环的起始位置。
在这里插入图片描述
如图,X 为表头,Y 为环的起始结点。假设两个指针第一次相遇的位置为结点 Z。此时,慢指针走过的距离为 a + b a + b a+b,快指针走过的距离为 a + b + c + b = 2 ( a + b ) a + b + c + b = 2(a + b) a+b+c+b=2(a+b),可得到 a = c a=c a=c
由图可知,环的长度 L = b + c = a + b L = b + c = a + b L=b+c=a+b,也就是说,从一开始到二者第一次相遇,循环的次数就等于环的长度。接着让两个指针分别从 X,Z 处开始每次走一步,那么它们正好会在 Y 处再次相遇。

ListNode *detectCycle(ListNode *head) {
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) break;
    }
    if (!fast || !fast->next) return NULL;
    slow = head;
    while (slow != fast) {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}

类似题目 环形链表链表的中间结点删除链表的倒数第N个节点旋转链表奇偶链表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值