第9章 集合

集合 (set)是不同 成员的无序 汇集。对集合的基本操作包括检验成员资格、添加成员和删除成员。其他操作包括集合的并、交、差和对称差。给定两个集合s和t,并集s+t是包含s和t中所有成员的一个集合,交集s*t包含所有既出现在s中、也出现在t中成员,差集s-t包含所有仅出现在s中、而不出现在t中的成员,对称差集通常记作s/t,包含了所有仅出现在s或t其中之一的成员。

描述集合时,通常会用到全集 (universe),即所有可能成员的集合。例如,字符的集合通常关联到由256个八位字符码构成的全集。当确定了全集U时,可以定义集合s的补集,即U-s。

由Set接口提供的集合不依赖全集。该接口导出了操作集合成员的函数,但从不直接查看集合成员。在这方面,Set接口的设计类似于Table接口,都由客户程序提供函数来查看特定集合中成员的属性。

应用程序使用集合的方式,与使用表的方式非常相似。实际上,Set接口定义的集合与表类似:集合成员是键,而与键关联的值被忽略了。

9.1 接口

set.h

〉≡  
  #ifndef SET_INCLUDED  
  #define SET_INCLUDED  
 
  #define T Set_T  
  typedef struct T *T;  
 
 〈exported functions

 100〉  
 
  #undef T  
  #endif  

Set接口导出的函数分为四组:分配和释放、基本集合操作、集合遍历和接受集合操作数并返回新集合的操作,如集合并操作。前三组中的函数与Table接口中的函数类似。

Set_T实例由下列函数分配和释放:

exported functions

 100〉≡  
  extern T  Set_new (int hint, 
      int cmp(const void *x, const void *y), 
      unsigned hash(const void *x));  
  extern void Set_free(T *set);  

Set_new分配、初始化并返回一新的T实例。hint是对集合预期会包含的成员数目的一个估计,准确的hint值可能会提高性能,但任何非负值都是可接受的。cmp用来比较两个成员,hash用来将成员映射到无符号整数。给定两个成员x和y,cmp(x,y)针对x小于y、x等于y或x大于y的情形,必须分别返回小于零、等于零或大于零的整数。如果cmp(x,y)返回0,那么x和y中只有一个会出现在集合中,而且hash(x)必定等于hash(y)。Set_new可能引发Mem_Failed异常。

如果cmp为NULL函数指针,那么假定集合的成员为原子,如果x=y,那么两个成员x和y就是相等的。类似地,如果hash是NULL函数指针,Set_new会自行提供一个适合于原子的哈希函数。

Set_free释放*set并将其赋值为NULL指针。Set_free并不释放集合的成员,该工作可使用Set_map完成。如果传递给Set_free的set或*set为NULL指针,则是已检查的运行时错误。

基本的集合操作由下列函数提供:

exported functions

 100〉+≡  
  extern int   Set_length(T set);  
  extern int   Set_member(T set, const void *member);  
  extern void  Set_put   (T set, const void *member);  
  extern void *Set_remove(T set, const void *member);  

Set_length返回集合的 (cardinality),或其所包含成员的数目。如果member在set中,Set_member返回1,否则返回0。Set_put将member添加到set(如果member尚不在set中),Set_put可能引发Mem_Failed异常。Set_remove将member从set删除(如果set包含了member),并返回删除的成员(可能是一个不同于member的指针)。否则(即member不在set中),Set_remove什么都不做并返回NULL。传递给上述例程的set或member为NULL,则是已检查的运行时错误。

下列函数遍历一个集合中的所有成员。

exported functions

 100〉+≡  
  extern void   Set_map    (T set, 
      void apply(const void *member, void *cl), void *cl);  
  extern void **Set_toArray(T set, void *end);  

Set_map对集合的每个成员都调用apply。它会将成员本身和客户程序相关的指针cl传递给apply。它并不查看cl。请注意,不同于Table_map,apply不能改变集合的成员。如果传递给Set_map的apply或set为NULL,或apply调用Set_put或Set_remove来改变set的内容,则构成已检查的运行时错误。

