即使是很复杂的程序,也是由简单的数组、表、散列表等简单东西堆砌起来的。
顺序检索,二分检索
快速排序
C函数库中的qsort,调用时必须提供一个比较函数。ANSI C的二分检索函数:bsearch,它也要求一个指向比较函数的指针。标准C++库中有个名为sort的类属算法。
某一种快速排序:
/* quicksort: sort v[0]..v[n-1] into increasing order */
void quicksort(int v[], int n)
{
int i, last;
if (n <= 1) /* nothing to do */
return;
swap(v, 0, rand() % n); /* move pivot elem to v[0] */
last = 0;
for (i = 1; i < n; i++) /* partition */
if (v[i] < v[0])
swap(v, ++last, i);
swap(v, 0, last); /* restore pivot */
quicksort(v, last); /* recursively sort */
quicksort(v+last+1, n-last-1); /* each part */
}
/* swap: interchange v[i] and v[j] */
void swap(int v[], int i, int j)
{
int temp;
temp = v[i];
v[i] = v[j];
v[j] = temp;
}
排序算法花费的时间正比于nlogn。
定义一个可增长数组,新元素被加到有关数组的最后。在必要时将自动增大以提供新的空间。删除名字需要一点诀窍,如果元素顺序不重要,最简单的方法就是把位于数组的最后元素复制到这里。
typedef struct Nameval Nameval;
struct Nameval {
char *name;
int value;
};
struct Nvtab {
int nval; /* current number of values */
int max; /* allocated number of values */
Nameval *nameval; /* array of name-value pairs */
};
enum { NVINIT = 1, NVGROW = 2 };
/* addname: add new name and value to nvtab */
int addname(Nameval newname)
{
Nameval *nvp;
if (nvtab.nameval == NULL) { /* first time */
nvtab.nameval = (Nameval *) malloc(NVINIT * sizeof(Nameval));
if (nvtab.nameval == NULL)
return -1;
nvtab.max = NVINIT;
nvtab.nval = 0;
} else if (nvtab.nval >= nvtab.max) { /* grow */
nvp = (Nameval *) realloc(nvtab.nameval,
(NVGROW*nvtab.max) * sizeof(Nameval));
if (nvp == NULL)
return -1;
nvtab.max *= NVGROW;
nvtab.nameval = nvp;
}
nvtab.nameval[nvtab.nval] = newname;
return nvtab.nval++;
}
/* delname: remove first matching nameval from nvtab */
int delname(char *name)
{
int i;
for (i = 0; i < nvtab.nval; i++)
if (strcmp(nvtab.nameval[i].name, name) == 0) {
memmove(nvtab.nameval+i, nvtab.nameval+i+1,
(nvtab.nval-(i+1)) * sizeof(Nameval));
nvtab.nval--;
return 1;
}
}
在ANSI C的标准库里定义了两个相关的函数:memcpy的速度快,但是如果源位置和目标位置重叠,它有可能覆盖掉存储区中的某些部分;memmove函数的速度可能慢些,但总能保证复制的正确完成。
单链表,构造表的最简单的办法就是把每个元素加在最前面。
我们无法在编译时初始化一个非空的表,这点也与数组不同。表应该完全是动态构造起来的。
二分检索完全不能适用于表。
/* apply: execute fn for each element of listp */
void apply(Nameval *listp, void (*fn)(Nameval*, void*), void *arg)
{
for ( ; listp != NULL; listp = listp->next)
(*fn)(listp, arg); /* call the function */
}
要使用apply,例如打印一个表的元素,我们可以写一个简单的函数,其参数包括一个格式描述串:
/* printnv: print name and value using format in arg */
void printnv(Nameval *p, void *arg)
{
char *fmt;
fmt = (char *) arg;
printf(fmt, p->name, p->value);
}
它的调用形式是:
apply(nvlist, printnv, “%s: %x"n”);
要销毁一个表就必须特别小心:
/* freeall: free all elements of listp */
void freeall(Nameval *listp)
{
Nameval *next;
for ( ; listp != NULL; listp = next) {
next = listp->next;
/* assumes name is freed elsewhere */
free(listp);
}
}
二叉树,树的构造,树的遍历。
常见做法是为每个散列值(或称桶)关联一个项的链表,即数组的每个元素是个链表,链接起具有该散列值的所有数据项。散列函数hash应该计算出什么东西。这个函数必须是确定性的,应该能算得很快,应该把数据均匀地散布到数组里。对于字符串,最常见的散列算法之一就是:逐个把字节加到已经构造的部分散列值的一个倍数上。乘法能把新字节在已有的值中散开来。这样,最后结果将是所有输入字节的一种彻底混合。根据经验,在对ASCII串的散列函数中,选择31和37作为乘数是很好的。
选择算法有几个步骤。首先,应参考所有可能的算法和数据结构,考虑程序将要处理的数据大概有多少。如果被处理数据的量不大,那么就选择最简单的技术。如果数据可能增长,请删掉那些不能对付大数据集合的东西。然后,如果有库或者语言本身的特征可以使用,就应该使用。如果没有,那么就写或者借用一个短的、简单的和容易理解的实现。如果实际测试说明它太慢,那么就需要改用某种更高级的技术。