数据结构 - 单链表 - C 语言

数据结构 - 单链表 - C 语言

通过组合使用结构和指针创建强大的数据结构。

链表 (linked list) 是一些包含数据的独立数据结构 (通常称为节点) 的集合。链表中的每个节点通过链或指针连接在一起。程序通过指针访问链表中的节点。通常节点是动态分配的,但有时你也能看到由节点数组构建的链表。即使在这种情况下,程序也是通过指针来遍历链表的。

1. 单链表

在单链表中,每个节点包含一个指向链表下一节点的指针。链表最后一个节点的指针字段的值为 NULL,提示链表后面不再有其他节点。在你找到链表的第 1 个节点后,指针就可以带你访问剩余的所有节点。为了记住链表的起始位置,可以使用一个根指针 (root pointer)。根指针指向链表的第 1 个节点。注意根指针只是一个指针,它不包含任何数据。

在这里插入图片描述

单链表图示
//============================================================================
// Name        : typedef - struct
// Author      : Yongqiang Cheng
// Version     : Version 1.0.0
// Copyright   : Copyright (c) 2019 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================

typedef struct structure_name
{
	data_type member_name1;
	data_type member_name2;
	data_type member_name3;
	data_type member_name4;
} type_name;

struct structure_name
{
	data_type member_name1;
	data_type member_name2;
	data_type member_name3;
	data_type member_name4;
} object_names;

声明创建结构

//============================================================================
// Name        : typedef - struct
// Author      : Yongqiang Cheng
// Version     : Version 1.0.0
// Copyright   : Copyright (c) 2019 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================

typedef struct NODE
{
	struct NODE *1ink;
	int value;
} Node;

存储于每个节点的数据是一个整型值。这个链表包含三个节点。如果你从根指针开始,随着指针到达第 1 个节点,你可以访问存储于那个节点的数据。随着第 1 个节点的指针可以到达第 2 个节点,你可以访问存储在那里的数据。最后,第 2 个节点的指针带你来到最后一个节点。零值提示它是一个 NULL 指针,在这里它表示链表中不再有更多的节点 。

在上面的图中,这些节点相邻在一起,这是为了显示链表所提供的逻辑顺序。链表中的节点可能分布于内存中的各个地方。对于一个处理链表的程序而言,各节点在物理上是否相邻并没有什么区别,因为程序始终用链 (指针) 从一个节点移动到另一个节点。

单链表可以通过链从开始位置遍历链表直到结束位置,但链表无法从相反的方向进行遍历。当你的程序到达链表的最后一个节点时,如果你想回到其他任何节点,你只能利用根指针从头开始。当然,程序在移动到下一个节点前可以保存一个指向当前位置的指针,甚至可以保存指向前面几个位置的指针。链表是动态分配的,可能增长到几百或几千个节点,所以要保存所有指向前面位置的节点的指针是不可行的。

在这个特定的链表中,节点根据数据的值按升序链接在一起。对于有些应用程序而言,这种顺序非常重要,比如根据一天的时间安排约会。对于那些不要求排序的应用程序,当然也可以创建无序的链表。

1.1 在单链表中插入 - 正常实现

假定我们有一个新值,比如 12,想把它插入到前面那个链表中。从概念上说,这个任务非常简单:从链表的起始位置开始,跟随指针直到找到第 1 个值大于 12 的节点,然后把这个新值插入到那个节点之前的位置。

我们按顺序访问链表,当到达内容为 15 的节点 (第 1 个值大于 12 的节点) 时就停下来。我们知道这个新值应该添加到这个节点之前,但前一个节点的指针字段必须进行修改以实现这个插入。但是,我们已经越过了这个节点,无法返回去。解决这个问题的方法就是始终保存一个指向链表当前节点之前的那个节点的指针。

把一个节点插入到一个有序的单链表中,函数的参数是一个指向链表第 1 个节点的指针以及需要插入的值。

//============================================================================
// Name        : typedef - struct
// Author      : Yongqiang Cheng
// Version     : Version 1.0.0
// Copyright   : Copyright (c) 2019 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================

/*
 ** Insert into an ordered, singly linked list. The arguments are
 ** a pointer to the first node in the list, and the value to insert.
 */

#include <stdlib.h>
#include <stdio.h>

#define	FALSE	1
#define	TRUE	0

