文章目录
24. 两两交换链表中的节点
识别
这段代码的目的是实现一个函数 swapPairs
,它接收一个链表的头节点 head
作为参数,并返回一个新的链表,其中原始链表的每对相邻节点都被交换了位置。
核心/易错
核心:
- 使用一个虚拟头节点
dummyhead
来简化对链表头的处理。 - 通过遍历链表并交换每对相邻节点来实现功能。
易错:
- 确保在交换节点时正确地更新所有相关的指针。
- 在交换过程中避免丢失对链表的跟踪。
难点/亮点
难点:
- 在不破坏链表结构的情况下交换节点。
- 确保在交换过程中不会丢失对链表的跟踪。
亮点:
- 使用虚拟头节点简化了代码逻辑,避免了对链表头节点的特殊处理。
- 通过简单的指针操作实现了节点的交换,没有使用额外的存储空间。
算法设计思路
- 虚拟头节点:创建一个虚拟头节点
dummyhead
,它的next
指向链表的头节点head
。这样做的好处是统一了对链表的处理,无论链表是否为空,代码逻辑都相同。 - 遍历链表:使用一个指针
cur
从虚拟头节点开始遍历链表。在每次迭代中,检查当前节点的下一个和下下一个节点是否存在。如果都存在,则进行交换。 - 交换节点:通过调整
next
指针来交换当前节点对的顺序。这是通过重新连接next
指针来实现的,而不是实际移动节点。 - 移动指针:在每次交换后,将
cur
指针移动到下一对节点的起始位置,继续遍历链表。
代码实现
struct ListNode* swapPairs(struct ListNode* head) {
// 定义虚拟节点
struct ListNode* dummyhead = (struct ListNode*)malloc(sizeof(struct ListNode));
dummyhead->next = head;
struct ListNode* cur = dummyhead;
while (cur->next != NULL && cur->next->next != NULL) {
struct ListNode* temp = cur->next; // 第一个节点
struct ListNode* temp1 = cur->next->next; // 第二个节点
// 交换节点
temp->next = temp1->next;
temp1->next = temp;
cur->next = temp1;
// 移动cur到下一对节点
cur = temp;
}
// 返回新的头节点
struct ListNode* newHead = dummyhead->next;
free(dummyhead); // 释放虚拟头节点
return newHead;
}
19.删除链表倒数第n个节点(快慢双指针)
识别
删除链表中倒数第N个节点的函数实现即通过双指针技术在O(1)空间复杂度和O(N)时间复杂度。一个指针(fast)先移动n+1步,然后两个指针一起移动,直到fast指针到达链表末尾。此时,slow指针将指向要删除节点的前一个节点。然后,通过修改slow指针的next指针来删除目标节点,并释放被删除节点的内存。最后,返回新的头节点。
即(用双指针找到倒数第n个节点:快指针先移动n步,然后快慢指针再同时移动直到快指针指向了空节点此时慢指针就指向了要删的这个节点。
删除操作:指向删除节点的前一个节点,即快指针先移动n+1步,然后快慢指针再同时移动直到快指针指向了空节点此时慢指针就指向了要删的这个节点的前一个节点。)
核心/易错
核心在于使用双指针技术来找到要删除节点的前一个节点。
易错点包括:
- 初始化指针时,需要确保fast和slow都指向哑节点(dummy),而不是直接使用局部变量。
- 在移动fast指针时,需要先移动n+1步,而不是n步。
- 在删除节点后,需要正确地更新指针,避免内存泄漏。
难点/亮点
难点在于如何不使用额外空间来找到倒数第N个节点。
亮点是使用哑节点简化了头节点也需要删除时的逻辑处理。
算法设计思路
- 创建一个哑节点,其next指针指向头节点,这样可以简化边界条件的处理。
- 初始化两个指针fast和slow,都指向哑节点。
- 移动fast指针n+1步,这样当fast到达链表末尾时,slow将指向倒数第N+1个节点。
- 同时移动fast和slow指针,直到fast到达链表末尾。
- 此时,slow指针的下一个节点就是要删除的节点。通过修改slow的next指针来删除该节点。
- 释放被删除节点的内存。
- 返回哑节点的next指针,即新的头节点。
代码实现
// 删除链表的倒数第N个节点
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
struct ListNode dummy; // 创建哑节点
dummy.next = head;
struct ListNode *fast = &dummy;
struct ListNode *slow = &dummy;
// 快指针先走n+1步
for (int i = 0; i <= n; ++i) {
fast = fast->next;
}
// 快慢指针一起走,直到快指针走到链表末尾
while (fast != NULL) {
fast = fast->next;
slow = slow->next;
}
// 删除慢指针指向的节点的下一个节点
struct ListNode *toDelete = slow->next;
slow->next = slow->next->next;
// 释放被删除节点的内存
free(toDelete);
// 返回新的头节点
return dummy.next;
}
142.环形链表II
识别
Floyd 的循环检测算法,也称为龟兔赛跑算法。它用于检测链表中是否存在环,并找到环的起始节点。
核心/易错
核心:
判断有环及环的入口
- 使用两个指针(快指针
fast
和慢指针slow
)来遍历链表。 - 快指针每次移动两个节点,慢指针每次移动一个节点。
- 一旦检测到快慢指针相遇,代码就会从头节点开始一个新的指针 entry,并与慢指针一起移动,直到它们相遇,这个相遇点就是环的起始节点。如果快指针或快指针的下一个节点为 NULL,则说明链表中没有环,函数返回 NULL。
易错:
- 确保在循环中正确地检查快指针和快指针的下一个节点是否为
NULL
。 - 在快慢指针相遇后,正确地从头节点开始一个新的指针,与慢指针一起移动,直到它们相遇。
难点/亮点
难点:
- 正确地处理链表的边界条件,确保算法在链表无环或只有一个节点时也能正确工作。
亮点:
- 算法的时间复杂度为 O(n),空间复杂度为 O(1),因为它只需要常数级别的额外空间。
- 通过快慢指针的相遇来确定环的存在,这是一种非常高效的检测环的方法。
算法设计思路
- 初始化指针:设置两个指针
fast
和slow
,都指向链表的头节点。 - 移动指针:在循环中,
fast
指针每次移动两步,slow
指针每次移动一步。 - 检测相遇:如果
fast
指针和slow
指针在链表中相遇,说明链表中存在环。 - 找到环的起始节点:如果检测到环,从头节点开始一个新的指针
entry
,并与slow
指针一起移动,直到它们相遇,这个相遇点就是环的起始节点。 - 处理无环情况:如果
fast
指针或其下一个节点为NULL
,则链表中没有环。
代码实现
struct ListNode* detectCycle(struct ListNode* head) {
// 初始化快指针和慢指针都指向头节点
struct ListNode* fast = head;
struct ListNode* slow = head;
// 使用while循环移动快慢指针,直到快指针或快指针的下一个节点为NULL
while (fast != NULL && fast->next != NULL) {
// 快指针移动两步
fast = fast->next->next;
// 慢指针移动一步
slow = slow->next;
// 如果快慢指针相遇,说明链表中有环
if (fast == slow) {
// 初始化一个新的指针entry指向头节点
struct ListNode* entry = head;
// 移动entry和slow指针,直到它们相遇
// 这个相遇点就是环的起始节点
while (entry != slow) {
entry = entry->next;
slow = slow->next;
}
// 返回环的起始节点
return entry;
}
}
// 如果快指针或快指针的下一个节点为NULL,说明链表中无环
return NULL;
}