Set_toArray返回一个指针,指向一个N+1个元素的数组,其中以任意顺序包含了集合的N个元素。end的值(通常是NULL指针)赋值给数组的第N+1个元素。Set_toArray可能引发Mem_Failed异常。客户程序必须释放返回的数组。传递给Set_toArray的set为NULL,则是已检查的运行时错误。

下列各函数

exported functions

 100〉+≡  
  extern T Set_union(T s, T t);  
  extern T Set_inter(T s, T t);  
  extern T Set_minus(T s, T t);  
  extern T Set_diff (T s, T t);  

执行本章开头描述的4个集合操作。Set_union返回s+t,Set_inter返回s*t,Set_minus返回s-t,Set_diff返回s/t。所有这4个函数都会创建并返回新的T实例并可能引发Mem_Failed异常。这些函数将为NULL的s或t解释为空集,但总是返回一个新的、非NULL的T实例。因而,Set_union(s,NULL)返回s的一个副本。对于上述各个函数,如果s和t都是NULL,或s和t都不是NULL,但二者的比较函数和哈希函数不同时,则构成已检查的运行时错误。即,此前调用Set_new创建s和t时,必须指定了相同的比较函数和哈希函数。

9.2 例子:交叉引用列表

xref输出其各个输入文件中标识符的交叉引用列表,这是很有用的,例如,可用于找到程序源文件中对特定标识符的所有引用。例如,

% xref xref.c getword.c

  
...  
FILE   getword.c: 6 
       xref.c: 18 43 72  
...  
c      getword.c: 7 8 9 10 11 16 19 22 27 34 35 
       xref.c: 141 142 144 147 148 
...  

输出表明,FILE用于getword.c的第6行和xref.c的第18行、第43行和第72行。类似地,c出现在getword.c中11个代码行,以及xref.c中的5个代码行。一个行号只列出一次,即使该标识符在这一行出现了多次。输出按排序次序列出了文件和行号。

如果程序没有参数,xref将输出标准输入中的标识符的交叉引用列表,并省略上述的样例输出中的文件名:

% cat xref.c getword.c | xref

  
...  
FILE 18 43 72 157  
...  
c 141 142 144 147 148 158 159 160 161 162 167 170 173 178 
185 186 ...  

xref的实现说明了如何协同使用集合和表。它建立了一个表,由标识符索引,而每个相关联的值则是另一个表,由文件名索引。表中的值是集合,包含了若干整数指针,指向标识符出现的行号。图9-1描述了这个结构,并给出了上述第一次输出后与标识符FILE相关的细节。在单一的顶层表中与FILE(在下文的代码中,即为标识符的值)相关联的值,是一个处于第二层的表Table_T,包含了两个键:表示getword.c和xref.c的原子。与这些键关联的值是Set_T实例,集合中保存的是整数指针,指向FILE出现的行号。在顶层表中,对于每个标识符,都有一个二级表;在每一个二级表中,每个键—值对中的值都是一个集合。

114

图9-1 交叉引用列表的数据结构

xref.c

〉≡  
 〈xref includes

 103〉  
 〈xref prototypes

 104〉  
 〈xref data

 105〉  
 〈xref functions

 102〉  

xref的main函数与wf的非常相似:它创建标识符表,然后处理文件名参数。它会分别打开每个文件,并用FILE指针、文件名和标识符表作为参数,来调用xref函数。如果没有参数,则调用xref时,传递的参数是NULL文件名指针、对应于标准输入的FILE指针和标识符表:

