数据结构(23)-散列表查找

定义

散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每一个关键字key对应一个存储位置f(key)

        存储位置 = $f(关键字)$

查找的时候,直接根据这个确定的对应关系找到存储位置。我们把这种对应关系f称为散列函数,也称为哈希(Hash)函数。

采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间成为散列表或者哈希表(Hash Table)。关键字对应的记录存储位置称之为散列地址或者哈希地址。

key1 ≠ key2,却存在f(key1) = f(key2),这种情况称之为散列冲突,或哈希冲突。

散列表查找步骤

整个散列过程其实就是两步:

  1. 在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录。
  2. 当查找记录时,通过同样的散列函数计算记录的散列地址,然后按照此地址访问对应的记录。

所以说,散列技术既是一种存储方法,也是一种查找方法。由于散列的记录之间不存在逻辑关系,只是有关键的关联,所以散列主要是面向查找的存储结构。

散列技术最适合的求解问题就是查找与给定值相等的记录。散列表不适用与一个关键字对应多条记录的查找;也不适用于范围查找。

散列函数的构造

什么才算是好的散列函数呢?原则有二:

  1. 计算简单。
  2. 散列地址分布均匀。这样既可保证存储空间的有效利用,并能减少处理冲突所耗费的时间。

下面,我们就来看看常见的散列函数构造方法

直接定址法

使用关键字的某个线性函数为散列地址:

f(key) = a * key + b (a,b均为常数)

适合查找表比较小且连续的情况。

数字分析法

如果关键字是位数较多的数字,比如说手机号码,那么极有可能前7位都是相同的,我们可以把后四位抽取出来作为散列地址。如果这个地址还是会出现冲突,可以对抽取出来的数字进行反转、左移、右移、或者前面得加等方式形成一个散列函数。数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布,即可使用该方法。

平方取中法

平方取中法,就是假设关键字是123,它的平方是15129,取中间3位就是512。一般适用于不知道关键字的分布,而且位数又不是很大的情况。

折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数不够也可),然后将这几部分叠加求和,并按照散列表表长,取后几位作为散列地址。比如关键字是9876543210,散列表长为3位,我们可以将其分为4组987 | 654 | 321 | 0,叠加求和987+654+321+0=1962,再取后3位为962。折叠法适合关键字位数较多的情况。

除留余数法

对于散列表长度为m的散列函数为:

 $f(key) = key mod p(p <= m)$ (mod)是取模的意思

从公式可以看出,此函数的关键就是选择合适的p。根据前人的经验来看,通常p取小于或等于表长(最好接近表长)的最小质数或者不包含小于20质因子的合数。

随机数法

选择一个随机数,取关键字的随机函数值作为它的散列地址。
f(key) = random(key)

适用于关键字的长度不等的情况。

总之,选择散列函数,可以从以下条件入手:

  • 计算散列地址
  • 关键字的长度
  • 散列表的大小
  • 关键字的分布情况
  • 记录查找的频率

处理散列冲突

开放定址法

开放定址法,就是一旦发生了冲突就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。公式如下:

    $f_i(key) = (f(key) + d_i) MOD m (d_i = 1,2,...m-1)$

比如,关键字集合为{12, 67, 56, 16, 25, 37, 22, 29, 15,47,48,34},表长为12,我们可以使用散列函数f(key) = key MOD 12。计算前5个数的时候,都没有冲突,可以直接存入:12对应0,25对应1,16对应4,67对应7,56对应8。然后当我们计算37的时候,发现37也对应1,而此时1的位置已经存储了25,这就发生了冲突。此时,我们就需要使用上面的公式,f(37) = (f(37) + 1) MOD 12 = 2,将37对应2。依次类推,出现冲突就更新d_i的值。等到48的时候,f(48) = 0 冲突,于是f(48) = (f(48) + 1) MOD 12 = 1,还是冲突,一直到f(48) = (f(48) + 6) MOD 12 = 6,此时才有空位,赶快存入。

再散列函数法

再散列函数法,就是准备多个散列函数,发生冲突的时候,就换一个散列函数就行计算。

链地址法

链地址法,将出现冲突的关键字存在一个链表中。如下图所示:

散列链地址法.png

这样出现冲突,我们只需要给单链表增加结点即可。链地址法的问题就是遍历链表会带来性能损耗。

公共溢出区法

专门增加一个溢出表,用来存储冲突的关键字。

公共溢出区.png

在查找时,对给定值通过散列函数计算出散列地址之后,先与基本表的相应位置进行对比,如果相等则查找成功;如果不相等,则到溢出表中顺序查找。

散列表的查找实现

#define T_ERROR -1
#define T_OK 1
#define HASH_TABLE_LENTH 12
#define NULL_ELEMENT -32768
typedef int TStatus;

typedef struct {
    int *contents;
    int count;
}HashTable;

TStatus initHashTable(HashTable *ht) {
    ht->count = HASH_TABLE_LENTH;
    ht->contents = (int *)malloc(HASH_TABLE_LENTH * sizeof(int));
    for (int i = 0; i < HASH_TABLE_LENTH; i++) {
        ht->contents[i] = NULL_ELEMENT;
    }
    return T_OK;
}

// 散列函数: 除留余数法
int hash(int key) {
    return key % HASH_TABLE_LENTH;
}

// 向散列表插入关键字
void insertHash(HashTable *ht, int key) {
    int hashAddress = hash(key);
    
    /// 不为空 则说明有冲突
    while (ht->contents[hashAddress] != NULL_ELEMENT) {
        // 开放定址法线性探测
        hashAddress = (hashAddress + 1) % HASH_TABLE_LENTH;
    }
    
    /// 有空位 直接插入
    ht->contents[hashAddress] = key;
}

// 从散列表中查找
TStatus searchHash(HashTable ht, int key, int *hashAddress) {
    *hashAddress = hash(key);
    // 通过散列地址循环查找散列表
    while (ht.contents[*hashAddress] != key) {
        *hashAddress = (*hashAddress + 1) % HASH_TABLE_LENTH;
        if (ht.contents[*hashAddress] == NULL_ELEMENT || *hashAddress == hash(key)) {
            // 将整个散列表循环了一遍还是未找到 说明关键字不存在
            return T_ERROR;
        }
    }
    return T_OK;
}

通过代码可以看出,如果没有冲突的话,散列表查找的时间复杂度为O(1)

参考文献

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