目录
1.合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
方法一:
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode *dummy = new ListNode(0);
ListNode *current = dummy;
while (list1 != nullptr && list2 != nullptr) {
if (list1->val <= list2->val) {
current->next = list1;
list1 = list1->next;
} else {
current->next = list2;
list2 = list2->next;
}
current = current->next;
}
if (list1 != nullptr) {
current->next = list1;
} else {
current->next = list2;
}
return dummy->next;
}
};
条件:list1 != nullptr && list2 != nullptr
只要两个链表都不为空,就可以比较它们的当前节点值。
分支逻辑:
如果 list1->val 小于等于 list2->val:
将 list1 的当前节点连接到合并链表的尾部,即 current->next = list1。
移动 list1 指针到下一个节点,即 list1 = list1->next。
否则:
将 list2 的当前节点连接到合并链表的尾部,即 current->next = list2。
移动 list2 指针到下一个节点,即 list2 = list2->next。
指针更新:无论进入哪个分支,都需要将 current 移动到当前链表的末尾:current = current->next。
因为两个链表可能长度不同,其中一个会先到达末尾 (nullptr)。
直接将剩余的链表连接到合并链表的末尾,完成操作。
方法二:递归
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
// 如果其中一个链表为空,直接返回另一个链表
if (list1 == nullptr) return list2;
if (list2 == nullptr) return list1;
// 比较当前节点的值,选择较小的节点作为当前结果
if (list1->val <= list2->val) {
list1->next = mergeTwoLists(list1->next, list2);
return list1;
} else {
list2->next = mergeTwoLists(list1, list2->next);
return list2;
}
}
};
递归逻辑
比较 list1 和 list2 当前节点的值:
如果 list1->val <= list2->val,说明 list1 的当前节点应出现在结果链表中:
递归调用 mergeTwoLists 合并剩余的 list1->next 和 list2。
将 list1 的当前节点与递归返回的结果连接。
否则,list2 的当前节点应出现在结果链表中:
递归调用 mergeTwoLists 合并 list1 和 list2->next。
将 list2 的当前节点与递归返回的结果连接。
返回结果每次递归都返回较小节点的指针,从而逐步构建合并后的链表。
方法三:原地修改迭代
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
if (list1 == nullptr) return list2;
if (list2 == nullptr) return list1;
ListNode* head = (list1->val <= list2->val) ? list1 : list2;
if (head == list1) list1 = list1->next;
else list2 = list2->next;
ListNode* current = head;
while (list1 != nullptr && list2 != nullptr) {
if (list1->val <= list2->val) {
current->next = list1;
list1 = list1->next;
} else {
current->next = list2;
list2 = list2->next;
}
current = current->next;
}
current->next = (list1 != nullptr) ? list1 : list2;
return head;
}
};
比较 list1 和 list2 当前节点的值:
如果 list1->val <= list2->val,将 list1 当前节点接到结果链表,并移动 list1。
否则,将 list2 当前节点接到结果链表,并移动 list2。
同时移动结果链表指针 current,保持末尾指向最新节点。
循环结束时,至少有一个链表到达末尾 (nullptr)。
直接将剩余的链表连接到结果链表末尾。
2.移动零
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
方法一:这是一个经典的双指针问题,要求在原地修改数组。我们可以使用两个指针,分别表示当前遍历的元素位置和下一个非零元素应该放置的位置。
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int nonZeroIndex = 0;
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] != 0) {
nums[nonZeroIndex++] = nums[i];
}
}
for (int i = nonZeroIndex; i < nums.size(); ++i) {
nums[i] = 0;
}
}
};
方法二:通过一个指针 lastNonZeroFoundAt
记录下一个非零元素应该交换到的位置。
遍历数组:如果当前元素是非零元素,与 lastNonZeroFoundAt
位置的元素交换。然后将 lastNonZeroFoundAt
向前移动一位。
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int lastNonZeroFoundAt = 0; // 记录下一个非零元素应该交换到的位置
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] != 0) {
// 交换当前非零元素与 lastNonZeroFoundAt 位置的元素
std::swap(nums[lastNonZeroFoundAt], nums[i]);
lastNonZeroFoundAt++;
}
}
}
};
方法三:可以尝试通过双指针交换法来解决这个问题。这种方法只需遍历一次数组,通过交换操作将非零元素移到前面,零元素移到后面。
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int j = 0; // 指向下一个非零元素应该交换的位置
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] != 0) {
if (i != j) {
std::swap(nums[i], nums[j]);
}
++j; // 非零位置前移
}
}
}
};
3.相交链表
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
图示两个链表在节点 c1
开始相交**:**
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
listA - 第一个链表
listB - 第二个链表
skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数
评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA
和 headB
传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。
输入: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 中第四个节点) 在内存中指向相同的位置。。
输入: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 个节点。
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:No intersection
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
解法如下:
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (!headA || !headB) return nullptr;
ListNode *pA = headA;
ListNode *pB = headB;
while (pA != pB) {
pA = (pA == nullptr) ? headB : pA->next;
pB = (pB == nullptr) ? headA : pB->next;
}
return pA;
}
};
算法步骤:
初始化两个指针 pA 和 pB,分别指向链表 headA 和 headB。
遍历链表:
如果 pA 到达链表末尾,则将其指向链表 headB 的头节点。
如果 pB 到达链表末尾,则将其指向链表 headA 的头节点。
如果 pA 和 pB 在某一节点相遇,则该节点即为交点。
如果两者都遍历完链表且没有交点,则两者最终会同时为 null。
返回相交节点或 null。
4.反转链表
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
方法一:迭代法
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr; // 前一个节点
ListNode* curr = head; // 当前节点
while (curr != nullptr) {
ListNode* next = curr->next; // 保存下一个节点
curr->next = prev; // 反转当前节点的指针
prev = curr; // 移动 prev 到当前节点
curr = next; // 移动 curr 到下一个节点
}
return prev; // 返回新的头节点
}
};
算法步骤:
初始化两个指针:
prev 指向 nullptr,表示当前节点的前驱。
curr 指向链表头节点 head。
遍历链表:
保存当前节点的下一个节点 next。
将当前节点的 next 指向 prev,完成反转。
移动 prev 和 curr 指针到下一个节点。
当 curr 为 nullptr 时,链表已反转,prev 即为新链表的头节点。
返回 prev。
方法二:递归法
cpp
class Solution {
public:
ListNode* reverseList(ListNode* head) {
// 递归终止条件
if (head == nullptr || head->next == nullptr) {
return head;
}
// 反转剩余链表
ListNode* reversedHead = reverseList(head->next);
// 调整当前节点的指针
head->next->next = head;
head->next = nullptr;
return reversedHead; // 返回新的头节点
}
};
算法步骤:
递归终止条件:当前节点为 nullptr 或下一个节点为 nullptr(即到达链表尾部)。
递归反转剩余链表:
让新链表的头节点 reversedHead 指向 head->next 反转后的链表头。
调整指针:
让 head->next->next 指向 head,将当前节点追加到反转链表尾部。
设置 head->next = nullptr,断开当前节点和其余节点的连接。
返回 reversedHead。
5.回文链表
给你一个单链表的头节点 head
,请你判断该链表是否为回文链表。如果是,返回 true
;否则,返回 false
。
回文序列;回文 序列是向前和向后读都相同的序列。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
bool isPalindrome(ListNode* head) {
if (!head || !head->next) return true; // 如果链表为空或只有一个节点,则是回文
// Step 1: 找到链表的中点
ListNode* slow = head;
ListNode* fast = head;
// 使用快慢指针找到链表的中点
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
// Step 2: 反转链表的后半部分
ListNode* prev = nullptr;
ListNode* curr = slow;
while (curr) {
ListNode* nextTemp = curr->next;
curr->next = prev;
prev = curr;
curr = nextTemp;
}
// Step 3: 比较前半部分和反转后的后半部分
ListNode* left = head;
ListNode* right = prev; // prev是反转后的后半部分的头
while (right) {
if (left->val != right->val) {
return false; // 如果不相等,说明不是回文链表
}
left = left->next;
right = right->next;
}
return true; // 如果所有值都相等,返回 true
}
};
快慢指针:fast 指针每次走两步,slow 指针每次走一步。当 fast 指针走到链表末尾时,slow 指针正好在链表的中间位置。
反转后半部分链表:从 slow 指针开始,我们通过迭代将链表的后半部分反转,使其指向前一节点。
比较前后两部分:通过遍历链表的前半部分和反转后的后半部分,逐个比较它们的值,如果有不相等的地方,说明链表不是回文的。
空间复杂度:这段代码的空间复杂度是 O(1),因为我们只使用了常量级别的额外空间(除了输入链表本身)。
时间复杂度:时间复杂度是 O(n),其中 n 是链表的节点数。我们遍历链表两次:一次是找到中点并反转后半部分,另一次是比较两个部分。边界情况:
空链表或者只有一个节点的链表是回文的,直接返回 true。
链表的长度为 2 的时候,只需要比较两个元素是否相等。