xref functions

 102〉≡  
  int main(int argc, char *argv[]) {  
      int i;  
      Table_T identifiers = Table_new(0, NULL, NULL);  
 
      for (i = 1; i < argc; i++) {  
          FILE *fp = fopen(argv[i], "r");  
          if (fp == NULL) {  
              fprintf(stderr, "%s: can't open '%s' (%s)\n", 
                  argv[0], argv[i], strerror(errno));  
              return EXIT_FAILURE;  
          } else {  
              xref(argv[i], fp, identifiers);  
              fclose(fp);  
          }  
      }  
      if (argc == 1) xref(NULL, stdin, identifiers);  
     〈print the identifiers

 103〉  
      return EXIT_SUCCESS;  
  }  
 
〈xref includes

 103〉≡  
  #include <stdio.h>  
  #include <stdlib.h> 
  #include <errno.h>  
  #include "table.h"  

xref会建立一个复杂的数据结构,如果首先考察如何输出该数据结构的内容(通过遍历其各个组成部分),那么就更容易理解如何建立该数据结构。编写独立的代码块或函数来分别处理数据结构的各个组成部分,有助于读者理解遍历过程的细节。

第一步建立标识符及其值(二级表)的数组,并按标识符对数组排序,然后遍历数组调用另一个函数print来处理各个二级表。这个步骤与wf的代码块<print the words 103>很相似。

print the identifiers

 103〉≡  
  {  
      int i;  
      void **array = Table_toArray(identifiers, NULL);  
      qsort(array, Table_length(identifiers), 
          2*sizeof (*array), compare);  
      for (i = 0; array[i]; i += 2) {  
          printf("%s", (char *)array[i]);  
          print(array[i+1]);  
      }  
      FREE(array);  
  }  

identifiers中的各个键是原子,因此传递给标准库函数qsort的比较函数compare,与wf中使用的compare是相同的,都使用strcmp来比较一对标识符(8.2节解释了qsort的参数):

xref functions

 102〉+≡  
  int compare(const void *x, const void *y) {  
      return strcmp(*(char **)x, *(char **)y); 
  }  
 
〈xref includes

 103〉+≡  
  #include <string.h>  
 
〈xref prototypes

 104〉≡  
  int compare(const void *x, const void *y);  

identifiers中的每个值都是另一个表,将传递给print函数。这个表中的键是表示文件名的原子,因此可以使用与上文类似的代码,将键和值导出到一个数组中排序并遍历。

xref functions

 102〉+≡  
  void print(Table_T files) {  
      int i;  
      void **array = Table_toArray(files, NULL);  
 
      qsort(array, Table_length(files), 2*sizeof (*array), 
          compare);  
      for (i = 0; array[i]; i += 2) {  
          if (*(char *)array[i] != '\0')  
              printf("\t%s:", (char *)array[i]);  
         〈print the line numbers in the set

 array[i+1] 104〉  
          printf("\n");  
      }  
      FREE(array);  
  }  
〈xref prototypes

 104〉+≡  
  void print(Table_T);  

print可以使用compare,因为相关的键只是字符串。如果没有文件名参数,那么传递给print的每个表都只有一个项,其键是一个零长度的原子。print使用该约定,来避免在输出行号列表之前输出文件名。

传递给print的表中,每个值都是一个行号的集合。因为Set实现了指针的集合,xref用指向整数的指针来表示行号,并将这些指针添加到集合中。为输出它们,程序调用Set_toArray构建并返回一个整数指针的数组,以NULL指针结尾,然后排序该数组并输出整数行号:

print the line numbers in the set

 array[i+1] 104〉≡  
  {  
      int j;  
      void **lines = Set_toArray(array[i+1], NULL);  
      qsort(lines, Set_length(array[i+1]), sizeof (*lines), 
          cmpint);  
      for (j = 0; lines[j]; j++)  
          printf(" %d", *(int *)lines[j]);  
      FREE(lines);  
}  

cmpint与compare类似,但其参数为两个指向整数的双重指针,通过比较整数值来返回结果:

xref functions

 102〉+≡  
  int cmpint(const void *x, const void *y) {  
      if (**(int **)x < **(int **)y)  
          return -1;  
      else if (**(int **)x > **(int **)y)  
          return +1;  
      else  
          return 0;  
  }  
 