typedef struct NODE
{
	struct NODE *link;
	int value;
} Node;

int sll_insert(Node *current, int new_value)
{
	Node *previous;
	Node *new_node;

	/*
	 ** Look for the right place by walking down the list
	 ** until we reach a node whose value is greater than or equal to the new value.
	 */
	while (current->value < new_value)
	{
		previous = current;
		current = current->link;
	}

	/*
	 ** Allocate a new node and store the new value into it.
	 ** In this event, we return FALSE.
	 */
	new_node = (Node *) malloc(sizeof(Node));
	if (NULL == new_node)
	{
		return FALSE;
	}

	new_node->value = new_value;

	/*
	 ** Insert the new node into the list, and return TRUE.
	 */
	new_node->link = current;
	previous->link = new_node;

	return TRUE;
}

我们用下面这种方法调用这个函数:

result = sll_insert(root, 12);

让我们仔细跟踪代码的执行过程,看看它是否把新值 12 正确地插入到链表中。首先,传递给函数的参数是 root 变量的值,它是指向链表第 1 个节点的指针。当函数刚开始执行时,链表的状态如下:

在这里插入图片描述

这张图并没有显示 root 变量,因为函数不能访问它。它的值的一份拷贝作为形参 current 传递给函数,但函数不能访问 root。现在 current->value 是 5,它小于 12,所以循环体再次执行。当我们回到循环的顶部时,currentprevious 指针都向前移动了一个节点。

在这里插入图片描述

现在,current->value 的值为 10,因此循环体还将继续执行,结果如下:

在这里插入图片描述

现在,current->value 的值大于 12,所以退出循环。

此时,重要的是 previous 指针,因为它指向我们必须加以修改以插入新值的那个节点。但首先,我们必须得到一个新节点,用于容纳新值。下面这张图显示了新值被复制到新节点之后链表的状态。

在这里插入图片描述

把这个新节点链接到链表中需要两个步骤。首先,

new_node->link = current;

使新节点指向将成为链表下一个节点的节点,也就是我们所找到的第 1 个值大于 12 的那个节点。在这个步骤之后,链表的内容如下所示:

在这里插入图片描述

第二个步骤是让 previous 指针所指向的节点 (也就是最后一个值小于 12 的那个节点) 指向这个新节点。下面这条语句用于执行这项任务。

previous->link = new_node;

这个步骤之后,链表的状态如下:

在这里插入图片描述

然后函数返回,链表的最终样子如下:

在这里插入图片描述

从根指针开始,随各个节点的 link 字段逐个访问链表,我们可以发现这个新节点己被正确地插入到链表中。

不幸的是,这个插入函数是不正确的。

试试把 20 这个值插入到链表中,你就会发现一个问题:while 循环越过链表的尾部,并对一个 NULL 指针执行间接访问操作。为了解决这个问题,我们必须对 current 的值进行测试,在执行表达式 current->value 之前确保它不是一个 NULL 指针:

while ((NULL != current) && (current->value < new_value))

为了在链表的起始位置插入一个节点,函数必须修改根指针。但是,函数不能访问变量 root修正这个问题最容易的方法是把 root 声明为全局变量,这样插入函数就能修改它。不幸的是,这是最坏的一种问题解决方法。因为这样一来,函数只对这个链表起作用。

稍好的解决方法是把一个指向 root 的指针作为参数传递给函数。然后,使用间接访问,函数不仅可以获得 root (指向链表第 1 个节点的指针,也就是根指针) 的值,也可以向它存储一个新的指针值。root 是一个指向 Node 的指针,所以参数的类型应该是 Node **,也就是一个指向 Node 的指针的指针。

result = sll_insert(&root, 12);
//============================================================================
// Name        : typedef - struct
// Author      : Yongqiang Cheng
// Version     : Version 1.0.0
// Copyright   : Copyright (c) 2019 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================

/*
 ** Insert into an ordered, singly linked list. The arguments are
 ** a pointer to the first node in the list, and the value to insert.
 */

#include <stdlib.h>
#include <stdio.h>

#define	FALSE	1
#define	TRUE	0

typedef struct NODE
{
	struct NODE *link;
	int value;
} Node;

