链表 - 101 - 单链表
这是一个很长的答案。我之所以详细介绍,是因为我希望在适当的上下文中就地回答大量的链表问题。
当我学习 C 时,我很难使用指针。不过,在实现了链表之后,我终于开始掌握指针的概念了。主链表在 C 语言中是一件好事,它会帮助您熟悉指针。当事情看起来很混乱时,拿起铅笔和纸,画出一个列表图和节点的相关链接。当我处理复杂的列表实现时,我偶尔会这样做。
链表是一种存储数据记录的方式。与所有元素占用一个连续内存块的数组不同,链表元素占用内存的随机片段。
链表有两种基本类型;单链表和双链表。不同的是,单链表只能单向遍历;而双向链表可以双向遍历。
通过它的“头”指针或指向头列表节点的指针来访问单链表。双向链表也可以通过它的“头”指针或通过它的“尾”指针来访问。
与数组的每个元素都可以通过其数组索引直接寻址的数组不同,链表元素是按顺序访问的。
这是一个单链表的布局:
Node #1 Node #2 Node #3 EndOfList
---------- ---------- -------- ---------
HEADPTR--> NEXTPTR--> NEXTPTR--> NEXTPTR--> NULL
DataPayload DataPayload DataPayload
列表中的每个节点及其数据负载都是单独分配的。一个节点结构(在 C 中)可能看起来像这样:
typedef struct NODE_PAYLOAD_S
{
/* Data Payload (defined by coder) */
char name[10];
char desc[10];
int hours;
int workordernum;
} NODE_PAYLOAD_T;
typedef struct LIST_NODE_S
{
/* Next-node pointer */
struct LIST_NODE_S *next; /* pointer to the next node in the list. */
NODE_PAYLOAD_T payload; /* Data Payload (defined by coder) */
} LIST_NODE_T;
初始化上述结构的单链表:
LIST_NODE_T *listHead = NULL;
'listHead' 现在是一个指向链表的指针(没有节点)。
这里是如何在这个列表的头部添加一个新节点:
int LIST_InsertHeadNode(
LIST_NODE_T **IO_head,
问:为什么这里需要一个“双指针”(即:LIST_NODE_T **...)?为什么不使用“单级”指针(即:LIST_NODE_T *...)?
答:指向列表头的“单个”指针不足以执行此操作。具体来说,这个操作指定了一个新的“头节点”。这意味着this函数需要修改指向头节点的指针。
之前:
Node #Y Node #Z EndOfList
---------- ---------- ---------
HEADPTR--> NEXTPTR--> NEXTPTR--> NULL
DataPayload DataPayload
之后:
New Node Node #Y Node #Z EndOfList
---------- ---------- -------- ---------
HEADPTR--> NEXTPTR--> NEXTPTR--> NEXTPTR--> NULL
DataPayload DataPayload DataPayload
请注意,之前 HEADPTR 指向 'Node #Y';然后,HEADPTR 指向“新节点”。调用此函数时,会传入 listHead 指针的地址,从而允许此函数更改 listHead 指针指向的位置。换句话说,listHead 指针的地址被传递给这个函数,它(在这个函数内部)被表示为一个指向 listHead 指针的指针(一个指向指针的指针)。这就是为什么它是一个“双指针”。
char *I__name,
char *I__desc,
int I__hours,
int I__workordernum
)
{
int rCode=0;
LIST_NODE_T *newNode = NULL;
/* Allocate memory for new node (with its payload). */
newNode=malloc(sizeof(*newNode));
if(NULL == newNode)
{
rCode=ENOMEM; /* ENOMEM is defined in errno.h */
fprintf(stderr, "malloc() failed.\n");
goto CLEANUP;
问:“goto CLEANUP;”是什么意思?生意?
答:与 C++ 和 JAVA 不同,C 语言没有“异常”的概念。 C 中的错误检查至关重要。 malloc() 函数可能失败的原因有很多,如果失败,代码必须尽可能优雅地处理它。 “goto CLEANUP”语句使正常程序流跳过代码跳转到“CLEANUP:”标签(在此函数内,如下)。
显然,如果 malloc() 失败,尝试使用紧随其后的行初始化 NULL 指针(由失败的 malloc 返回)是不明智的。因此,转移程序流程以跳过此初始化(以及稍后出现的链接)非常重要。
“CLEANUP:”标签没有什么特别之处。我可以称它为“错误:”、“退出:”、“完成:”、“利亚霍纳:”、“我的坏”或其他任何适合我的名字。 (标签不必大写,也不必放在左边距。但是,我的风格是这样做,以便它们脱颖而出。)
标签(例如“CLEANUP:”)的范围仅限于放置它们的函数边界;这允许每个函数都有一个唯一的“CLEANUP:”标签(如果需要)。
}
/* Initialize the new node's payload. */
snprintf(newNode->payload.name, sizeof(newNode->payload.name), "%s", I__name);
snprintf(newNode->payload.desc, sizeof(newNode->payload.desc), "%s", I__desc);
newNode->payload.hours = I__hours;
newNode->payload.workordernum = I__workordernum;
/* Link this node into the list as the new head node. */
newNode->next = *IO_head;
*IO_head = newNode;
CLEANUP:
return(rCode);
}
上述函数可能调用如下:
#include <stdio.h>
#include <errno.h>
int LIST_InsertHeadNode(LIST_NODE_T **, char *, char *, int, int);
int main(void)
{
int rCode=0;
LIST_NODE_T *listHead = NULL;
rCode=LIST_InsertHeadNode(&listHead, "Mahonri", "Jareds Bro", 4, 2421);
if(rCode)
{
fprintf(stderr, "LIST_InsertHeadNode() reports: %d\n", rCode);
goto CLEANUP;
}
CLEANUP:
return(rCode);
}
可以多次调用 LIST_InsertHeadNode() 函数。每次调用都会将一个新节点添加到列表中。新节点将被放置在列表的“头部”,这具有将其余节点推到列表下方的效果。
将几个节点添加到列表后,访问列表可能会很好;也许打印每个节点的有效负载:
int PrintListPayloads(
LIST_NODE_T *head;
)
{
int rCode=0;
LIST_NODE_T *cur = head
int nodeCnt=0;
while(cur)
{
++nodeCnt;
printf("%s, %s, %d, %d\n",
cur->payload.name,
cur->payload.desc,
cur->payload.hours,
cur->payload.workordernum
);
cur=cur->next;
}
printf("%d nodes printed.\n", nodeCnt);
return(rCode);
}
上面的函数可以从main()中调用:
#include <stdio.h>
#include <errno.h>
int LIST_InsertHeadNode(LIST_NODE_T **, char *, char *, int, int);
int PrintListPayloads(LIST_NODE_T *);
int main(void)
{
int rCode=0;
LIST_NODE_T *listHead = NULL;
/* Insert a linked-list node. */
rCode=LIST_InsertHeadNode(&listHead, "Mahonri", "Jareds Bro", 4, 2421);
if(rCode)
{
fprintf(stderr, "LIST_InsertHeadNode() reports: %d\n", rCode);
goto CLEANUP;
}
/* Insert a linked-list node. */
rCode=LIST_InsertHeadNode(&listHead, "Joe", "CEO", 5, 2419);
if(rCode)
{
fprintf(stderr, "LIST_InsertHeadNode() reports: %d\n", rCode);
goto CLEANUP;
}
/* Insert a linked-list node. */
rCode=LIST_InsertHeadNode(&listHead, "Eve", "Mother", 24, 2);
if(rCode)
{
fprintf(stderr, "LIST_InsertHeadNode() reports: %d\n", rCode);
goto CLEANUP;
}
rCode=PrintListPayloads(listHerad);
if(rCode)
{
fprintf(stderr, "PrintListPayloads() reports: %d\n", rCode);
goto CLEANUP;
}
CLEANUP:
return(rCode);
}
在列表的头部添加节点[即:LIST_InsertHeadNode()] 是添加节点的一种方法。但是,有时将节点添加到列表的另一端(即:列表'tail')更可取。下面的代码显示了这是如何完成的。
首先,一个函数将返回列表的当前“尾节点”。
int LIST_GetTailNode(
LIST_NODE_T *I__listHead, /* The caller supplied list head pointer. */
LIST_NODE_T **_O_listTail /* The function sets the callers pointer to the
last node. */
)
{
int rCode=0;
LIST_NODE_T *curNode = I__listHead;
/* Iterate through all list nodes until the last node is found. */
/* The last node's 'next' field, which is always NULL. */
if(curNode)
{
while(curNode->next)
curNode=curNode->next;
}
/* Set the caller's pointer to point to the last (ie: tail) node. */
if(_O_listTail)
*_O_listTail = curNode;
return(rCode);
}
接下来,一个函数将在列表的尾部插入一个节点。
int LIST_InsertTailNode(
LIST_NODE_T **IO_head,
char *I__name,
char *I__desc,
int I__hours,
int I__workordernum
)
{
int rCode=0;
LIST_NODE_T *tailNode;
LIST_NODE_T *newNode = NULL;
/* Get a pointer to the last node in the list. */
rCode=LIST_GetTailNode(*IO_head, &tailNode);
if(rCode)
{
fprintf(stderr, "LIST_GetTailNode() reports: %d\n", rCode);
goto CLEANUP;
}
重要提示:LIST_GetTailNode() 函数会将tailNode 指针设置为链表中的最后一个节点; - 除非 - 列表中没有节点。当列表为空时,LIST_GetTailNode() 会将tailNode 指针设置为NULL。
/* Allocate memory for new node (with its payload). */
newNode=malloc(sizeof(*newNode));
if(NULL == newNode)
{
rCode=ENOMEM; /* ENOMEM is defined in errno.h */
fprintf(stderr, "malloc() failed.\n");
goto CLEANUP;
}
/* Initialize the new node's payload. */
snprintf(newNode->payload.name, sizeof(newNode->payload.name), "%s", I__name);
snprintf(newNode->payload.desc, sizeof(newNode->payload.desc), "%s", I__desc);
newNode->payload.hours = I__hours;
newNode->payload.workordernum = I__workordernum;
/* Link this node into the list as the new tail node. */
newNode->next = NULL;
if(tailNode)
tailNode->next = newNode;
else
这种“else”情况表示tailNode为NULL时发生,这意味着(当前)链表没有节点。在这种情况下,该节点将是列表中的第一个(头)节点(也是最后一个)。因此,调用者的 'List Head' 指针被更新以指示这个新节点现在是头节点。
*IO_head = newNode;
CLEANUP:
return(rCode);
}
LIST_InsertTailNode() 函数的调用方式与 LIST_InsertHeadNode() 的调用方式相同。唯一的区别是使用 LIST_InsertTailNode() 时,新节点被插入到列表的尾部,而不是列表的头部。
好的,现在您可以在列表的头部或尾部插入一个新节点。在列表中间的某处插入一个新节点怎么样?
例如,假设您想要一个列表,其中所有节点都按有效负载中的某个字段(例如“名称”)进行排序。虽然可以添加所有节点,然后对列表进行排序;将每个新节点插入到列表中的适当位置要容易得多。这样做,列表将始终自动按排序顺序维护。
分两步完成。首先,分配和初始化新节点。然后找出它在列表中的正确位置,然后将新节点链接到该位置的列表中。
首先,一个将“父节点”返回给新节点的函数。 (此节点假定列表按名称排序维护):
int LIST_FetchParentNodeByName(
LIST_NODE_T *I__head,
const char *I__name,
LIST_NODE_T **_O_parent
)
{
int rCode=0;
LIST_NODE_T *parent = NULL;
LIST_NODE_T *curNode = I__head;
/* Inform the caller of an 'empty list' condition. */
if(NULL == I__head)
{
rCode=ENOENT;
goto CLEANUP;
}
/* Find a node with a payload->name string greater than the I__name string */
while(curNode)
{
if(strcmp(curNode->payload.name, I__name) > 0)
break;
parent = curNode; /* Remember this node. It is the parent of the next node. */
curNode=curNode->next; /* On to the next node. */
}
/* Set the caller's 'parent' pointer. */
if(_O_parent)
*_O_parent = parent;
CLEANUP:
return(rCode);
}
现在,一个将插入新节点的函数,保持列表按名称排序。
int LIST_InsertNodeByName(
LIST_NODE_T **IO_head,
char *I__name,
char *I__desc,
int I__hours,
int I__workordernum
)
{
int rCode=0;
LIST_NODE_T *parent;
LIST_NODE_T *newNode = NULL;
/* Allocate memory for new node (with its payload). */
newNode=malloc(sizeof(*newNode));
if(NULL == newNode)
{
rCode=ENOMEM; /* ENOMEM is defined in errno.h */
fprintf(stderr, "malloc() failed.\n");
goto CLEANUP;
}
/* Initialize the new node's payload. */
snprintf(newNode->payload.name, sizeof(newNode->payload.name), "%s", I__name);
snprintf(newNode->payload.desc, sizeof(newNode->payload.desc), "%s", I__desc);
newNode->payload.hours = I__hours;
newNode->payload.workordernum = I__workordernum;
/* Find the proper place to link this node */
rCode=LIST_FetchParentNodeByName(*IO_head, I__name, &parent);
switch(rCode)
{
case 0:
break;
case ENOENT:
/* Handle empty list condition */
newNode->next = NULL;
*IO_head = newNode;
rCode=0;
goto CLEANUP;
default:
fprintf(stderr, "LIST_FetchParentNodeByName() reports: %d\n", rCode);
goto CLEANUP;
}
重要提示:LIST_FetchParentNodeByName() 函数会将父指针设置为列表中立即小于指定 I__name 的节点; - 除非 - 头节点大于指定的 I__name。对于这种特殊情况,LIST_FetchParentNodeByName() 会将父指针设置为 NULL。
/* Handle the case where all current list nodes are greater than the new node. */
/* (Where the new node will become the new list head.) */
if(NULL == parent)
{
newNode->next = *IO_head;
*IO_head = newNode;
goto CLEANUP;
}
/* Final case, insert the new node just after the parent node. */
newNode->next = parent->next;
parent->next = newNode;
CLEANUP:
return(rCode);
}
LIST_InsertNodeByName() 函数的调用方式与 LIST_InsertHeadNode() 或 LIST_InsertTailNode() 的调用方式相同。唯一的区别是使用 LIST_InsertNodeByName() 时,新节点被插入到列表中其排序(按名称)的位置;而不是在列表的头部或尾部。
有时必须从列表中删除节点。这是通过定位要删除的节点,从列表中取消链接节点,然后删除节点及其有效负载来完成的。
首先,通过有效负载名称字段定位特定节点的函数。
int LIST_FetchNodeByName(
LIST_NODE_T *I__head,
const char *I__name,
LIST_NODE_T **_O_node,
LIST_NODE_T **_O_parent
)
{
int rCode=0;
LIST_NODE_T *parent = NULL;
LIST_NODE_T *curNode = I__head;
/* Search the list for a matching payload name. */
while(curNode)
{
if(0 == strcmp(curNode->payload.name, I__name))
break;
parent = curNode; /* Remember this node; it will be the parent of the next. */
curNode=curNode->next;
}
/* If no match is found, inform the caller. */
if(NULL == curNode)
{
rCode=ENOENT;
goto CLEANUP;
}
/* Return the matching node to the caller. */
if(_O_node)
*_O_node = curNode;
/* Return parent node to the caller. */
if(_O_parent)
*_O_parent = parent;
CLEANUP:
return(rCode);
}
这是一个从列表中删除与特定负载名称匹配的节点的函数。
int LIST_DeleteNodeByName(
LIST_NODE_T **IO_head,
char *I__name
)
{
int rCode=0;
LIST_NODE_T *parent;
LIST_NODE_T *delNode = NULL;
/* Find the node to delete. */
rCode=LIST_FetchNodeByName(*IO_head, I__name, &delNode, &parent);
switch(rCode)
{
case 0:
break;
case ENOENT:
fprintf(stderr, "Matching node not found.\n");
goto CLEANUP;
default:
fprintf(stderr, "LIST_FetchNodeByName() reports: %d\n", rCode);
goto CLEANUP;
}
重要提示:LIST_FetchNodeByName()函数会设置delNode的父指针; - 除非 - delNode 是头节点。对于这种特殊情况,LIST_FetchNodeByName() 会将父指针设置为 NULL。
/* Unlink the delNode from the list. */
if(NULL == parent)
*IO_head = delNode->next;
else
parent->next = delNode->next;
/* Free the delNode and its payload. */
free(delNode);
CLEANUP:
return(rCode);
}
注意:以上所有代码都已经过测试,应该可以正常运行,可以下载为:23279119_List_101.c
(待续——根据要求...)