对于初学者来说,将单数链接(循环)链表作为链表的介绍,需要更多的思考和理解,而不仅仅是一个简单的 Head->Tail 列表,其中 last->next 指针只是 @987654322 @。以非循环列表为例:
Singly Linked-List (non-circular)
Node1 Node2 Node3
+------------+ +------------+ +------------+
| Payload | | Payload | | Payload |
+------------+ +------------+ +------------+
| Next |------->| Next |------->| Next |--->NULL
+------------+ +------------+ +------------+
上面,简单地链接(在将last->next 指针设置为NULL 时,只需通过next 指针将节点挂钩在一起。这使得添加到列表中变得微不足道,因为您可以简单地将新节点添加为新的第一个节点,每次更改列表地址,例如
list *newnode = malloc (sizeof *newnode); /* validate, set data values, ... */
newnode->next = list;
list = newnode;
或者您可以简单地迭代while (node->next != NULL),然后在末尾添加新节点,例如
node->next = newnode;
newnode->next = NULL;
非循环列表的优点是简单,但缺点是只能从列表的开头迭代到结尾。您不能从列表中的任何点从任何节点迭代回到它之前的节点。 (这可以对大型列表产生很大的效率差异)
为了解决这个问题,循环 列表让last->next 指针指向列表的开头。通过这一添加,您可以从 iter = current; 遍历整个列表 while (iter->next != current) 允许您从列表中的任何点迭代到列表中的任何其他点,而无需从头开始。这只会带来一点额外的复杂性。想一想:
Singly Linked-List (circular)
Node1 Node2 Node3
+------------+ +------------+ +------------+
| Payload | | Payload | | Payload |
+------------+ +------------+ +------------+
+--->| Next |------->| Next |------->| Next |--->+
| +------------+ +------------+ +------------+ |
| |
+<--------------------<---------------------<----------------------+
现在,在将节点添加到列表时,您需要确保处理一些特殊情况。例如添加第一个节点时,由于列表是循环的,所以第一个节点是自引用(或自引用,因为缺少更好的词),例如
Singly Linked-List (circular) - 1st Node is Self-Referencial
Node1
+------------+
| Payload |
+------------+
+--->| Next |--->+
| +------------+ |
| |
+<---------------------+
这不会增加很多复杂性,但需要您更深入地考虑如何将节点添加到列表中,并确保您的 last->next 指针始终指向列表的开头。这也需要在迭代列表时更加小心,因为您一直迭代直到 current->next 指针等于起点(通常是列表地址,但可以是任何节点)。
从循环列表开始你的列表学习很好,但你必须把指针放在头脑中。最好的方法是简单地拉出铅笔并画出您正在处理的节点(很像我用作图表的框),当您需要添加节点时,请确保重新连接所有指针在列表中插入新节点时正确。从列表中删除节点也是如此。用你的橡皮擦擦掉它,然后对指针的重新布线进行编码,将你的列表重新缝合在一起。
您的代码通过不使用描述性变量名称(至少混合 children 和 players 和 child,使事情变得比它需要的更难,等等...在我看来效果不佳)struct 中的每个节点都将保存一个player,而不是多个children。保持变量名的单数和复数形式对保持逻辑的正确性大有帮助。一些重命名,例如
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXNM 32 /* avoid generic defines like SIZE (maxname?) */
typedef struct _node {
char player[MAXNM];
struct _node *next;
} node;
(注意:简单地创建一个node 和一个node 也有帮助,并且不需要创建一个指向firstnode 的全局指针。相反,只需创建一个方便的typedef 即可在你的代码中使用。)
然后在main() 中,您同样可以使用一个名为player 的缓冲区来保存您从用户那里读取的输入,例如
int main (void) {
char player[MAXNM] = "";
int nplayers = 0;
node *list = NULL;
顺便说一句,您不需要多个printf 语句来输出多行文本或防止文本从页面的一侧换行。在 C 中,引用的字符串是连接在一起的。此外,虽然您的编译器可能会将更改作为自动优化进行,但如果您的字符串中没有 转换说明符,您不妨使用 puts 或 fputs 并避免调用 可变参数函数,除非它是必需的。例如,
fputs ( " Welcome To The Count Out Game!\n"
"------------------------------------\n\n"
"How Many Children Will Be Playing? : ", stdout );
(为什么在这里使用fputs 而不是puts?——这是一件好事……)
接下来,您必须验证所有用户输入并处理出现的任何错误。否则,您将误入 Undefined Behavior 处理垃圾和不确定值,直到您的程序发生非常糟糕的事情。虽然使用fgets 然后调用sscanf 来解析值(或strtol 等)会更好地为您服务,但如果您至少检查返回值,scanf 可以正确使用。这样您就可以验证至少发生了预期的转化次数,并且您的变量中有有效的输入:
if (scanf ("%d", &nplayers) != 1 || nplayers < 1) {
fputs ("error: invalid integer input.\n", stderr);
return 1;
}
但是使用scanf 的缺陷是它会将尾随的'\n'(由用户按Enter 生成)留在输入缓冲区中,您必须在使用@ 输入之前处理它987654361@ 或除"%s" 之外的其他非数字或其他转换说明符(它本身添加了由于它在第一个空格处停止读取而产生的其他问题列表 - 因此,如果空格后有其他/意外字符,你有麻烦了)
因此,您可以自行删除输入缓冲区中剩余的任何其他字符(此处为stdin)。您可以通过简单的循环和getchar()(或fgetc(),如果从另一个打开的文件流中读取)来执行此操作,例如
/* remove any additional characters from stdin */
for (int c = getchar(); c != '\n' && c != EOF; c = getchar()) {}
这将我们带到您将调用insertnode(或您的createList)的玩家姓名的输入循环,以开始将节点添加到您的列表(并显示main()的完成)
/* prompt for player and insert node in list */
while (nplayers-- &&
fputs ("name: ", stdout) != EOF &&
fgets (player, MAXNM, stdin)) {
player[strcspn (player, "\n")] = 0; /* trim '\n' from end */
if (!insertnode (&list, player))
break;
}
displaylist (list); /* output all players in list */
freelist (list); /* free list memory */
return 0;
}
请注意上面调用insertnode (&list, player) 的位置。您正在将列表的 地址 传递给您的插入函数。如果列表地址(即第一个节点)更改,则这样做,新的列表地址将在调用函数中可用。如果您未能将列表指针的地址传递,那么您将需要从函数中返回列表地址,并在每次调用函数中将其分配回。
你还需要用一个有意义的返回类型来声明你的函数,它可以指示插入操作的成功/失败。简单地返回一个指向成功时插入的节点的指针很方便,或者失败时返回NULL。
在您的插入函数中,除了验证player 不是NULL,您还需要确定列表是否存在,如果不存在,则将新节点添加为第一个节点(设置next 指针指向到自己),--或者-- 你需要迭代到列表的末尾并在那里插入新节点。每次确保next 指针指向列表地址。一个简单的实现是:
node *insertnode (node **list, char *player)
{
/* validate player not NULL, handle error */
if (!player)
return NULL;
/* allocate/validate new node */
node *newnode = malloc (sizeof *newnode);
if (!newnode) {
perror ("malloc-newnode");
return NULL;
}
/* copy player to node, initialize next NULL */
strcpy (newnode->player, player);
newnode->next = NULL;
/* check whether list exists, or is this 1st node? */
if (!*list) {
newnode->next = newnode; /* circular list is self-referencial */
*list = newnode;
}
else { /* list exist, find last node in circular list */
node *iter = *list; /* create pointer to iterate to end */
for (; iter->next != *list; iter = iter->next) {} /* iterate */
iter->next = newnode; /* insert as last node */
newnode->next = *list; /* circular, set next to *list */
}
return newnode; /* return node as convenience & success/failure */
}
对于您想为列表编写的任何其他函数,只需拔出铅笔并计算出如何遍历列表以从列表中获取所需数据。例如,您可以提供打印列表或displaylist() 函数以及释放与列表关联的内存的函数。
注意如何处理迭代的微妙之处(在freelist函数开始删除第二个节点的情况下,以确保last->next指针引用一个有效地址以指示迭代结束),例如
void displaylist (node *list)
{
node *iter = list;
/* iterate from first to last node, setting iter NULL after last */
for (; iter; iter = (iter->next != list ? iter->next : NULL))
puts (iter->player);
}
void freelist (node *list)
{
node *victim = list->next, /* free 2nd node 1st, leaving valid 1st */
*next = NULL;
while (victim != list) { /* while victim isn't list address */
next = victim->next; /* save next address before free */
free (victim); /* free victim */
victim = next; /* assign next to victim */
}
free (victim); /* free 1st node */
}
总而言之,您会得到如下内容:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXNM 32 /* avoid generic defines like SIZE (maxname?) */
typedef struct _node {
char player[MAXNM];
struct _node *next;
} node;
node *insertnode (node **list, char *player);
void displaylist (node *list);
void freelist (node *list);
int main (void) {
char player[MAXNM] = "";
int nplayers = 0;
node *list = NULL;
fputs ( " Welcome To The Count Out Game!\n"
"------------------------------------\n\n"
"How Many Children Will Be Playing? : ", stdout );
if (scanf ("%d", &nplayers) != 1 || nplayers < 1) {
fputs ("error: invalid integer input.\n", stderr);
return 1;
}
/* remove any additional characters from stdin */
for (int c = getchar(); c != '\n' && c != EOF; c = getchar()) {}
/* prompt for player and insert node in list */
while (nplayers-- &&
fputs ("name: ", stdout) != EOF &&
fgets (player, MAXNM, stdin)) {
player[strcspn (player, "\n")] = 0; /* trim '\n' from end */
if (!insertnode (&list, player))
break;
}
displaylist (list); /* output all players in list */
freelist (list); /* free list memory */
return 0;
}
node *insertnode (node **list, char *player)
{
/* validate player not NULL, handle error */
if (!player)
return NULL;
/* allocate/validate new node */
node *newnode = malloc (sizeof *newnode);
if (!newnode) {
perror ("malloc-newnode");
return NULL;
}
/* copy player to node, initialize next NULL */
strcpy (newnode->player, player);
newnode->next = NULL;
/* check whether list exists, or is this 1st node? */
if (!*list) {
newnode->next = newnode; /* circular list is self-referencial */
*list = newnode;
}
else { /* list exist, find last node in circular list */
node *iter = *list; /* create pointer to iterate to end */
for (; iter->next != *list; iter = iter->next) {} /* iterate */
iter->next = newnode; /* insert as last node */
newnode->next = *list; /* circular, set next to *list */
}
return newnode; /* return node as convenience & success/failure */
}
void displaylist (node *list)
{
node *iter = list;
/* iterate from first to last node, setting iter NULL after last */
for (; iter; iter = (iter->next != list ? iter->next : NULL))
puts (iter->player);
}
void freelist (node *list)
{
node *victim = list->next, /* free 2nd node 1st, leaving valid 1st */
*next = NULL;
while (victim != list) { /* while victim isn't list address */
next = victim->next; /* save next address before free */
free (victim); /* free victim */
victim = next; /* assign next to victim */
}
free (victim); /* free 1st node */
}
使用/输出示例
一个简短的使用例子是:
$ ./bin/ll-cir_players
Welcome To The Count Out Game!
------------------------------------
How Many Children Will Be Playing? : 5
name: Tom
name: Dick
name: Harry
name: Gus
name: Sarah
Tom
Dick
Harry
Gus
Sarah
内存使用/错误检查
在您编写的任何动态分配内存的代码中,对于分配的任何内存块,您都有 2 个职责:(1)始终保留指向起始地址的指针内存块,因此,(2) 当不再需要它时可以释放。
您必须使用内存错误检查程序来确保您不会尝试访问内存或写入超出/超出分配块的边界,尝试读取或基于未初始化的值进行条件跳转,最后,以确认您释放了已分配的所有内存。
对于 Linux,valgrind 是正常的选择。每个平台都有类似的内存检查器。它们都易于使用,只需通过它运行您的程序即可。
$ valgrind ./bin/ll-cir_players
==29803== Memcheck, a memory error detector
==29803== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==29803== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==29803== Command: ./bin/ll-cir_players
==29803==
Welcome To The Count Out Game!
------------------------------------
How Many Children Will Be Playing? : 5
name: Tom
name: Dick
name: Harry
name: Gus
name: Sarah
Tom
Dick
Harry
Gus
Sarah
==29803==
==29803== HEAP SUMMARY:
==29803== in use at exit: 0 bytes in 0 blocks
==29803== total heap usage: 5 allocs, 5 frees, 200 bytes allocated
==29803==
==29803== All heap blocks were freed -- no leaks are possible
==29803==
==29803== For counts of detected and suppressed errors, rerun with: -v
==29803== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
始终确认您已释放已分配的所有内存并且没有内存错误。
现在这比预期的要长得多,但很明显,您在理解和接近单链循环链表方面完全迷失了方向。希望这会给您一个基本的了解,并让您从这里继续前进。