关于C之ADT链表

有时用链表代替数组,那么先看一个数组的例子:

使用一个结构储存每部电影,一个数组储存一年内看过的电影。为简单起见,我们规定结构中只有两个成员:片名和评级(0~10)。

#include<stdio.h>
#include<string.h>
#define TSIZE 45 /* 储存片名的数组大小 */
#define FMAX 5 /* 影片的最大数量 */
struct film {
	char title[TSIZE];
	int rating;
};
char* s_gets(char str[], int lim);
int main(void) {
	struct film movies[FMAX];// 使用一个结构数组
	int i = 0;
	int j;
	puts("Enter first movie title:");
	while (i < FMAX && s_gets(movies[i].title, TSIZE) != NULL
                && movies[i].title[0] != '\0') {
		puts("Enter your rating<0-10>:");
		scanf("%d", &movies[i++].rating);
		while (getchar() != '\n')
			continue;
		puts("Enter next movie title(empty line to stop):");
	}
	if (i == 0)
		printf("No data entered.");
	else
		printf("Here is the movie list:\n");
	for (j = 0; j < i; j++)
		printf("Movie:%s Rating:%d\n", movies[j].title, movies[j].rating);
	printf("Bye!\n");
	return 0;
}
Enter first movie title:
|Harvard Road
Enter your rating<0-10>:
|7
Enter next movie title(empty line to stop):
|Bomb Disposal Expert
Enter your rating<0-10>:
|5
Enter next movie title(empty line to stop):
 
Here is the movie list:
Movie:Harvard Road Rating:7
Movie:Bomb Disposal Expert Rating:5
Bye!

该程序真正的问题是,数据表示太不灵活。程序在编译时确定所需内存量,其实在运行时确定会更好。要解决这个问题,应该使用动态内存分配来表示数据。可以这样做:

#define TSIZE 45 /*储存片名的数组大小*/
struct film {
    char title[TSIZE];
    int rating;
};
...
int n, i;
struct film * movies; /* 指向结构的指针 */
...
printf("Enter the maximum number of movies you'll enter:\n");
scanf("%d", &n);
movies = (struct film *) malloc(n * sizeof(struct film));
while (i < FMAX && s_gets(movies[i].title, TSIZE) != NULL
        && movies[i].title[0] != '\0')

使用malloc(),可以推迟到程序运行时才确定数组中的元素数量。所以,如果只需要20个元素,程序就不必分配存放500个元素的空间。但是,这样做的前提是,用户要为元素个数提供正确的值。

如果用户输入3部影片,程序就调用malloc()3次;如果用户输入300部影片,程序就调用malloc()300次。

不过,我们又制造了另一个麻烦。比较一下,一种方法是调用malloc()一次,为300个film结构请求分配足够的空间;另一种方法是调用malloc()300次,分别为每个file结构请求分配足够的空间。前者分配的是连续的内存块,只需要一个单独的指向struct变量(film)的指针,该指针指向已分配块中的第1个结构。简单的数组表示法让指针访问块中的每个结构,如前面代码段所示。第2种方法的问题是,无法保证每次调用malloc()都能分配到连续的内存块。这意味着结构不一定被连续储存。因此,与第1种方法储存一个指向300个结构块的指针相比,你需要储存300个指针,每个指针指向一个单独储存的结构。

下图直观展示了,一块内存中分配结构和单独分配结构:

一种解决方法是创建一个大型的指针数组,并在分配新结构时逐个给这些指针赋值,但是我们不打算使用这种方法:

#define TSIZE 45 /*储存片名的数组大小*/
#define FMAX 500 /*影片的最大数量*/
struct film {
    char title[TSIZE];
    int rating;
};
...
struct film * movies[FMAX]; /* 结构指针数组 */
int i;
...
movies[i] = (struct film *) malloc (sizeof (struct film));

如果用不完500个指针,这种方法节约了大量的内存,因为内含500个指针的数组比内含500个结构的数组所占的内存少得多。尽管如此,如果用不到 500 个指针,还是浪费了不少空间。而且,这样还是有500个结构的限制。

链表:

