数据结构与算法分析 —— C 语言描述:分离链接法

分离链接法(separate chaining)是解决哈希冲突的一种简单方法,其做法是将散列到同一个值的所有元素保留在一个表中。为方便起见,这些表都有表头。如果空间很紧,则更可取的方法是避免使用这些表头。本文假设关键字是前 10 个完全平方数并设散列函数就是 Hash(X) =Xmod 10。(表的大小不是素数,用在这里是为了简单起见)。

为执行 Find ,我们使用散列函数来确定究竟考察哪个表。此时我们以通常的方式遍历该表并返回所找到的被查找项所在位置。为执行 Insert,我们遍历一个相应的表以检查该元素是否已经处在适当的位置(如果要插入重复元,那么通常要留出一个额外的域,这个域当重复元出现时增 1)。如果这个元素是个新的元素,那么或者插入到表的前端,或者插入到表的末尾,哪个容易就执行哪个。当编写程序的时候这是最容易寻找的一种。有时将元素插入到表的前端不仅因为方便,而且还因为新近插入的元素最有可能最先被访问。

实现分离链接法所需要的类型如下:

#ifndef _HashSep_H

struct ListNode;
typedef struct ListNode *Position;
struct HashTbl;
typedef struct HashTbl *HashTable;

HashTable InitializaTable( int TableSize );
void DestroyTable( HashTable H );
Postion Find( ElementType Key, HashTable H);
void Insert( ELementType Key, HashTable H );
ElementType Retrieve( Position P );
/* Routines such as Delete and MakeEmpty are omitted */

#endif /* _HashSep_H */

/* Place in the implementation file */
struct ListNode
{
    ElementType Element;
    Position Next;
};

typedef Position List;

/* List *TheList will be an array of lists, allocated later */
/* The lists use headers (for simplicity), though this wastes space */
struct HashTbl
{
    int TableSize;
    List *TheLists;
};

上面程式码中的 ListNode 结构与链式表声明相同。程式码中散列表结构包含一个链表数组(以及数组中的链表的个数),它们在散列表结构初始化时动态分配空间。此处的 HashTable 类型就是指向该结构的指针类型。

注意,TheList 域实际上是一个指向指向 ListNode 结构的指针的指针。如果不使用这些 typedef,那可能会相当混乱。

初始化函数如下:

HashTale
InitializaTable( int TableSize ) 
{
    HashTable H;
    int i;
/* 1 */ if(TableSize < MinTableSize)
/* 2 */ {
/* 3 */     Error("Table size too small")   ;
            return NULL;
        }
        /* Allocate table */
/* 4 */ H = malloc( sizeof( struct HashTbl ));
/* 5 */ if( H == NULL)
/* 6 */            FatalError( "Out of space!!!");

/* 7 */ H->TableSize = NextPrime( TableSize );

       /* Allocate array of lists */
/* 8 */ H->TheLists = malloc( sizeof(list)*H->TableSize);
/* 9 */ if(H->TheLists == NULL ) 
/* 10 */    FatalError("Out of space!!!");

      /* Allocate list headers */
/* 11 */for( i=0; i < H->TableSize; i++ )
        {
/* 12 */    H->TheLists[i] = malloc(sizeof( struct ListNode));
/* 13 */    if(H->TheLists[i] == NULL)
/* 14 */        FatalError("Out of space!!!"); 
            else
/* 15 */        H->TheLists[i]->Next = NULL;
        }
/* 16 */return H;
}

它用到与栈的数组实现中相同的想法。第 4~6 行给一个散列表分配空间。如果空间允许,则 H 将指向一个结构,该结构包含一个整数和指向一个表的指针。第 7 行设置表的大小为一素数,而第 8~10 行则试图指定 List 的一个数组。由于 List 被定义为一个指针,因此结果为指针的数组。

假如 List 的实现不用表头,那么我们就可以到此为止了。但是我们使用了表头,因此必须给每个表分配一个表头并设置它的 Next 域为 NULL。这由第 11~15 行实现。当然,第 12~15 行可以用语句

