【程序设计实践】第2章 算法和数据结构

2章 算法和数据结构

即使是很复杂的程序,也是由简单的数组、表、散列表等简单东西堆砌起来的。

检索

顺序检索,二分检索

排序

快速排序

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串的散列函数中,选择3137作为乘数是很好的。

小结

选择算法有几个步骤。首先,应参考所有可能的算法和数据结构,考虑程序将要处理的数据大概有多少。如果被处理数据的量不大,那么就选择最简单的技术。如果数据可能增长,请删掉那些不能对付大数据集合的东西。然后,如果有库或者语言本身的特征可以使用,就应该使用。如果没有,那么就写或者借用一个短的、简单的和容易理解的实现。如果实际测试说明它太慢,那么就需要改用某种更高级的技术。


你可能感兴趣的:(程序设计实践)