2.7 表

    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 )的情况时(类似于栈的情况)。如果程序里存在多个互相独立地增长和收缩的栈,采用表比用数组能更有效地利用存储。当信息之间有一种内在顺序,就像一些事先不知道长短的链,例如文本中顺序的一系列单词,用表实现也非常合适。如果同时还必须对付频繁的更新和随机访问,那么最好就是使用某些非线性的数据结构,例如树或者散列表等。 (引自原书)






猜你喜欢

转载自blog.csdn.net/coulson_zhao/article/details/79833920
2.7