〈xref prototypes

 104〉+≡  
  int cmpint(const void *x, const void *y);  

xref建立上述代码输出的数据结构时,使用的是此前讨论过的代码。它使用getword从输入读取标识符。对于每个标识符,程序从数据结构中找到对应的集合,并将当前行号添加到集合中:

xref functions

 102〉+≡  
  void xref(const char *name, FILE *fp, 
          Table_T identifiers){  
      char buf[128];  
 
      if (name == NULL)  
          name = "";  
      name = Atom_string(name);  
      linenum = 1;  
      while (getword(fp, buf, sizeof buf, first, rest)) {  
          Set_T set;  
          Table_T files;  
          const char *id = Atom_string(buf);  
         〈files ← file table in

 identifiers associated with

 id 106〉  
         〈set ← set in

 files associated with

 name 106〉  
         〈add

 linenum to

 set, if necessary

 107〉  
      }  
  }  
 
〈xref includes

 103〉+≡  
  #include "atom.h"  
  #include "set.h"  
  #include "mem.h"  
  #include "getword.h"  
 
〈xref prototypes

 104〉+≡  
  void xref(const char *, FILE *, Table_T);  

linenum是一个全局变量,每次first遇到一个换行符时,都将linenum加1,first是传递给getword的函数指针参数,用于识别标识符的首字母:

xref data

 105〉≡  
  int linenum;  
 
〈xref functions

 102〉+≡  
  int first(int c) {  
      if (c == '\n')  
          linenum++;  
      return isalpha(c) || c == '_';  
  }  
 
  int rest(int c) {  
      return isalpha(c) || c == '_' || isdigit(c);  
  }  
 
〈xref includes

 103〉+≡  
  #include <ctype.h>  

getword以及传递给它的first和rest函数,已经在8.2节描述过。

xref prototypes

 104〉+≡  
  int first(int c);  
  int rest (int c);  

穿过两层表以找到适当集合的代码,必须处理数据结构中某些部分缺失的问题。例如,在第一次遇到某个标识符时,identifiers表中没有对应的项,因此代码需要创建文件表(下面代码中的files表),并将“标识符—文件表”对(下面代码中的id和files)即时添加到identifiers表:

〈files ← file table in

 identifiers associated with

 id 106〉≡  
  files = Table_get(identifiers, id);  
  if (files == NULL) {  
      files = Table_new(0, NULL, NULL);  
      Table_put(identifiers, id, files);  
  }  

类似地,在一个新文件中第一次遇到某个标识符时,行号的集合尚不存在,因此需要创建一个新集合并将其添加到files表:

〈set ← set in

 files associated with

 name 106〉≡  
  set = Table_get(files, name);  
  if (set == NULL) {  
      set = Set_new(0, intcmp, inthash);  
      Table_put(files, name, set);  
  }  

这些集合的成员是指向整数的指针,intcmp和inthash比较并散列整数值。intcmp类似上文的cmpint,但其参数是集合中的指针,因此它可以调用cmpint。可以直接用整数本身作为其哈希码:

xref functions

 102〉+≡  
  int intcmp(const void *x, const void *y) {  
      return cmpint(&x, &y); 
  }  
 
  unsigned inthash(const void *x) { 
      return *(int *)x; 
  }  
 
〈xref prototypes

 104〉+≡  
  int      intcmp (const void *x, const void *y);  
  unsigned inthash(const void *x);  

在控制到达代码块<add linenum to set, if necessary 107>时,set就是应该插入当前行号的目标集合。插入操作由下述代码完成:

int *p; 
NEW(p); 
*p = linenum; 
Set_put(set, p);  

但如果set已经包含linenum,该代码将产生内存泄漏,因为指向新分配内存空间的指针不会添加集合中 [1] 。仅当linenum不在set中时才分配内存空间,即可避免内存泄漏。