int sll_insert(Node **rootp, int new_value)
{
	Node *current;
	Node *previous;
	Node *new_node;

	if (NULL == rootp)
	{
		return FALSE;
	}

	/*
	 ** Get the pointer to the first node.
	 */
	current = *rootp;
	previous = NULL;

	/*
	 ** Look for the right place by walking down the list
	 ** until we reach a node whose value is greater than or equal to the new value.
	 */
	while ((NULL != current) && (current->value < new_value))
	{
		previous = current;
		current = current->link;
	}

	/*
	 ** Allocate a new node and store the new value into it.
	 ** In this event, we return FALSE.
	 */
	new_node = (Node *) malloc(sizeof(Node));
	if (NULL == new_node)
	{
		return FALSE;
	}
	new_node->value = new_value;

	/*
	 ** Insert the new node into the list, and return TRUE.
	 */
	new_node->link = current;
	if (NULL == previous)
	{
		*rootp = new_node;
	}
	else
	{
		previous->link = new_node;
	}

	return TRUE;
}

这第 2 个版本包含了另外一些语句。

previous = NULL;

我们需要这条语旬,这样我们在以后就可以检查新值是否应为链表的第 1 个节点。

current = *rootp ;

这条语句对根指针参数执行间接访问操作,得到的结果是 root 的值,也就是指向链表第 1 个节点的指针。

	/*
	 ** Insert the new node into the list, and return TRUE.
	 */
	new_node->link = current;
	if (NULL == previous)
	{
		*rootp = new_node;
	}
	else
	{
		previous->link = new_node;
	}

这条语句被添加到函数的最后。它用于检查新值是否应该被添加到链表的起始位置。如果是,我们使用间接访问修改根指针,使它指向新节点。

这个函数可以正确完成任务,而且在许多语言中,这是你能够获得的最佳方案。我们还可以做得更好一些,因为 C 允许我们获得现存对象的地址 (即指向该对象的指针)。

//============================================================================
// Name        : typedef - struct
// Author      : Yongqiang Cheng
// Version     : Version 1.0.0
// Copyright   : Copyright (c) 2019 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================

/*
 ** Insert into an ordered, singly linked list. The arguments are
 ** a pointer to the first node in the list, and the value to insert.
 */

#include <stdio.h>
#include <stdlib.h>

#define	FALSE	1
#define	TRUE	0

typedef struct NODE
{
	struct NODE *link;
	int value;
} Node;

// This function prints contents of linked list starting from the given node.
int sll_display(const Node *root)
{
	const Node *head = root;

	if (NULL == root)
	{
		return FALSE;
	}

	printf("\n[ ");

	while (NULL != head)
	{
		printf("%d ", head->value);
		head = head->link;
	}

	printf("]");

	return TRUE;
}

int sll_insert(Node **rootp, int new_value)
{
	Node *current;
	Node *previous;
	Node *new_node;

	if (NULL == rootp)
	{
		return FALSE;
	}

	/*
	 ** Get the pointer to the first node.
	 */
	current = *rootp;
	previous = NULL;

	/*
	 ** Look for the right place by walking down the list
	 ** until we reach a node whose value is greater than or equal to the new value.
	 */
	while ((NULL != current) && (current->value < new_value))
	{
		previous = current;
		current = current->link;
	}

	/*
	 ** Allocate a new node and store the new value into it.
	 ** In this event, we return FALSE.
	 */
	new_node = (Node *) malloc(sizeof(Node));
	if (NULL == new_node)
	{
		return FALSE;
	}
	new_node->value = new_value;

	/*
	 ** Insert the new node into the list, and return TRUE.
	 */
	new_node->link = current;
	if (NULL == previous)
	{
		*rootp = new_node;
	}
	else
	{
		previous->link = new_node;
	}

	return TRUE;
}

int main()
{
	Node *root = NULL;
	int result = 0;
	int ret = 0;

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 5);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 10);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 15);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 9);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 12);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 0);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 20);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	return TRUE;
}

NULL pointer.
[ 5 ]
[ 5 10 ]
[ 5 10 15 ]
[ 5 9 10 15 ]
[ 5 9 10 12 15 ]
[ 0 5 9 10 12 15 ]
[ 0 5 9 10 12 15 20 ]

1.2 在单链表中插入 - 优化实现

