2.7表
除了数组之外,链表是典型程序中使用最多的数据结构。虽然在C++和Java里表已经由程序库实现了,我们还是需要知道如何使用以及何时使用它。而在C语言里我们就必须自己实现。这一节我们准备讨论C的表,从中学到的东西可以用到更广泛的地方去。
一个单链表包含一组项,每个项都包含了有关数据和指向下一个项的指针。表的头就是一个指针,它指向第一个项,而表的结束则用空指针表示。而下面是一个最基本的链表。
数组和表之间有一些很重要的差别。首先,数组具有固定的大小,而表则永远具有恰好能容纳其所有内容的大小,在这里每个项目都需要一个指针的附加存储开销。第二,通过修改几个指针,表里的各种情况很容易重新进行安排,与数组里需要做大面积的元素移动相比,修改几个指针的代价要小得多。最后,当有某些项被插入或者删除时,其他的项都不必移动。如果把指向一些项的指针存入其他数据结构的元素中,表的修改也不会使这些指针变为非法的。
这些情况说明,如果一个数据集合里的项经常变化,特别是如果项的数目无法预计时,表是一种可行的存储它们的方式。经过这些比较,我们容易看到,数组更适合存储相对静态的数据。但不适用用于前面那节中的可增长数组,因为计算机内存管理和那个的原理我觉得差不多。
在C里通常不是直接定义表的类型List,而是从某种元素类型开始,例如HTML的元素Nameval,给它加一个指针,以便能链接到下一个元素:
typedef struct Nameval Nameval;
struct Nameval{
char *name;
int value;
Nameval *next;
};
对于一个基本的链表,我们希望它可以:
1.可以生成新的元素
2.可以在表头添加元素
3.可以在表尾添加元素
4.可以查找元素
5.可以删除元素
在这里,我们要先了解一下函数指针
void(*fn)(Nameval*, void*)
这说明了fn是一个指向void函数的指针。也就是说,fn本身是个变量,它将以一个返回值为void的函数的地址作为值。被fn指向的函数应该有两个参数,一个参数的类型是Nameval*,即表的元素类型;另一个是void*,是个通用指针,它将作为fn所指函数的一个参数。
也就是说,fn将指向一个指定的写好的函数,例如:
void printnv(Nameval *p, void *arg)
而当我们执行调用的时候,则需要采用:
apply(list_1, printnv, "%s\t: %x\n");
在这里,printfnv是调用的函数,第三个参数就是printfnv中要用到的变量。第二个和第三个参数都将会随着调用函数的不同而出现变化。
则这一部分的代码为:
#include "stdafx.h"
#include "stdlib.h"
#include "string.h"
#include <iostream>
using namespace std;
typedef struct Nameval Nameval;
struct Nameval{
char *name;
int value;
Nameval *next;
};
/*生成新的项*/
Nameval *newitem(char *name, int value)
{
Nameval *newp;
newp = (Nameval *)malloc(sizeof(Nameval));
if (newp == NULL)
{
cout << "Failed" << endl;
exit(1);
}
/*这里赋值的是name的地址*/
newp->name = name;
newp->value = value;
newp->next = NULL;
return newp;
}
/*将新加入的项放在表头*/
Nameval *addfront(Nameval *listp, Nameval *newp)
{
/*表头元素newp指向原表头元素listp*/
newp->next = listp;
return newp;
}
/*将项加入到表的尾部*/
/*这里的listp应该是已有表中任意一个项,newp是待加入的项*/
Nameval *addend(Nameval *listp, Nameval *newp)
{
Nameval *p;
if (listp == NULL)
{
return newp;
}
for (p = listp; p->next != NULL; p = p->next);
p->next = newp;
return listp;
}
/*寻找特定的名字*/
Nameval *lookup(Nameval *listp, char *name)
{
for (; listp != NULL; listp = listp->next)
{
if (strcmp(name, listp->name) == 0)
{
return listp;
}
}
return NULL;
}
/*计算表的长度、打印整个表*/
/*第二个参数是一个函数指针*/
void apply(Nameval *listp, void(*fn)(Nameval*, void*), void *arg)
{
for (; listp != NULL; listp = listp->next)
{
/*函数调用*/
/*相当于
printnv(listp, "%s\t: %x\n")?*/
(*fn)(listp, arg);
}
}
/*打印链表*/
void printnv(Nameval *p, void *arg)
{
char *fmt;
fmt = (char *)arg;
printf(fmt, p->name, p->value);
}
/*链表元素数量*/
void innccounter(Nameval *p, void *arg)
{
int *ip;
ip = (int *)arg;
(*ip)++;
}
/*销毁链表*/
void freeall(Nameval *listp)
{
Nameval *next;
next = listp;
for (; listp != NULL; listp = next)
{
next = listp->next;
free(listp);
}
}
int main()
{
int n = 0;
Nameval *list_1, *list_2;
list_2 = newitem("def", 2);
list_1 = addfront(list_2, newitem("abc", 1));
printf("name\t\t\t; value\n");
/*在这里,第三个参数要根据被调用的函数来决定*/
apply(list_1, printnv, "%s\t\t\t: %x\n");
apply(list_1, innccounter, &n);
printf("链表中元素数量为\t: %d\n", n);
freeall(list_1);
return 0;
}
更加复杂的双链表更适合寻找与插入,但是考虑到头脑风暴的程度,则在这里不予考虑。
表特别适合那些需要在中间插入和删除的情况,也适用于管理一批规模经常变动的无顺序数据,特别是当数据的访问方式接近后进先出 ( LIFO )的情况时(类似于栈的情况)。如果程序里存在多个互相独立地增长和收缩的栈,采用表比用数组能更有效地利用存储。当信息之间有一种内在顺序,就像一些事先不知道长短的链,例如文本中顺序的一系列单词,用表实现也非常合适。如果同时还必须对付频繁的更新和随机访问,那么最好就是使用某些非线性的数据结构,例如树或者散列表等。 (引自原书)