add

 linenum to

 set, if necessary

 107〉≡  
  {  
      int *p = &linenum;  
      if (!Set_member(set, p)) {  
          NEW(p);  
          *p = linenum;  
          Set_put(set, p);  
      }  
  }  

9.3 实现

Set接口的实现与Table的实现非常相似。它用哈希表表示集合,并使用比较函数和哈希函数在表中定位成员。章后的习题,针对下述实现和Table接口的实现,探讨了一些可行的备选方案。

set.c

〉≡  
  #include <limits.h>  
  #include <stddef.h>  
  #include "mem.h"  
  #include "assert.h"  
  #include "arith.h"  
  #include "set.h"  
  #define T Set_T  
 
 〈types

 108〉  
 〈static functions

 108〉  
 〈functions

 108〉  

Set_T是一个哈希表,其中通过链表来保存集合的成员:

types

 108〉≡  
  struct T {  
      int length;  
      unsigned timestamp;  
      int (*cmp)(const void *x, const void *y);  
      unsigned (*hash)(const void *x);  
      int size;  
      struct member {  
          struct member *link;  
          const void *member;  
      } **buckets;  
  };  

length是集合中成员的数目,timestamp用于实现Set_map中的已检查的运行时错误,即禁止apply修改集合,cmp和hash分别指向比较函数和哈希函数。

类似Table_new,Set_new会为buckets数组计算一个适当的容量,并将容量值记录在size字段中,并分配struct T实例和buckets数组所需的空间:

functions

 108〉≡  
  T Set_new(int hint, 
      int cmp(const void *x, const void *y), 
      unsigned hash(const void *x)) {  
      T set;  
      int i;  
      static int primes[] = { 509, 509, 1021, 2053, 4093,  
          8191, 16381, 32771, 65521, INT_MAX };  
 
      assert(hint >= 0);  
      for (i = 1; primes[i] < hint; i++) 
          ;  
      set = ALLOC(sizeof (*set) + 
          primes[i-1]*sizeof (set->buckets[0]));  
      set->size = primes[i-1];  
      set->cmp  = cmp  ? cmp : cmpatom;  
      set->hash = hash ? hash : hashatom;  
      set->buckets = (struct member **)(set + 1);  
      for (i = 0; i < set->size; i++)  
          set->buckets[i] = NULL;  
      set->length = 0;  
      set->timestamp = 0;  
      return set;  
  }  

Set_new使用hint选择primes中的一个值,作为buckets数组的容量(参见8.3节)。如果成员是原子(cmp或者hash是NULL函数指针,即表明这一点),Set_new将使用下列比较和哈希函数,这与Table_new使用的函数是相同的。

static functions

 108〉≡  
  static int cmpatom(const void *x, const void *y) { 
      return x != y; 
  }  
 
  static unsigned hashatom(const void *x) { 
      return (unsigned long)x>>2; 
  }  

9.3.1 成员操作

检验成员资格类似在表中查找键:散列所检验的成员,并搜索buckets中与散列值对应的链表

functions

 108〉+≡  
  int Set_member(T set, const void *member) {  
       int i;  
       struct member *p;  
 
       assert(set);  
       assert(member);  
      〈search

 set for

 member 109〉  
       return p != NULL;  
  }  
 
〈search

 set for

 member 109〉≡  
  i = (*set->hash)(member)%set->size;  
  for (p = set->buckets[i]; p; p = p->link)  
      if ((*set->cmp)(member, p->member) == 0)  
          break;  

如果搜索取得成功,那么p不是NULL,否则为NULL,因此检验p即可确定Set_member的结果。

添加一个新成员的过程类似:搜索集合查找该成员,如果搜索失败则添加它。

functions

 108〉+≡  
  void Set_put(T set, const void *member) {  
      int i;  
      struct member *p;  
 
      assert(set);  
      assert(member);  
     〈search

 set for

 member 109〉  
      if (p == NULL) {  
         〈add

 member t

o set 109〉  
      } else  
          p->member = member;  
      set->timestamp++;  
  }  
 