把一个节点插入到链表的起始位置似乎需要作为一种特殊情况进行处理,我们此时插入新节点需要修改的指针是根指针。对于任何其他节点,对指针进行修改时实际修改的是前一个节点的 link 字段。这两个看上去不同的操作实际上是一样的。

消除特殊情况的关键在于:我们必须认识到,链表中的每个节点都有一个指向它的指针。对于第 1 个节点,这个指针是根指针,对于其他节点,这个指针是前一个节点的 link 字段。重点在于每个节点都有一个指针指向它。至于该指针是不是位于一个节点的内部则无关紧要。

这是第 1 个节点和指向它的指针 。

在这里插入图片描述

如果新值插入到第 1 个节点之前,这个指针就必须进行修改。

下面是第 2 个节点和指向它的指针。

在这里插入图片描述

如果新值需要插入到第 2 个节点之前,那么这个指针必须进行修改。注意我们只考虑指向这个节点的指针,至于哪个节点包含这个指针则无关紧要。对于链表中的其他节点,都可以应用这个模式。

现在让我们看一下修改后的函数 (当它开始执行时)。下面显示了第 1 条赋值语句之后各个变量的情况。

在这里插入图片描述

我们拥有一个指向当前节点的指针,以及一个指向当前节点的 link 字段的指针。除此之外,我们就不需要别的了!如果当前节点的值大于新值,那么 rootp 指针就会告诉我们哪个 link 字段必须进行修改,以便让新节点链接到链表中。如果在链表其他位置的插入也可以用同样的方式进行表示,就不存在前面提到的特殊情况了。其关键在于我们前面看到的指针/节点关系。

当移动到下一个节点时,我们保存一个指向下一个节点的 link 字段的指针,而不是保存一个指向前一个节点的指针。我们很容易画出一张描述这种情况的图。

在这里插入图片描述

注意,这里 rootp 并不指向节点本身,而是指向节点内部的 link 字段。这是简化插入函数的关键所在,但我们必须能够取得当前节点的 link 字段的地址。在 C 中,这种操作是非常容易的。表达式 &current->link 就可以达到这个目的。rootp 参数现在称为 linkp,因为它现在指向的是不同的 link 字段,而不仅仅是根指针。我们不再需要 previous 指针,因为我们的 link 指针可以负责寻找需要修改的 link 字段。前面那个函数最后部分用于处理特殊情况的代码也不见了,因为我们始终拥有一个指向需要修改的 link 字段的指针。我们用一种和修改节点的 link 字段完全一样的方式修改 root 变量。最后,我们在函数的指针变量中增加了 register 声明,用于提高结果代码的效率。

. - 访问结构成员
-> - 访问结构指针成员
* - 间接访问
& - 取地址

在这里插入图片描述

我们在最终版本中的 while 循环中增加了一个窍门,它嵌入了对 current 的赋值。下面是一个功能相同,但长度稍长的循环。

	/*
	 ** Look for the right place by walking down the list
	 ** until we reach a node whose value is greater than or equal to the new value.
	 */
	current = *linkp;
	while ((NULL != current) && (current->value < new_value))
	{
		linkp = &current->link;
		current = *linkp;
	}

一开始,current 被设置为指向链表的第 1 个节点。while 循环测试我们是否到达了链表的尾部。如果没有,它接着检查我们是否到达了正确的插入位置。如果不是,循环体继续执行,并把 linkp 设置为指向当前节点的 link 字段,并使 current 指向下一个节点。

循环的最后一条语句和循环之前的那条语句相同,这就促使我们对它进行简化,方法是把 current 的赋值嵌入到 while 表达式中。其结果是一个稍为复杂但更加紧凑的循环,因为我们消除了 current 的冗余赋值。

插入到一个有序单链表。函数的参数是一个指向链表第一个节点的指针,以及一个需要插入的新值。

//============================================================================
// Name        : typedef - struct
// Author      : Yongqiang Cheng
// Version     : Version 1.0.0
// Copyright   : Copyright (c) 2019 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================

/*
 ** Insert into an ordered, singly linked list.  The arguments are
 ** a pointer to the first node in the list, and the value to insert.
 */

#include <stdio.h>
#include <stdlib.h>

#define	FALSE	1
#define	TRUE	0

typedef struct NODE
{
	struct NODE *link;
	int value;
} Node;