H->TheLists[i] = MakeEmpty();

代替。虽然我们没有选择使用这条语句,但是因为该例中它胜过使程序尽可能自包含,所以它当然值得考虑。这个程序的一个低效之处在于第 12 行上的 malloc 执行了 H→TableSize 次。这可以通过在循环出现之前调用一次 malloc 操作

H->TheLists=malloc(H->TableSize*sizeof(struct ListNode));

代替第 12 行来避免。第 16 行返回 H。

Find(Key, H) 的调用将返回一个指针,该指针指向包含 Key 的那个单元。实现它的程序如下:

Position
Find( ElementType Key, HashTable H)
{
        Postion P;
        List L;

/*1*/   L = H->TheLists[( Key, H->TableSize)];
/*2*/   P = L->Next;
/*3*/   while( P!= NULL && P->Element != key)
            /* Probably need strcmp!!*/
/*4*/       P = P->Next;
/*5*/   return P;
}

注意,第 2~5 行等同于链表执行的 Find 程序。记住,如果 ElementType 是一个字符串,那比较和赋值必须相应地使用 strcpm 和 strcpy 来进行。

下面是一个插入例程:

viod
Insert( ElementType Key, HashTable H)
{
        Position Pos, NewCell;
        List L;
/*1*/   Pos = Find(key, H);
/*2*/   if(Pos == NULL) /* Key is not found*/
        {
/*3*/       NewCell = malloc(sizeof(struct ListNode));
/*4*/       if(NewCell == NULL)
/*5*/           FatalError("Out of space!!!");
            else
            {
/*6*/           L = H->TheLists[Hash(Key, H->TableSize)];
/*7*/           NewCell->Next =  L->Next;
/*8*/           NewCell->ELement = key; /* Probably need strcpy! */
/*9*/           L->Next = NewCell;
            }
        }   
}

如果要插入的项已经存在,那么我们就什么也不做;否则我们把它放到表的前端。该元素可以放在表的任何地方,此处这样做是最方便的。注意,插入到表的前端的程序基本上等同于使用链表实现 Push 的程序。

上面的例程写得多少有些不好,因为它计算了两次散列函数。多余的计算总是不好的,因此,如果这些散列例程真的构成程序运行时间的重要部分,那么这个程序就应该重写。

删除例程是链表中的删除操作的直接实现,如果在散列的诸例程中不包括删除操作,那么最好不要使用表头,因为使用表头不仅不能简化问题而且还要浪费大量的空间。

除了链表外,任何的方案都有可能用来解决冲突现象 —— 一棵二叉查找树甚至另外一个散列表均可胜任,但是我们期望如果表大,同时散列函数好,那么所有的表就应该短,这样就不至于进行任何复杂的尝试了。

我们定义散列表的装载因子(load factor)\lambda 为散列表中元素个数与散列表大小的比值。在本文的例子中,\lambda=1.0。表的平均长度为 \lambda。执行一次查找所需要的工作是计算散列函数值所需要的常数时间加上遍历表所用的时间。再一次不成功的查找中,遍历的链接数平均为 \lambda(不包括最后的 NULL 链接)。成功的查找则需要遍历大约 1+(\frac{\lambda}2)个链接;它保证必然会遍历一个链接(因为查找是成功的),而我们也期望沿着一个表中途就能找到匹配的元素。这就指出,表的大小实际上并不重要,而装载因子才是重要的。分离链接散列的一般法则是使得表的大小尽量与预料的元素个数差不多(换句话说,让 \lambda \approx 1)。因此,使表的大小是素数以保证一个好的分布,这也是一个好的想法。

关于对素数取模的好处,我们首先要知道素数是什么?素数(质数)即只有两个因数,分别为 1 和其本身。如果对一个合数取模(有两个以上的因数),那么对其某个因数取模,结果可能仍然一致。例如 10 对 8 取模,结果为 2,对 4 取模,结果也为 2。而我们对一个素数取模,由于素数只有 1 和其本身两个素数,即不可能出现上述情形。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容