〈add

 member to

 set 109〉≡  
  NEW(p);  
  p->member = member;  
  p->link = set->buckets[i];  
  set->buckets[i] = p;  
  set->length++;  

timestamp用于Set_map中,以强制实施已检查的运行时错误。

Set_remove会删除一个成员,该函数使用一个指向member结构的双重指针pp遍历适当的哈希链,直至*pp为NULL或(*pp)->member即为我们感兴趣的成员,后一种情况下,下述代码中的赋值操作*pp=(*pp)->link即可从哈希链中删除该成员。

functions

 108〉+≡  
  void *Set_remove(T set, const void *member) {  
      int i;  
      struct member **pp;  
 
      assert(set);  
      assert(member);  
      set->timestamp++;  
      i = (*set->hash)(member)%set->size;  
      for (pp = &set->buckets[i]; *pp; pp = &(*pp)->link)  
          if ((*set->cmp)(member, (*pp)->member) == 0) {  
                struct member *p = *pp;  
                *pp = p->link;  
                member = p->member;  
                FREE(p);  
                set->length--;  
                return (void *)member;  
          }  
      return NULL;  
  }  

使用pp遍历哈希链,这使用了与Table_remove相同的惯用法,请参见8.3节。

Set_remove和Set_put通过将集合的length字段减1和加1来跟踪集合中成员的数目,Set_length函数会返回该字段:

functions

 108〉+≡  
  int Set_length(T set) {  
      assert(set);  
      return set->length;  
  }  

如果集合是非空的,Set_free首先必须遍历各个哈希链,释放其中的member结构实例,然后才能释放集合本身并将*set清零。

functions

 108〉+≡  
  void Set_free(T *set) {  
      assert(set && *set);  
      if ((*set)->length > 0) {  
      int i;  
      struct member *p, *q;  
      for (i = 0; i < (*set)->size; i++)  
          for (p = (*set)->buckets[i]; p; p = q) {  
              q = p->link;  
              FREE(p);  
              }  
      }  
      FREE(*set);  
  }  

Set_map与Table_map几乎相同:它会遍历各个哈希链,并对每个成员调用apply。

functions

 108〉+≡  
  void Set_map(T set,  
      void apply(const void *member, void *cl), void *cl) {  
      int i;  
      unsigned stamp;  
      struct member *p;  
 
      assert(set);  
      assert(apply);  
      stamp = set->timestamp;  
      for (i = 0; i < set->size; i++)  
          for (p = set->buckets[i]; p; p = p->link) {  
              apply(p->member, cl);  
              assert(set->timestamp == stamp);  
          }  
  }  

一个差别是,Set_map将每个成员(而不是指向每个成员的指针)传递给apply,因此apply不能改变集合中的指针。但它仍然可以通过转换,来修改这些成员所指向的值,这会破坏集合的语义。

Set_toArray比Table_toArray简单,类似List_toArray,它只需分配一个数组并将集合的成员复制到数组中:

functions

 108〉+≡  
  void **Set_toArray(T set, void *end) {  
      int i, j = 0;  
      void **array;  
      struct member *p;  
 
      assert(set);  
      array = ALLOC((set->length + 1)*sizeof (*array));  
      for (i = 0; i < set->size; i++)  
          for (p = set->buckets[i]; p; p = p->link)  
              array[j++] = (void *)p->member;  
      array[j] = end;  
      return array;  
  }  

p->member必须从const void*转换为void*,因为数组并未声明为常量。

9.3.2 集合操作

所有4个集合操作的实现都是类似的。例如,s + t通过将s和t的每个成员都添加到一个新的集合来实现,实现时可以首先建立s的一个副本,而后将t的各个成员都添加到副本中(如果尚未包含在该集合中的话):

