1. 功能概述
bool ListDelete(LinkList &L, int i, ElemType &e)
函数的功能是:在带头结点的单链表 L
中,删除第 i
个位置的结点,并将被删除结点的数据通过引用参数 e
带回给调用者。
- 函数名:
ListDelete
,清晰表达了其功能。 - 返回值:
bool
类型。true
表示删除成功,false
表示删除失败(例如,位置i
不合法)。 - 参数:
LinkList &L
: 链表的头指针(指向头结点),使用引用传递,意味着函数内部对链表的修改会直接作用于原始链表。int i
: 要删除的结点的位序(从1开始计数)。ElemType &e
: 一个引用参数,用于“返回”被删除结点的数据值。这是一种常见的C++技巧,比使用全局变量或指针返回值更安全、更优雅。
2. 代码逐行分析
bool ListDelete(LinkList &L, int i, ElemType &e) {// 1. 边界条件检查:位序是否合法if (i < 1) return false;
- 逻辑: 链表的位序是从1开始的。如果传入的
i
小于1,它肯定是一个非法位置。 - 优点: 这是最先进行的检查,可以快速处理无效输入,提高代码的健壮性。这是一种“防御性编程”的体现。
// 2. 初始化指针和计数器LNode *p = L; // p指向头结点int j = 0; // 计数器
- 逻辑:
LNode *p = L;
: 声明一个指针p
,并让它指向链表的头结点。这是遍历链表的起点。注意:p
指向的是头结点,而不是第一个数据结点。头结点的位序可以看作是0
。int j = 0;
: 声明一个计数器j
,用来记录当前指针p
指向的是第几个结点。因为p
从头结点(位序0)开始,所以j
初始化为0
。
// 3. 循环查找第 i-1 个结点while (p != NULL && j < i-1) { p = p->next;j++;}
- 逻辑: 这是整个算法的核心步骤之一。循环的目标是让指针
p
移动到待删除结点的前驱结点上。- 为什么是
i-1
? 因为在单链表中,要删除一个结点,必须找到它的前一个结点,然后通过修改前驱结点的next
指针来“跳过”待删除结点。我们无法直接访问一个结点的前驱。 - 循环条件:
p != NULL
: 确保指针没有走到链表的末尾。如果p
已经是NULL
,说明链表长度不足i-1
,位置i
肯定不合法。j < i-1
: 只要计数器j
还没达到i-1
,就继续向后移动。
- 为什么是
- 循环结束后
p
的状态:- 成功情况:
p
指向了第i-1
个结点。 - 失败情况:
p
变成了NULL
,说明链表长度小于i-1
。
- 成功情况:
// 4. 再次检查位置i的合法性if (p == NULL || p->next == NULL) return false;
- 逻辑: 在
while
循环结束后,需要进行一次最终的合法性检查。这个检查非常关键,它处理了两种失败情况:p == NULL
: 对应while
循环因为p != NULL
条件不满足而退出的情况。说明链表太短,根本没有第i-1
个结点。p->next == NULL
: 这种情况更微妙。它说明虽然找到了第i-1
个结点(p
不为空),但这个结点是链表的最后一个结点,它后面没有第i
个结点了。因此,i
的值等于链表长度+1,同样是一个非法位置。
- 优点: 这个双重检查确保了所有非法的
i
值(太小或太大)都能被捕获。
// 5. 执行删除操作LNode *q = p->next; // q指向待删除结点e = q->data; // 保存删除结点的值p->next = q->next; // 前驱结点跳过qfree(q); // 释放q的内存
- 逻辑: 如果通过了所有检查,说明删除操作是安全的,可以执行。
LNode *q = p->next;
: 创建一个临时指针q
,让它指向待删除的结点(即第i
个结点)。这样做是为了后续能安全地释放它的内存。e = q->data;
: 将待删除结点的数据存入引用参数e
,以便调用者获取。p->next = q->next;
: 这是删除操作的核心。将前驱结点p
的next
指针,修改为指向待删除结点q
的后继结点。这样,从p
开始遍历,就会跳过q
,q
结点就从链表中“断开”了。free(q);
: 至关重要的一步。使用free()
函数释放q
所指向的内存空间。如果不释放,就会造成内存泄漏(Memory Leak),被删除的结点所占用的内存将永远无法被程序再次使用,直到程序结束。
// 6. 返回成功return true;
}
- 逻辑: 所有操作都成功完成,返回
true
表示删除成功。
3. 算法复杂度分析
- 时间复杂度: O(n)
- 最坏情况:删除链表的最后一个结点(
i = n
),需要从头结点开始遍历整个链表,执行n-1
次移动。 - 最好情况:删除第一个结点(
i = 1
),while
循环条件j < 0
不满足,直接跳过,时间复杂度为 O(1)。 - 平均情况:在任意位置删除的概率均等,平均需要移动
(n-1)/2
次。 - 综合来看,时间复杂度与链表长度
n
呈线性关系,因此为 O(n)。主要时间消耗在查找第i-1
个结点的过程。
- 最坏情况:删除链表的最后一个结点(
- 空间复杂度: O(1)
- 算法只使用了固定数量的指针变量(
p
,q
)和整型变量(j
),这些额外占用的内存空间不随链表规模n
的变化而变化。因此,空间复杂度是常数级 O(1)。
- 算法只使用了固定数量的指针变量(
4. 优点与潜在问题
优点:
- 逻辑清晰,结构标准: 完全遵循了“检查 -> 查找 -> 修改 -> 释放”的标准单链表操作范式,易于理解和教学。
- 健壮性强: 通过
if (i < 1)
和if (p == NULL || p->next == NULL)
两道防线,处理了所有可能的非法输入,保证了程序的稳定性。 - 正确性高: 代码逻辑严谨,特别是对
p->next == NULL
的检查,确保了不会对链表末尾之后的位置进行操作。 - 资源管理良好: 使用
free(q)
释放了被删除结点的内存,避免了内存泄漏,这是良好编程习惯的体现。 - 接口设计合理: 使用引用参数
&e
返回被删除的元素值,比使用指针或全局变量更符合现代C++的编程风格。
总结
这是一段高质量、教科书级别的单链表删除操作实现。它清晰地展示了算法的核心思想,并且在健壮性和资源管理方面都做得很好。对于学习数据结构的初学者来说,这是一个极佳的范例。唯一的改进点是在纯C++项目中,应将 free()
替换为 delete
以保持内存管理风格的一致性。