每日一题——三道链表简单题:回文,环形合并有序
三道经典链表问题
- 1. 回文链表
- 1.1 问题描述
- 1.2 解题思路
- 1.3 代码实现
- 1.4 复杂度分析
- 2. 环形链表
- 2.1 问题描述
- 2.2 解题思路
- 2.3 代码实现
- 2.4 复杂度分析
- 3. 合并两个有序链表
- 3.1 问题描述
- 3.2 解题思路
- 3.3 代码实现
- 3.4 复杂度分析
链表是数据结构中的重要组成部分,也是面试中经常考察的内容。本文将详细介绍三道经典的链表问题:回文链表、环形链表和合并两个有序链表。我们将从问题描述、解题思路、代码实现以及复杂度分析等方面进行详细讲解。
1. 回文链表
1.1 问题描述
给定一个单链表的头节点 head
,请你判断该链表是否为回文链表。如果是,返回 true
;否则,返回 false
。
示例 1:
输入:head = [1,2,2,1]
输出:true
示例 2:
输入:head = [1,2]
输出:false
提示:
- 链表中节点数目在范围
[1, 10^5]
内 0 <= Node.val <= 9
1.2 解题思路
判断回文链表的关键在于如何高效地比较链表的前半部分和后半部分。常见的方法是使用快慢指针找到链表的中点,然后反转后半部分链表,最后进行比较。这种方法的时间复杂度为 O(n),空间复杂度为 O(1)。
具体步骤如下:
- 找到链表中点:使用快慢指针,快指针每次走两步,慢指针每次走一步。当快指针到达链表末尾时,慢指针恰好位于链表中点。
- 反转后半部分链表:从链表中点开始,反转后半部分链表。
- 比较前后两部分:从链表头部和反转后的后半部分头部开始,逐节点比较。
- 恢复链表:比较完成后,将链表恢复原状(可选)。
- 返回结果:如果所有节点都匹配,则返回
true
,否则返回false
。
1.3 代码实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
// 反转链表函数
struct ListNode* reverse(struct ListNode* head) {
if (head == NULL || head->next == NULL) {
return head;
}
struct ListNode* pre = NULL;
struct ListNode* next = NULL;
while (head) {
next = head->next;
head->next = pre;
pre = head;
head = next;
}
return pre;
}
// 判断回文链表
bool isPalindrome(struct ListNode* head) {
if (head == NULL || head->next == NULL) {
return true;
}
// 使用快慢指针找到链表中点
struct ListNode* slow = head;
struct ListNode* fast = head;
while (fast->next != NULL && fast->next->next != NULL) {
slow = slow->next;
fast = fast->next->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;
}
1.4 复杂度分析
- 时间复杂度:O(n),其中 n 是链表的长度。我们需要遍历链表两次。
- 空间复杂度:O(1),仅使用了常数级别的额外空间。
2. 环形链表
2.1 问题描述
给定一个链表的头节点 head
,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。返回 true
表示存在环,否则返回 false
。
示例 1:
输入:head = [3,2,0,-4]
,pos = 1
输出:true
示例 2:
输入:head = [1,2]
,pos = 0
输出:true
示例 3:
输入:head = [1]
,pos = -1
输出:false
提示:
- 链表中节点的数目范围是
[0, 10^4]
-10^5 <= Node.val <= 10^5
2.2 解题思路
判断链表中是否存在环的经典方法是使用快慢指针。快指针每次走两步,慢指针每次走一步。如果链表中存在环,快指针最终会追上慢指针;如果链表中没有环,快指针会先到达链表末尾。
2.3 代码实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
bool hasCycle(struct ListNode* head) {
if (head == NULL || head->next == NULL) {
return false;
}
struct ListNode* slow = head;
struct ListNode* fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
return true; // 发现环
}
}
return false; // 未发现环
}
2.4 复杂度分析
- 时间复杂度:O(n),其中 n 是链表的长度。在最坏情况下,快指针会遍历整个链表。
- 空间复杂度:O(1),仅使用了常数级别的额外空间。
3. 合并两个有序链表
3.1 问题描述
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 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
均按非递减顺序排列
3.2 解题思路
合并两个有序链表的关键在于如何高效地将两个链表的节点按顺序拼接。我们可以使用一个虚拟头节点(dummy node)来简化操作,并通过迭代的方式逐节点比较和拼接。
3.3 代码实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
struct ListNode dummy; // 虚拟头节点
dummy.next = NULL;
struct ListNode* tail = &dummy; // 指向结果链表的尾部
while (list1 != NULL && list2 != NULL) {
if (list1->val < list2->val) {
tail->next = list1;
list1 = list1->next;
} else {
tail->next = list2;
list2 = list2->next;
}
tail = tail->next;
}
// 拼接剩余的链表
if (list1 != NULL) {
tail->next = list1;
} else {
tail->next = list2;
}
return dummy.next; // 返回合并后的链表头节点
}
3.4 复杂度分析
- 时间复杂度:O(n + m),其中 n 和 m 分别是两个链表的长度。我们需要逐节点比较并拼接。
- 空间复杂度:O(1),仅使用了常数级别的额外空间。