functions

 108〉+≡  
  T Set_union(T s, T t) {  
      if (s == NULL) {  
          assert(t);  
          return copy(t, t->size);  
      } else if (t == NULL)  
          return copy(s, s->size);  
      else {  
          T set = copy(s, Arith_max(s->size, t->size));  
          assert(s->cmp == t->cmp && s->hash == t->hash);  
          {〈for each member

 q in

 t 112〉  
              Set_put(set, q->member);  
          }  
          return set;  
      }  
  }  
 
〈for each member

 q in

 t 112〉≡  
  int i;  
  struct member *q;  
  for (i = 0; i < t->size; i++)  
      for (q = t->buckets[i]; q; q = q->link)  

内部函数copy返回其参数的一个副本,参数必须不能为NULL。

static functions

 108〉+≡  
  static T copy(T t, int hint) {  
      T set;  
 
      assert(t);  
      set = Set_new(hint, t->cmp, t->hash);  
      {〈for each member

 q in

 t 112〉  
          〈add

 q->member to

 set 112〉  
      }  
      return set;  
  }  
 
〈add

 q->member to

 set 112〉≡  
  {  
      struct member *p;  
      const void *member = q->member;  
      int i = (*set->hash)(member)%set->size;  
     〈add

 member to

 set 109〉  
}  

Set_union和copy都可以访问特许信息:它们都知道集合的表示,因而可以通过向Set_new传递适当的hint值,来为新的集合指定哈希表的大小。Set_union在建立s的副本时需要提供hint,它使用s或t中较大的哈希表的容量,因为结果集合包含的成员数目,至少等于Set_union中最大的参数集合的成员数。copy可以调用Set_put将每个成员添加到副本集合中,但它使用<add q->member to set 155>代码块,这使得添加操作更为直接,避免了Set_put中不必要的搜索步骤。

交集操作s*t,将利用s或t中较小的哈希表创建一个新的集合,仅当某个成员同时出现在s和t中时,才将其添加到新的集合:

functions

 108〉+≡  
  T Set_inter(T s, T t) {  
      if (s == NULL) {  
          assert(t);  
          return Set_new(t->size, t->cmp, t->hash);  
      } else if (t == NULL)  
          return Set_new(s->size, s->cmp, s->hash);  
      else if (s->length < t->length)  
          return Set_inter(t, s);  
      else {  
          T set = Set_new(Arith_min(s->size, t->size), 
              s->cmp, s->hash);  
          assert(s->cmp == t->cmp && s->hash == t->hash);  
          {〈for each member

 q in

 t 112〉  
               if (Set_member(s, q->member))  
                  〈add

 q->member to

 set 112〉  
          }  
          return set;  
      }  
  }  

如果s成员数比t少,那么Set_inter将在调换s和t之后,递归调用自身 [2] 。这使得最后一个else子句中的for循环将遍历较小的集合。

差集操作s-t将创建一个新集合,并将s中那些不属于t的成员添加到新集合中。下述代码调换 了参数的名称,以便使用代码块<for each member q in t 112>来遍历s:

functions

 108〉+≡  
  T Set_minus(T t, T s) {  
      if (t == NULL){  
          assert(s);  
          return Set_new(s->size, s->cmp, s->hash);  
      } else if (s == NULL)  
          return copy(t, t->size);  
      else {  
          T set = Set_new(Arith_min(s->size, t->size), 
              s->cmp, s->hash);  
          assert(s->cmp == t->cmp && s->hash == t->hash);  
          {〈for each member

 q in

 t 112〉  
                if (!Set_member(s, q->member))  
                   〈add

 q->member to

 set 112〉  
          }  
          return set;  
      }  
  }  

对称差操作s/t创建的集合中,其成员只出现在s或t其中一个集合中,而不会同时出现在s和t中。如果s或t是空集,那么s/t等于t或s。否则,s/t等于(s-t)+(t-s),即首先遍历s,将不在t中的各个成员添加到新集合中,而后遍历t,将不在s中的各个成员添加到新集合。代码块<for each member q in t 112>可以用于这两次遍历,只需要在两次遍历之间切换s和t的值即可:

