1.合并两个有序链表
思路分析(双指针法):可以使用双指针的方式,逐步将两个链表的节点合并到一个新的链表中。
- 初始化一个新的链表头节点和一个指向新链表末尾的指针current;
- 使用两个指针分别指向两个链表的头节点;
- 比较l1和l2的节点值,将较小值的节点连接到current的后面,然后移动current和选中的节点指针;
- 当其中一个链表遍历完后,将另一个链表的剩余部分直接连接到新链表后面;
- 返回dummy->next作为合并后的新链表的头节点。
具体实现代码(详解版):
/**
* 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(0);//创建一个哨兵节点,简化边界情况
ListNode* current = &dummy;//指针
while(list1 && list2){
if(list1->val < list2->val){
current->next = list1;
list1 = list1->next;
}else{
current->next = list2;
list2 = list2->next;
}
current = current->next;
}
//将剩余的链表加到新链表末尾
if(list1) current->next = list1;
else current->next = list2;
return dummy.next;//实际头节点
}
};
此算法的时间复杂度为 O ( m + n ) O(m+n) O(m+n),其中m和n分别是l1和l2的长度,空间复杂度 O ( 1 ) . O(1). O(1).,因为外面只使用了常量级别的额外空间。
2.两数相加
思路分析:要将两个链表表示的数字相加,可以采用逐位相加的方式来实现。链表中的数字是逆序存储的,所以我们从头节点开始,一位一位地相加,并处理进位。
- 逐位相加:
- 从两个链表的头节点开始,分别取出每个链表对应的节点值相加;
- 如果链表较短,则该链表的当前节点视为0(表示没有这一位)
- 处理进位
- 如果两个节点的值相加(加上前一位的进位)大于等于10,则取个位数作为该位的值,进位保留给下一位;
- 生成新链表
- 将每位计算出的值生成新链表的节点并连接到结果链表上
- 处理剩余进位
- 最后如果还有进位,则将进位作为新节点追加到结果链表末尾。
具体实现代码(详解版):
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode dummy(0);//哨兵节点
ListNode* current = &dummy;//指向当前处理节点的指针,从哨兵节点开始
int carry = 0;//进位初始为0
//遍历两个链表,直到两个链表都遍历完
while(l1 != nullptr || l2 != nullptr){
//取出当前节点的值,如果链表已遍历完则视为0
int x = (l1 != nullptr) ? l1->val : 0;
int y = (l2 != nullptr) ? l2->val : 0;
int sum = x + y + carry;//求和,要加上进位哦
carry = sum / 10;//更新进位 若当前位的和大于等于10则进位1,否则为0
// 创建一个新节点存储当前位的结果,取sum的个位数
current->next = new ListNode(sum % 10);
current = current->next;// 将current指针移动到新创建的节点
// 将l1和l2分别指向下一个节点(若有下一个节点)
if(l1 != nullptr) l1 = l1->next;
if(l2 != nullptr) l2 = l2->next;
}
//如果还有进位,则添加一个新节点
if(carry > 0) current->next = new ListNode(carry);
return dummy.next;//返回结果链表的头节点
}
};
- 时间复杂度: O ( m a x ( m , n ) ) O(max(m,n)) O(max(m,n)),其中m和n分别是l1和l2的长度,因为需要遍历两个链表的每个节点
- 空间复杂度: O ( m a x ( m , n ) ) O(max(m,n)) O(max(m,n)),用于存储结果链表的节点数。
3.删除链表的第n个节点
在对链表进行操作时,一种常用的技巧是添加一个哑节点(dummy node),它的 next 指针指向链表的头节点。这样一来,我们就不需要对头节点进行特殊的判断了。
例如,在本题中,如果我们要删除节点 y,我们需要知道节点 y 的前驱节点 x,并将 x 的指针指向 y 的后继节点。但由于头节点不存在前驱节点,因此我们需要在删除头节点时进行特殊判断。但如果我们添加了哑节点,那么头节点的前驱节点就是哑节点本身,此时我们就只需要考虑通用的情况即可。
特别地,在某些语言中,由于需要自行对内存进行管理。因此在实际的面试中,对于「是否需要释放被删除节点对应的空间」这一问题,我们需要和面试官进行积极的沟通以达成一致。下面的代码中默认不释放空间。
思路分析1(计算链表长度):
- 先从头节点开始对链表进行一次遍历,得到链表的长度 L
- 随后我们再从头节点开始对链表进行一次遍历,当遍历到第 L−n+1 个节点时,它就是我们需要删除的节点。
- 为了方便删除操作,我们可以从哑节点开始遍历 L−n+1 个节点。当遍历到第 L−n+1 个节点时,它的下一个节点就是我们需要删除的节点,这样我们只需要修改一次指针,就能完成删除操作。
具体实现代码(详解版):
class Solution {
public:
int getLength(ListNode* head){
int len = 0;
while(head != nullptr){
len ++;
head = head->next;
}
return len;
}
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0,head);//创建哨兵节点
int len = getLength(head);//获取链表的长度
ListNode* cur = dummy;//当前指针从哨兵节点开始
//移动指针到倒数第n+1个节点
for(int i = 1 ; i < len - n + 1 ; i ++){
cur = cur->next;
}
//删除倒数第n个节点
cur->next = cur->next->next;
//返回删除节点后的链表头
ListNode* res = dummy->next;
delete dummy;//释放哨兵节点
return res;//返回链表的新头节点
}
};
- 时间复杂度:O(L),其中L是链表的长度
- 空间复杂度:O(1).
思路分析2(栈):我们也可以在遍历链表的同时将所有节点依次入栈。根据栈「先进后出」的原则,我们弹出栈的第 n 个节点就是需要删除的节点,并且目前栈顶的节点就是待删除节点的前驱节点。这样一来,删除操作就变得十分方便了。
- 创建哨兵节点dummy,值为0,其next指针指向链表的头节点head;
- 创建一个栈stk来存储链表节点的指针,当前指针指向哨兵节点;
- 遍历链表,将每个节点的指针压入栈中;
- 找出倒数第n个节点:弹出栈中的n个节点,这样栈顶指针将指向倒数第n+1个节点;
- 删除目标节点:只需要让prev指向要删除节点的下一个节点即可。prev->next = prev->next->next;
- 然后返回链表头,且释放dummy节点的内存
具体实现代码(详解版):
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head); // 创建哨兵节点
stack<ListNode*> stk; // 创建一个栈来存储链表节点
ListNode* cur = dummy; // 当前指针指向哨兵节点
// 遍历链表,将每个节点压入栈中
while (cur) {
stk.push(cur);
cur = cur->next;
}
// 弹出栈中 n 个节点,找到倒数第 n + 1 个节点
for (int i = 0; i < n; i++) stk.pop();
// 找到倒数第 n + 1 个节点
ListNode* prev = stk.top(); // prev 指向要删除节点的前一个节点
prev->next = prev->next->next; // 删除倒数第 n 个节点
// 返回删除节点后的链表头
ListNode* res = dummy->next;
delete dummy; // 释放哨兵节点
return res; // 返回链表的新头节点
}
- 时间复杂度:O(L),其中L是链表的长度
- 空间复杂度:O(L),其中L是链表的长度,主要为栈的开销。
思路分析3(双指针法)
- 哨兵节点:dummy初始值为0,指向head
- 快慢指针
- first指针先走n+1步,使得first和second指针相距n步
- 然后同时移动first和second指针,当first到达链表尾部时,second位于待删除节点的前一个节点
- 删除节点:second->next = second->next->next; 删除倒数第 n 个节点。
- 返回结果:返回dummy->next,即新的链表头.
具体实现代码(详解版):
/**
* 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* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0,head);
//定义两个指针,初始都指向哨兵节点
ListNode* first = dummy;
ListNode* second = dummy;
//让first指针先移动n+1步,保证两个指针相距n步
for(int i = 0 ; i <= n ; i ++) first = first->next;
//同时移动first和second指针,直到first到达链表末尾
while(first != nullptr){
first = first->next;
second = second->next;
}
//此时second指向倒数第n+1个节点,删除其下一个节点
second->next = second->next->next;
//返回删除节点后的链表头
ListNode* res = dummy->next;
delete dummy;
return res;
}
};
- 时间复杂度:O(L),其中L是链表的长度
- 空间复杂度:O(1).
4.两两交换链表中的节点
思路分析1(递归):
- 基本情况
- 确定新头节点
- 创建一个指针 newHead,指向当前头节点的下一个节点(即 head->next)。这个节点将成为新的头节点。
- 递归调用
- 递归调用 swapPairs 函数来处理剩下的节点(即从 newHead->next 开始的子链表)。这个调用将返回处理后的链表
- 交换节点
- 更新 head->next 指向递归返回的结果(即 newHead 之后的节点),实现对节点的连接
- 然后将 newHead->next 指向 head,完成两个节点的交换
- 返回新头节点
- 最后返回newhead,作为当前链表的新的头节点。
具体实现代码(详解版):
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
// 基础情况:如果链表为空或只有一个节点,直接返回头节点
if (head == nullptr || head->next == nullptr) {
return head;
}
// newHead 是将要交换的第二个节点
ListNode* newHead = head->next;
// 递归交换 newHead 之后的节点对
head->next = swapPairs(newHead->next);
// 交换头节点和 newHead
newHead->next = head;
// 返回新的头节点
return newHead;
}
};
-
时间复杂度:O(n),其中 n 是链表的节点数量。需要对每个节点进行更新指针的操作。
-
空间复杂度:O(n),其中 n 是链表的节点数量。空间复杂度主要取决于递归调用的栈空间。
思路分析2(迭代)
- 虚拟头节点:创建一个虚拟头节点 dummy,使其 next 指向原链表的头节点 head。这有助于处理链表的头节点,避免特殊情况的判断。
- 指针初始化
- prev 初始化为虚拟头节点,用于跟踪已经处理的节点。
- first 和 second 用于指向当前需要交换的两个节点。
- 循环迭代
- 使用 while 循环,检查 prev->next 和 prev->next->next 是否为 nullptr,以确保存在两个可交换的节点
- 在循环中获取当前的 first 和 second 节点。
- 交换节点
- 将first的next指向second的下一个节点,这样first就不再指向second
- second的next指向first,完成了first和second的交换
- 更新prev->next = seond,指向新的头节点
- 更新指针:将prev更新为first,以便处理下一个节点对的交换
- 返回结果
- 返回虚拟头节点的 next,即新的链表头。
- 最后释放虚拟头节点的内存。
具体实现代码(详解版):
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
// 创建一个虚拟头节点,简化边界条件的处理
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* prev = dummy; // 前一个节点
ListNode* first; // 第一个节点
ListNode* second; // 第二个节点
// 迭代链表,直到没有更多的节点可以交换
while (prev->next != nullptr && prev->next->next != nullptr) {
first = prev->next; // 第一个节点
second = first->next; // 第二个节点
// 交换节点
first->next = second->next; // 将第一个节点指向第三个节点
second->next = first; // 将第二个节点指向第一个节点
prev->next = second; // 将前一个节点指向新的头节点(第二个节点)
// 更新指针
prev = first; // 将前一个节点移动到交换后的第一个节点
}
ListNode* result = dummy->next; // 获取新链表的头节点
delete dummy; // 释放虚拟节点的内存
return result; // 返回新链表的头节点
}
};
- 时间复杂度:O(n),其中n是链表中节点的数量,每个节点被访问了一次;
- 空间复杂度:O(1),没有使用额外的空间来存储节点,只使用了常数级别的额外变量