// This function prints contents of linked list starting from the given node.
int sll_display(const Node *root)
{
	const Node *head = root;

	if (NULL == root)
	{
		return FALSE;
	}

	printf("\n[ ");

	while (NULL != head)
	{
		printf("%d ", head->value);
		head = head->link;
	}

	printf("]");

	return TRUE;
}

int sll_insert(register Node **linkp, int new_value)
{
	register Node *current;
	register Node *new_node;

	if (NULL == linkp)
	{
		return FALSE;
	}

	/*
	 ** Look for the right place by walking down the list
	 ** until we reach a node whose value is greater than or equal to the new value.
	 */
	current = *linkp;
	while ((NULL != current) && (current->value < new_value))
	{
		linkp = &current->link;
		current = *linkp;
	}

	/*
	 ** Allocate a new node and store the new value into it.
	 ** In this event, we return FALSE.
	 */
	new_node = (Node *) malloc(sizeof(Node));
	if (NULL == new_node)
	{
		return FALSE;
	}

	new_node->value = new_value;

	/*
	 ** Insert the new node into the list, and return TRUE.
	 */
	new_node->link = current;
	*linkp = new_node;

	return TRUE;
}

int main()
{
	Node *root = NULL;
	int result = 0;
	int ret = 0;

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 5);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 10);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 15);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 9);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 12);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 0);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	result = sll_insert(&root, 20);
	if (result)
	{
		printf("Failed to insert element.");
	}

	ret = sll_display(root);
	if (ret)
	{
		printf("NULL pointer.");
	}

	return TRUE;
}

NULL pointer.
[ 5 ]
[ 5 10 ]
[ 5 10 15 ]
[ 5 9 10 15 ]
[ 5 9 10 12 15 ]
[ 0 5 9 10 12 15 ]
[ 0 5 9 10 12 15 20 ]

消除特殊情况使这个函数更为简单。这个改进之所以可行是由于两方面的因素。第 1 个因素是我们正确解释问题的能力。除非你可以在看上去不同的操作中总结出共性,不然你只能编写额外的代码来处理特殊情况。通常,这种知识只有在你学习了一阵数据结构并对其有进一步的理解之后才能获得。第 2 个因素是 C 语言提供了正确的工具帮助你归纳问题的共性。

这个改进的函数依赖于 C 能够取得现存对象的地址这一能力。和许多 C 语言特性一样,这个能力既成力巨大,又暗伏凶险。防止错误诸如越界引用数组元素或产生一种类型的指针但实际上指向另一种类型的对象。

C 的指针限制要少得多,这也是我们能改进插入函数的原因所在。C 程序员在使用指针时必须加倍小心,以避免产生错误。Pascal 语言的指针哲学有点类似下面这样的说法:使用锤子可能会伤着你自己,所以我们不给你锤子。C 语言的指针哲学则是:给你锤子,实际上你可以使用好几种锤子,祝你好运!有了这个能力之后,C 程序员较之 Pascal 程序员更容易陷入麻烦,但优秀的 C 程序员可以比他们的 Pascal 和 Modula 同行产生体积更小、效率更高、可维护性更佳的代码。这也是 C 语言在业界为何如此流行以及经验丰富的 C 程序员为何如此受青昧的原因之一。

2. 警告的总结

  1. 落到链表尾部的后面。
  2. 使用指针时应格外小心,因为 C 并没有对它们的使用提供安全网。
  3. if 语句中提炼语句可能会改变测试结果。

3. 编程提示的总结

  1. 消除特殊情况使代码更易于维护。
  2. 通过提炼语句消除 if 语句中中的重复语句。
  3. 不要仅仅根据代码的大小评估它的质量。

语句提炼是一种简化程序的技巧,其方法是消除程序中冗余的语句。如果一条 if 语句的 thenelse 子句以相同序列的语句结尾,它们可以被一份单独的出现于 if 语句之后的拷贝代替。相同序列的语句也可以从 if 语句的起始位置提炼出来,但这种提炼不能改变 if 的测试结果。如果不同的语句事实上执行相同的功能,你可以把它们写成相同的样子,然后再使用语句提炼简化程序。

发布了509 篇原创文章 · 获赞 1824 · 访问量 110万+

猜你喜欢

转载自blog.csdn.net/chengyq116/article/details/104845751
今日推荐