一、来源
酷壳上的这篇《LINUS:利用二级指针删除单向链表》文章里提到Linux作者Linus举的一个例子。我引用其中一部分中文翻译如下:
我见过很多人在删除一个单项链表的时候,维护了一个”prev”表项指针,然后删除当前表项,就像这样:
if (prev) prev->next = entry->next; else list_head = entry->next;
当我看到这样的代码时,我就会想“这个人不了解指针”。了解指针的人会使用链表头的地址来初始化一个“指向节点指针的指针”。当遍历链表的时候,可以不用任何条件判断:
*pp = entry->next
二、完整C代码
Linus并未提供完整代码,原博客中也引用了其他网友的代码,但是也不是完整代码,无法编译通过。
于是我参考了《二级指针实现单链表的插入、删除及 linux内核源码双向链表之奇技淫巧》这篇文章,写了一个完整版的代码,把Linus喜欢和不喜欢的版本都实现了一下,并且力图长得跟Linus的差不多(无法完全一样)。
完整C代码如下:
#include <stdio.h>
#include <stdlib.h>
typedef struct node {
int data;
struct node *next;
} Node;
/** 插入节点,采用头插法 */
int insert(Node **pp, int v) {
Node *t = (Node *) malloc(sizeof(Node));
if (NULL == t) {
return 0;
}
t->data = v;
t->next = *pp;
*pp = t;
return 1;
}
/** 输出链表所有元素 */
void print(Node **pp) {
for (Node *cur = *pp; cur;) {
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
/** 删除节点值为v的所有节点(Linus不喜欢的版本) */
void remove_that_linus_dislikes(Node **list_head, int v) {
Node *entry = *list_head;
Node *prev = NULL;
while (entry) {
Node *to_be_freed = NULL;
if (entry->data == v) {
to_be_freed = entry;
if (prev) {
prev->next = entry->next;
} else {
*list_head = entry->next;
}
} else {
prev = entry;
}
entry = entry->next;
if (to_be_freed) {
free(to_be_freed);
}
}
}
/** 删除节点值为v的所有节点(Linus喜欢的版本) */
void remove_that_linus_likes(Node **pp, int v) {
while (*pp) {
Node *entry = *pp;
if (entry->data == v) {
Node *to_be_freed = *pp;
*pp = entry->next; // 【关键代码】
free(to_be_freed);
} else {
pp = &entry->next;
}
}
}
/** 初始化列表 */
Node *init_list() {
Node *list_head = NULL;
insert(&list_head, 4);
insert(&list_head, 3);
insert(&list_head, 3);
insert(&list_head, 2);
insert(&list_head, 2);
insert(&list_head, 1);
return list_head;
}
/** 测试Linux不喜欢的版本 */
void test_remove_that_linus_dislikes() {
printf("---------------------- 分隔线 测试Linus不喜欢的版本:----------------------\n");
Node *list_head = init_list();
printf(" 原链表:");
print(&list_head);
remove_that_linus_dislikes(&list_head, 1);
printf(" 删除头(1)之后的链表:");
print(&list_head);
remove_that_linus_dislikes(&list_head, 4);
printf(" 删除尾(4)之后的链表:");
print(&list_head);
remove_that_linus_dislikes(&list_head, 2);
printf("删除中间(2)之后的链表:");
print(&list_head);
}
/** 测试Linux喜欢的版本 */
void test_remove_that_linus_likes() {
printf("---------------------- 分隔线 测试Linus喜欢的版本:----------------------\n");
Node *list_head = init_list();
printf(" 原链表:");
print(&list_head);
remove_that_linus_likes(&list_head, 1);
printf(" 删除头(1)之后的链表:");
print(&list_head);
remove_that_linus_likes(&list_head, 4);
printf(" 删除尾(4)之后的链表:");
print(&list_head);
remove_that_linus_likes(&list_head, 2);
printf("删除中间(2)之后的链表:");
print(&list_head);
}
int main() {
test_remove_that_linus_dislikes();
printf("\n");
test_remove_that_linus_likes();
return 0;
}
三、打印结果
打印结果如下:
---------------------- 分隔线 测试Linus不喜欢的版本:----------------------
原链表:1 2 2 3 3 4
删除头(1)之后的链表:2 2 3 3 4
删除尾(4)之后的链表:2 2 3 3
删除中间(2)之后的链表:3 3
---------------------- 分隔线 测试Linus喜欢的版本:----------------------
原链表:1 2 2 3 3 4
删除头(1)之后的链表:2 2 3 3 4
删除尾(4)之后的链表:2 2 3 3
删除中间(2)之后的链表:3 3
四、两个版本对比
4.1 Linus不喜欢的版本
/** 删除节点值为v的所有节点(Linus不喜欢的版本) */
void remove_that_linus_dislikes(Node **list_head, int v) {
Node *entry = *list_head;
Node *prev = NULL;
while (entry) {
Node *to_be_freed = NULL;
if (entry->data == v) {
to_be_freed = entry;
if (prev) {
prev->next = entry->next;
} else {
*list_head = entry->next;
}
} else {
prev = entry;
}
entry = entry->next;
if (to_be_freed) {
free(to_be_freed);
}
}
}
其中第9~13行力图跟Linus的描述保持一致,但是他演示的代码里面是list_head
,而我用list_head
无法实现,我用的是*list_head
。
意图是这样的:现在要删除当前节点entry
,如果我的前面有节点,那就让前面节点的next
指向我的后续节点;如果我的前面没有节点,那就让链表的头list_head
指向我的后续节点。
坦白讲,作为C语言小白,为了实现这段被人鄙视的代码,我试错了好几次才写出来,写出来了还要被人鄙视。可能这就是普通人跟神仙的区别吧。
4.2 Linus喜欢的版本
/** 删除节点值为v的所有节点(Linus喜欢的版本) */
void remove_that_linus_likes(Node **pp, int v) {
while (*pp) {
Node *entry = *pp;
if (entry->data == v) {
Node *to_be_freed = *pp;
*pp = entry->next; // 【关键代码】
free(to_be_freed);
} else {
pp = &entry->next;
}
}
}
其中第7行是关键代码,解释如下:*pp
里面存的值是当前节点entry
的内存地址,既然当前节点要删除了,那我直接改这个值就行了,改成什么呢?当前是改成下一个节点的内存地址,即entry->next
。
不得不说真的很简洁。
其实这两种实现方式我都无法一眼看懂,都要靠调试之后才能真实清楚里面在干什么,所以如果你有疑惑的话,复制完整代码然后断点调试一下吧。
五、补充:Linus回答原文
Linus回答的原文在这里,搜索关键字Linus: Hmm
可以直接定位到该回答,该回答的大意我理解如下:
- 我(指Linus本人)已经不写代码了,我的工作是看别人的代码,然后合并他们。
- 由于我要合并代码,我看得最多的不是好代码(
cool
),而是烂代码(broke
),然后骂这些人。 - Linux内核里面确实有很多好代码,我尤其引以为傲的是文件查找缓存(
filename lookup cache
)。 - 凡人不必去看文件查找缓存的实现代码(因为太复杂而又太微妙了),但是还是应该理解二级指针(
pointers-to-pointers
)之类的底层代码。 - (然后举了我们本文的例子)
- 总结:把小细节做好是很值得骄傲的。
呵呵,看到第3条的时候我已经准备去找文件查找缓存
的源码了,紧接着又意识到我其实就是第4条里面说的凡人
~
全文完