对于初学者来说,将单向(循环)链表作为对链表的介绍,比last->next指针简单的简单的 Head->Tail 链表需要更多的思考和理解NULL。以非循环列表为例:
Singly Linked-List (non-circular)
Node1 Node2 Node3
+------------+ +------------+ +------------+
| Payload | | Payload | | Payload |
+------------+ +------------+ +------------+
| Next |------->| Next |------->| Next |--->NULL
+------------+ +------------+ +------------+
上面,简单地链接next(通过指针将节点挂钩在一起是在将last->next指针设置为时所需的全部内容NULL。这使得添加到列表中变得微不足道,因为您可以简单地将新节点添加为新的第一个节点,每次更改列表地址,例如
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和player和child等......在我看来效果不佳)您中的每个节点都struct将持有一个 single player,而不是 multiple 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;
(注意:简单地制作 a也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? - 这是一件好事去弄清楚......)
接下来,您必须验证所有用户输入并处理出现的任何错误。否则,您将误入处理垃圾和不确定值的未定义行为,直到您的程序发生非常糟糕的事情。虽然您最好使用fgets然后调用sscanf来解析值(或strtol等),scanf但如果您至少检查 return ,则可以正确使用。这样,您可以验证至少发生了预期的转换次数,并且您的变量中有有效的输入:
if (scanf ("%d", &nplayers) != 1 || nplayers < 1) {
fputs ("error: invalid integer input.\n", stderr);
return 1;
}
但是使用的陷阱scanf是它会将尾随'\n'(由用户按下生成)留在输入缓冲区中,您必须在使用其他非数字或其他转换说明符Enter进行输入之前处理它(它本身添加了一个完整的其他由于它在第一个空格处停止读取而产生的问题列表 - 所以如果空格后面有其他/意外字符,你就有麻烦了)fgets"%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失败时很好。
在您的插入函数中,除了验证playeris not之外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
内存使用/错误检查
在您编写的任何动态分配内存的代码中,对于分配的任何内存块,您有两个责任:(1)始终保留指向内存块起始地址的指针,(2)它可以在没有时被释放更需要。
您必须使用内存错误检查程序来确保您不会尝试访问内存或写入超出/超出分配块的范围,尝试读取或基于未初始化值的条件跳转,最后确认释放所有分配的内存。
对于 Linuxvalgrind是正常的选择。每个平台都有类似的内存检查器。它们都易于使用,只需通过它运行您的程序即可。
$ 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)
始终确认您已释放所有已分配的内存并且没有内存错误。
现在,这比预期的要长得多,但很明显,您在理解和处理单链循环链表时完全迷失了方向。希望这会给你一个基本的了解,让你从这里开始。