(一定要参考:第17章-高级数据表示

还有一种更好的方法。每次使用 malloc()为新结构分配空间时,也为新指针分配空间。但是,还得需要另一个指针来跟踪新分配的指针,用于跟踪新指针的指针本身,也需要一个指针来跟踪,以此类推。要重新定义结构才能解决这个潜在的问题,即每个结构中包含指向 next 结构的指针。然后,当创建新结构时,可以把该结构的地址储存在上一个结构中。简而言之,可以这样定义film结构:

#define TSIZE 45 /* 储存片名的数组大小*/
struct film {
    char title[TSIZE];
    int rating;
    struct film * next;
};

虽然结构不能含有与本身类型相同的结构,但是可以含有指向同类型结构的指针。这种定义是定义链表(linked list)的基础,链表中的每一项都包含着在何处能找到下一项的信息。

在学习链表的代码之前,我们先从概念上理解一个链表。假设用户输入的片名是Modern Times,等级为10。程序将为film类型的结构分配空间,把字符串Modern Times拷贝到结构中的title成员中,然后设置rating成员为10。为了表明该结构后面没有其他结构,程序要把next成员指针设置为NULL(NULL是一个定义在stdio.h头文件中的符号常量,表示空指针)。当然,还需要一个单独的指针储存第1个结构的地址,该指针被称为头指针(head pointer)。头指针指向链表中的第1项。下图直观展示了这种结构:

如果输入更多部电影,建立如下图所示的链表:

假设要显示这个链表,每显示一项,就可以根据该项中已储存的地址来定位下一个待显示的项。然而,这种方案能正常运行,还需要一个指针储存链表中第1项的地址,因为链表中没有其他项储存该项的地址。此时,头指针就派上了用场。

从概念上了解了链表的工作原理,接着我们来实现它。用链表而不是数组来储存电影信息。

创建链表涉及下面3步:

(1)使用malloc()为结构分配足够的空间;

(2)储存结构的地址;

(3)把当前信息拷贝到结构中。

#include <stdio.h>
#include <stdlib.h> /* 提供malloc()原型 */
#include <string.h> /* 提供strcpy()原型 */
#define TSIZE 45 /* 储存片名的数组大小 */
struct film {
	char title[TSIZE];
	int rating;
	struct film* next; /* 指向链表中的下一个结构 */
};
char* s_gets(char* st, int n);
int main(void) {
	struct film* head = NULL;
	struct film* prev, * current; ←连续声明几个指针变量,变量名前都要加“*”
	char input[TSIZE];
	puts("Enter first movie title:");
	while (s_gets(input, TSIZE) != NULL && input[0] != '\0') {
		current = (struct film*) malloc(sizeof(struct film));
		if (head == NULL) /* 第1个结构 */
			head = current;
		else              /* 后续的结构 */
			prev->next = current;
		current->next = NULL;
		strcpy(current->title, input); ← 字符数组是不能直接赋值“=”的!
		puts("Enter your rating <0-10>:");
		scanf("%d", &current->rating);
		while (getchar() != '\n') // 过滤掉scanf留下的结束输入的回车换行符'\n'。
			continue;
		puts("Enter next movie title (empty line to stop):");
		/* 要为下一次输入做好准备。尤其是,要设置 prev 指向当前结构。
		 * 因为在用户输入下一部电影且程序为新结构分配空间后,当前结构将成为新结构的上一个结构 */
		prev = current; // 赋值操作(一个指针变量的值就是某个变量的地址或称为某变量的指针)
	}
	/* 显示电影列表 */
	if (head == NULL)
		printf("No data entered. ");
	else
		printf("Here is the movie list:\n");
	current = head;
	/* 遍历链表时,为何不直接使用head指针,而要重新创建一个新指针(current)?
	 * 因为如果使用head会改变head中的值,程序就找不到链表的开始处。*/
	while (current != NULL) {
		printf("Movie: %s  Rating: %d\n", current->title, current->rating);
		current = current->next;
	}
	/* 完成任务,释放已分配的内存 */
	current = head;
	while (current != NULL) {
		current = head;
		if (current == NULL) // 最后一次循环current一定为NULL
			break;
		head = current->next;
		free(current);// current还是指向该内存,free()只是将里面的值全部抹空
	}
	printf("Bye!\n");
	return 0;
}
Enter first movie title:
|Harvard Road
Enter your rating<0-10>:
|7
Enter next movie title(empty line to stop):
|Bomb Disposal Expert
Enter your rating<0-10>:
|5
Enter next movie title(empty line to stop):
 
Here is the movie list:
Movie:Harvard Road Rating:7
Movie:Bomb Disposal Expert Rating:5
Bye!

以上程序还有些不足。例如,程序没有检查 malloc()是否成功请求到内存,也无法删除链表中的项。这些不足可以弥补。例如,添加代码检查malloc()的返回值是否是NULL(返回NULL说明未获得所需内存)。如果程序要删除链表中的项,还要编写更多的代码。

这种用特定方法解决特定问题,并且在需要时才添加相关功能的编程方式通常不是最好的解决方案。另一方面,通常都无法预料程序要完成的所有任务。随着编程项目越来越大,一个程序员或编程团队事先计划好一切模式,越来越不现实。很多成功的大型程序都是由成功的小型程序逐步发展而来。

如果要修改程序,首先应该强调最初的设计,并简化其他细节。程序清单中的程序示例没有遵循这个原则,它把概念模型和代码细节混在一起。例如,该程序的概念模型是在一个链表中添加项,但是程序却把一些细节(如,malloc()和 current->next 指针)放在最明显的位置,没有突出接口。如果程序能以某种方式强调给链表添加项,并隐藏具体的处理细节(如调用内存管理函数和设置指针)会更好。把用户接口和代码细节分开的程序,更容易理解和更新。要实现这些目标,参考:关于抽象数据类型(ADT)理论
——建立抽象——

从根本上看,电影项目所需的是一个项链表。每一项包含电影名和评级。你所需的操作是把新项添加到链表的末尾和显示链表中的内容。我们把需要处理这些需求的抽象类型叫作链表。链表具有哪些属性?首先,链表应该能储存一系列的项。也就是说,链表能储存多个项,而且这些项以某种方式排列,这样才能描述链表的第1项、第2项或最后一项。其次,链表类型应该提供一些操作,如在链表中添加新项。下面是链表的一些有用的操作:

初始化一个空链表;
在链表末尾添加一个新项;
确定链表是否为空;
确定链表是否已满;
确定链表中的项数;
访问链表中的每一项执行某些操作,如显示该项。

对该电影项目而言,暂时不需要其他操作。但是一般的链表还应包含以下操作:

在链表的任意位置插入一个项;
移除链表中的一个项;
在链表中检索一个项(不改变链表);
用另一个项替换链表中的一个项;
在链表中搜索一个项。

为了让示例尽量简单,我们采用一种简化的链表作为抽象数据类型。它只包含电影项目中的所需属性。该类型总结如下:

类型名:     简单链表
类型属性:    可以储存一系列项
类型操作:    初始化链表为空
确定链表为空
确定链表已满
确定链表中的项数
在链表末尾添加项
遍历链表,处理链表中的项
清空链表

建立接口:

这个简单链表的接口有两个部分。第1部分是描述如何表示数据,第2部分是描述实现ADT操作的函数。例如,要设计在链表中添加项的函数和报告链表中项数的函数。接口设计应尽量与ADT的描述保持一致。因此,应该用某种通用的Item类型而不是一些特殊类型,如int或struct film。可以用C的typedef功能来定义所需的Item类型:

#define TSIZE 45 /* 储存电影名的数组大小 */
struct film {
    char title[TSIZE];
    int rating;
};
typedef struct film Item;

然后,就可以在定义的其余部分使用 Item 类型。如果以后需要其他数据形式的链表,可以重新定义Item类型,不必更改其余的接口定义。定义了 Item 之后,现在必须确定如何储存这种类型的项。实际上这一步属于实现步骤,但是现在决定好可以让示例更简单些。

typedef struct node {
    Item item;
    struct node * next;
} Node;
typedef Node * List;

在链表的实现中,每一个链节叫作节点(node)。每个节点包含形成链表内容的信息和指向下一个节点的指针。为了强调这个术语,我们把node作为节点结构的标记名,并使用typedef把Node作为struct node结构的类型名。最后,为了管理链表,还需要一个指向链表开始处的指针,创建了该链表所需类型的指针movies:List movies; 

其实我们可以如下定义List类型,还可以添加一个变量记录项数:

typedef struct list {
    Node * head; /* 指向链表头的指针 */
    int size;   /* 链表中的项数 */
} List; /* List的另一种定义 */

这样初始化:

movies.next = NULL;
movies.size = 0;

使用List的人都不用担心这些细节,只要能使用下面的代码就行:

InitializeList(movies);

InitializeList()的函数原型如下:

/* 操作:初始化一个链表 */
/* 前提条件:plist指向一个链表 */
/* 后置条件:该链表初始化为空 */
void InitializeList(List * plist);

这里要注意两点:

第1,前提条件:是调用该函数前应具备的条件。例如,需要一个待初始化的链表。

第2,后置条件:是执行完该函数后的情况。

注:该函数的参数是一个指向链表的指针,而不是一个链表。所以应该这样调用该函数:

InitializeList(&movies);

由于按值传递参数,所以该函数只能通过指向该变量的指针才能更改主调程序传入的变量。

这里,由于语言的限制使得接口和抽象描述略有区别。

定义接口:

/* list.h -- 简单链表类型的头文件 */
#ifndef LIST_H_
#define LIST_H_
#include <stdbool.h> /* C99特性 */
/* 特定程序的声明 */
#define TSIZE   45 /* 储存电影名的数组大小 */
typedef struct film {
	char title[TSIZE];
	int rating;
} Item;
typedef struct node {
	Item item;
	struct node *next;
} Node, *List;
/* 函数原型 */
/* 操作:     初始化一个链表 */
/* 前提条件:  plist指向一个链表 */
/* 后置条件:  链表初始化为空 */
void InitializeList(List *plist);
/* 操作:     确定链表是否为空定义,plist指向一个已初始化的链表 */
/* 后置条件:  如果链表为空,该函数返回true;否则返回false */
bool ListIsEmpty(const List *plist);
/* 操作:     确定链表是否已满,plist指向一个已初始化的链表 */
/* 后置条件:  如果链表已满,该函数返回真;否则返回假 */
bool ListIsFull(const List *plist);
/* 操作:     确定链表中的项数, plist指向一个已初始化的链表 */
/* 后置条件:  该函数返回链表中的项数 */
unsigned int ListItemCount(const List *plist);
/* 操作:     在链表的末尾添加项 */
/* 前提条件:  item是一个待添加至链表的项, plist指向一个已初始化的链表 */
/* 后置条件:  如果可以,该函数在链表末尾添加一个项,且返回true;否则返回false */
bool AddItem(Item item, List *plist);
/* 操作:     把函数作用于链表中的每一项 */
/*      plist指向一个已初始化的链表 */
/*      pfun指向一个函数,该函数接受一个Item类型的参数,且无返回值 */
/* 后置条件:  pfun指向的函数作用于链表中的每一项一次            */
void Traverse(const List *plist, void (*pfun)(Item item));
/* 操作:   释放已分配的内存(如果有的话) */
/*      plist指向一个已初始化的链表 */
/* 后置条件:  释放了为链表分配的所有内存,链表设置为空 */
void EmptyTheList(List *plist);
#endif

使用接口:

创建一个List类型的变量。
创建一个Item类型的变量。
初始化链表为空。
当链表未满且有输入时:
把输入读取到Item类型的变量中。
在链表末尾添加项。
访问链表中的每个项并显示它们。
/* films3.c -- 使用抽象数据类型(ADT)风格的链表 */
#include <stdio.h>
#include <stdlib.h>  /* 提供exit()的原型 */
#include "list.h"   /* 定义List、Item */
void showmovies(Item item);
char* s_gets(char *st, int n);
int main(void) {
	List movies;
	Item temp;
	InitializeList(&movies); /* 初始化 */
	if (ListIsFull(&movies)) {
		fprintf(stderr, "No memory available! Bye!\n");
		exit(1);
	}
	/* 获取用户输入并储存 */
	puts("Enter first movie title:");
	while (s_gets(temp.title, TSIZE) != NULL && temp.title[0] != '\0') {
		puts("Enter your rating <0-10>:");
		scanf("%d", &temp.rating);
		while (getchar() != '\n')
			continue;
		if (AddItem(temp, &movies) == false) {
			fprintf(stderr, "Problem allocating memory\n");
			break;
		}
		if (ListIsFull(&movies)) {
			puts("The list is now full.");
			break;
		}
		puts("Enter next movie title (empty line to stop):");
	}
	/* 显示 */
	if (ListIsEmpty(&movies))
		printf("No data entered. ");
	else {
		printf("Here is the movie list:\n");
		Traverse(&movies, showmovies);
	}
	printf("You entered %d movies.\n", ListItemCount(&movies));
	EmptyTheList(&movies);/* 清理 */
	printf("Bye!\n");
	return 0;
}
void showmovies(Item item) {
	printf("Movie: %s  Rating: %d\n", item.title, item.rating);
}

实现接口:

/* list.c -- 支持链表操作的函数 */
#include <stdio.h>
#include <stdlib.h>
#include "list.h"
/* 局部函数原型 */
static void CopyToNode(Item item, Node *pnode);
/* 接口函数 */
/* 把链表设置为空 */
void InitializeList(List *plist) {
	*plist = NULL;
}
/* 如果链表为空,返回true */
bool ListIsEmpty(const List *plist) {
	if (*plist == NULL)
        return true;
	else
        return false;
}
/* 如果链表已满,返回true */
bool ListIsFull(const List *plist) {
    int count = ListItemCount(plist);
    if (TSIZE == count)
        return true;
    return false;
}
/* 返回节点的数量 */
unsigned int ListItemCount(const List *plist) {
	unsigned int count = 0;
	Node *pnode = *plist; /* 设置链表的开始 */
	while (pnode != NULL) {
		++count;
		pnode = pnode->next; /* 设置下一个节点 */
	}
	return count;
}
/* 创建储存项的节点,并将其添加至由plist指向的链表末尾(较慢的实现)*/
bool AddItem(Item item, List *plist) {
	Node *pnew;
	Node *scan = *plist;
	pnew = (Node*) malloc(sizeof(Node));
	if (pnew == NULL)
		return false; /* 失败时退出函数 */
	CopyToNode(item, pnew);
	pnew->next = NULL;
	if (scan == NULL) /* 空链表,所以把pnew放在链表的开头 */
		*plist = pnew;
	else {
		while (scan->next != NULL)
			scan = scan->next; /* 找到链表的末尾把pnew添加到链表的末尾 */
		scan->next = pnew;
	}
	return true;
}
/* 访问每个节点并执行pfun指向的函数 */
void Traverse(const List *plist, void (*pfun)(Item item)) {
	Node *pnode = *plist; /* 设置链表的开始 */
	while (pnode != NULL) {
		(*pfun)(pnode->item); /* 把函数应用于链表中的项 */
		pnode = pnode->next; /* 前进到下一项 */
	}
}
/* 释放由malloc()分配的内存,设置链表指针为NULL */
void EmptyTheList(List *plist) {
	Node *psave;
	while (*plist != NULL) {
		psave = (*plist)->next; /* 保存下一个节点的地址  */
		free(*plist); /* 释放当前节点     */
		*plist = psave; /* 前进至下一个节点   */
	}
}
/* 局部函数定义 */
/* 把一个项拷贝到节点中 */
static void CopyToNode(Item item, Node *pnode) {
	pnode->item = item; /* 拷贝结构 */
}
Enter first movie title:
|Havard Rode
Enter your rating <0-10>:
|10
Enter next movie title (empty line to stop):
|My Dog
Enter your rating <0-10>:
|8
Enter next movie title (empty line to stop):

Here is the movie list:
Movie: Havard Rode  Rating: 10
Movie: My Dog  Rating: 8
You entered 2 movies.
Bye!

首先,通过如下代码:

char arr[6] = "hello";
bool b = (*(&arr) == &(*arr));
printf("%d", b);

打印输出为:1,证明了*与&确实是互逆操作的。

代码中有个疑问,如下:

List movies;
<=>Node *p_movies;
函数原型:void InitializeList(List *plist); 
调用:InitializeList(&movies);
<=>InitializeList(&p_movies);
<=>InitializeList(movies);
函数的实现:
void InitializeList(List *plist) {
    *plist = NULL;
    <=>*(&movies) = NULL;
    <=>movies = NULL;
}

为何要绕这个弯呢?

如果去掉参数及函数实现里面所有对应的*然后去掉主main()函数里面的&,结果是:

AddItem(temp, movies)里面执行到最后movies!=NULL,但直接在main()调用AddItem()之后直接打印movies为NULL,导致ListIsEmpty(movies) == true。

详解见:关于按值传递与指针误区解读

提示const限制:

多个处理链表的函数都把const List * plist作为形参,表明这些函数不会更改链表。这里, const确实提供了一些保护。它防止了*plist(即plist所指向的量)被修改。在该程序中,plist指向movies,所以const防止了这些函数修改movies。因此,在ListItemCount()中,不允许有类似下面的代码:

*plist = (*plist)->next; // 如果*plist是const,不允许这样做

因为改变*plist就改变了movies,将导致程序无法跟踪数据。然而,*plist和movies都被看作是const并不意味着*plist或movies指向的数据是const。例如,可以编写下面的代码:

(*plist)->item.rating = 3; // 即使*plist是const,也可以这样做

因为上面的代码并未改变*plist,它改变的是*plist指向的数据。由此可见,不要指望const能捕获到意外修改数据的程序错误。

通过下面直观了解电影程序的3个部分:

如果需要另一个简单的链表,也可以使用这些文件。假设你需要储存亲戚的一些信息:姓名、关系、地址和电话号码,那么先要在 list.h 文件中重新定义Item类型:

typedef struct itemtag {
    char fname[14];
    char lname [24];
    char relationship[36];
    char address [60];
    char phonenum[20];
} Item;

当前使用的AddItem()函数效率不高,因为它总是从链表第 1 个项开始,然后搜索至链表末尾。可以通过保存链表结尾处的地址来解决这个问题。例如,可以这样重新定义List类型:

typedef struct list {// 双向链表
    Node * head; /* 指向链表的开头 */
    Node * end;  /* 指向链表的末尾 */
} List;

以上即定义了一个双向链表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值