详解删除链表的倒数第k个结点:双指针法优化与边界处理
详解删除链表的倒数第k个结点:双指针法优化与边界处理
引言
删除链表的倒数第k个结点是链表操作中的经典问题,常见于面试与算法练习中。其核心挑战在于如何高效定位倒数第k个结点并完成删除,同时妥善处理各种边界情况(如k值无效、删除头结点等)。本文将从算法原理出发,优化原始代码的缺陷,提供清晰、健壮的实现方案。
一、问题分析与核心思路
问题定义
给定一个单链表,删除链表的倒数第k个结点,要求尽可能减少遍历次数(最优时间复杂度为O(n),n为链表长度)。
核心思路:双指针法(快慢指针)
传统思路是先遍历链表获取长度n,再遍历到第n-k个结点(倒数第k个结点的前驱),但需两次遍历。
双指针法优化:
- 让快指针(
fast
)先走k步; - 慢指针(
slow
)和快指针同时前进,直到快指针到达链表末尾; - 此时慢指针指向的即为倒数第k个结点。
若想直接定位前驱结点,可让快指针先走k+1步,此时慢指针最终指向倒数第k+1个结点(即倒数第k个的前驱),简化删除操作。
二、原始代码的缺陷分析
原始代码存在以下问题:
- 语法错误:函数返回类型为
void
,却使用return NULL
(C语言中void
函数不可返回值); - 指针传递错误:删除头结点时,直接修改函数内的
plist
(一级指针),无法同步更新外部头指针(值传递特性导致); - 前驱定位复杂:通过
count
计数获取前驱pPre
,逻辑冗余; - 边界处理不完善:如
k
大于链表长度时,未明确提示或处理; - 命名不规范:
pList
、pNode
等命名模糊,可读性差。
三、优化后的实现方案
1. 链表结构定义
#include <stdio.h>
#include <stdlib.h>// 链表结点结构
typedef struct ListNode {int val; // 结点值struct ListNode* next; // 指向下一结点的指针
} ListNode;// 创建新结点
ListNode* createNode(int val) {ListNode* node = (ListNode*)malloc(sizeof(ListNode));if (node == NULL) {printf("内存分配失败!\n");exit(1);}node->val = val;node->next = NULL;return node;
}
2. 核心删除函数(双指针法)
/*** 删除链表的倒数第k个结点* @param head 链表头指针的地址(二级指针,用于修改头结点)* @param k 倒数第k个结点(k>0)* @return 成功返回1,失败返回0(如k无效、链表为空等)*/
int deleteLastKNode(ListNode** head, int k) {// 边界检查:头指针为空或k<=0(无效输入)if (head == NULL || *head == NULL || k <= 0) {printf("输入无效:链表为空或k<=0\n");return 0;}// 快指针:先走k+1步,用于定位倒数第k个结点的前驱ListNode* fast = *head;// 慢指针:最终指向倒数第k个结点的前驱ListNode* slow = *head;// 步骤1:快指针先走k步(判断k是否超过链表长度)for (int i = 0; i < k; i++) {if (fast == NULL) {// 快指针提前为空,说明k > 链表长度printf("k值超过链表长度\n");return 0;}fast = fast->next;}// 特殊情况:若快指针此时为空,说明倒数第k个是头结点(k等于链表长度)if (fast == NULL) {ListNode* toDelete = *head; // 待删除结点为头结点*head = (*head)->next; // 更新头指针free(toDelete);toDelete = NULL;return 1;}// 步骤2:快指针再走1步(总步数k+1),慢指针开始同步移动fast = fast->next;while (fast != NULL) {fast = fast->next;slow = slow->next;}// 此时slow指向倒数第k+1个结点(前驱),删除其下一个结点(倒数第k个)ListNode* toDelete = slow->next;slow->next = toDelete->next; // 跳过待删除结点free(toDelete);toDelete = NULL;return 1;
}
四、代码优化点说明
- 语法修正:去除
void
函数的return NULL
,改用返回值int
标识成功/失败; - 二级指针传递:通过
ListNode**head
接收头指针地址,确保删除头结点时外部指针能正确更新;
3.** 简化前驱定位 :快指针先走k+1步,慢指针最终直接指向倒数第k个结点的前驱,无需额外计数;
4. 完善边界处理 **:- 处理
k<=0
、链表为空的情况; - 检测
k
是否超过链表长度(快指针提前为空); - 单独处理删除头结点的场景(k等于链表长度时);
5.** 内存安全 **:删除结点后及时free
并置空,避免野指针。
- 处理
五、算法复杂度分析
-** 时间复杂度 :O(n),仅需一次遍历(快指针总步数为n,慢指针同步移动);
- 空间复杂度 **:O(1),仅使用常数个额外指针,无额外空间开销。
六、测试用例与演示
测试步骤
- 构建测试链表;
- 调用
deleteLastKNode
删除倒数第k个结点; - 打印链表验证结果。
// 打印链表
void printList(ListNode* head) {ListNode* cur = head;while (cur != NULL) {printf("%d->", cur->val);cur = cur->next;}printf("NULL\n");
}// 测试主函数
int main() {// 构建链表:1->2->3->4->5(长度5)ListNode* head = createNode(1);head->next = createNode(2);head->next->next = createNode(3);head->next->next->next = createNode(4);head->next->next->next->next = createNode(5);printf("原始链表:");printList(head); // 输出:1->2->3->4->5->NULL// 测试1:删除倒数第2个结点(4)int k = 2;if (deleteLastKNode(&head, k)) {printf("删除倒数第%d个结点后:", k);printList(head); // 输出:1->2->3->5->NULL}// 测试2:删除倒数第4个结点(2)k = 4;if (deleteLastKNode(&head, k)) {printf("删除倒数第%d个结点后:", k);printList(head); // 输出:1->3->5->NULL}// 测试3:删除倒数第3个结点(1,头结点)k = 3;if (deleteLastKNode(&head, k)) {printf("删除倒数第%d个结点后:", k);printList(head); // 输出:3->5->NULL}// 测试4:k=10(超过链表长度)k = 10;deleteLastKNode(&head, k); // 输出:k值超过链表长度return 0;
}
七、总结
删除链表的倒数第k个结点的关键是双指针法的高效定位,通过一次遍历即可完成前驱结点的查找,时间复杂度优化至O(n)。实现时需重点关注:
- 二级指针的使用(确保头结点修改生效);
- 边界情况的全面覆盖(k无效、删除头结点等);
- 内存安全(及时释放删除的结点)。