functions

 108〉+≡  
  T Set_diff(T s, T t) {  
      if (s == NULL) {  
          assert(t);  
          return copy(t, t->size);  
      } else if (t == NULL)  
          return copy(s, s->size);  
      else {  
          T set = Set_new(Arith_min(s->size, t->size), 
              s->cmp, s->hash);  
          assert(s->cmp == t->cmp && s->hash == t->hash);  
          {〈for each member

 q in

 t 112〉  
               if (!Set_member(s, q->member))  
                  〈add

 q->member to

 set 112〉  
          }  
          { T u = t; t = s; s = u; }  
          {〈for each member

 q in

 t 112〉  
               if (!Set_member(s, q->member))  
                  〈add

 q->member to

 set 112〉  
          }  
          return set;  
      }  
  }  

这4个操作的更高效实现是可能的,习题探讨了其中一些方案。一个特例是当s和t中的哈希表容量相同时,这对一些应用程序可能是比较重要的,参见习题9.7。

9.4 扩展阅读

Set接口导出的集合模仿了Icon[Griswold and Griswold,1990]中的集合,其实现也类似于Icon[Griswold and Griswold,1986]中的实现。对于固定的、较小的全集来说,通常使用位向量来表示集合,第13章描述了使用该方法的一个接口。

Icon是将集合作为内建数据类型的少数语言之一。集合是SETL中的中心数据类型,其大部分运算符和控制结构都用来操作集合。

9.5 习题

9.1 使用Table接口实现Set接口。

9.2 使用Set接口实现Table接口。

9.3 Set和Table接口的实现有许多共同之处。设计并实现另一个接口,提炼出二者的共同特性。该接口的目的是,支持类似集合和表的ADT的实现。使用你的新接口,重新使用Set和Table接口。

9.4 为 (bag)设计一个接口。包类似集合,但其成员可以出现多次。例如,{1 2 3}是一个整数集合,而{1 1 2 2 3}是一个整数包。使用前一道习题中设计的支持接口,来实现本接口。

9.5 copy在创建其参数集合的副本时,每次复制一个成员。因为它知道副本中成员的数目,因此可以一次性地分配所有的member结构实例,然后在填充副本集合时将这些member实例添加到适当的哈希链。实现该方案并测量其好处。

9.6 通过在member结构中存储哈希码,可能使一些集合操作更高效,这样对每个成员只需调用一次hash,仅当哈希码相等时才需要调用比较函数。分析此项改进预期会节省的时间,如果看起来值得,那么实现该改进并测量改进后的结果。

9.7 当s和t中哈希桶数目相同时,s+t相当于位于相同哈希链上的各个子集的并集。即s+t中的每个哈希链,是s和t中对应哈希链上的成员的并集。这种情况经常发生,因为许多应用程序在调用Set_new时指定了相同的hint。改变s + t、s * t、s - t和s / t的实现,以检测这种情况,并对其使用更简单、更高效的实现。

9.8 如果一个标识符出现在几个连续行中,xref将输出每一个行号。例如:

c        getword.c: 7 8 9 10 11 16 19 22 27 34 35  

修改xref.c,使之将多个连续行号替换为一个行范围:

c        getword.c: 7-11 16 19 22 27 34-35  

9.9 xref分配大量内存,但仅释放Table_toArray创建的数组。修改xref,使之最终释放它分配的所有东西(当然,原子除外)。在数据结构输出时,很容易增量式地完成释放工作。使用习题5.5的答案,来确认是否释放了所有分配的东西。

9.10 解释cmpint和intcmp为何使用显式比较来比较整数,而不是返回二者相减的结果。即cmpint的下述版本看起来简单得多,它有什么问题呢?

int cmpint(const void *x, const void *y) { 
    return **(int **)x - **(int **)y; 
} 

[1] 应当是集合,不是表。——译者注

[2] 其实可以直接调换s和t,不必要递归。——译者注