音视频开发之旅(29) 算法序列 - 散列表

目录

  1. 基本概念
  2. 散列表的构造
  3. 散列表的碰撞处理
  4. 代码实现
  5. 资料
  6. 收获

前面两篇我们学习实践的二叉查找树和平衡二叉树以及有序数组的二分查找、单链表的逐个查找,都需要进行比较和递归。那么有没有其他的查询技术呢?通过下图可以看到散列表我们还没有涉及,今天我们就来对散列表(哈希表)进行学习实践。


图片来源于《算法》

一、基本概念

散列技术,不需要比较和递归。通过记录存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)
把这种对应关系f称为散列函数,或者哈希函数。
采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或者哈希表(Hash table)

散列表的适用范围,针对一对一,有映射关系 查找效率比较高。一旦出现key重复的情况,查找效率就变低了。

散列表的查找步骤

  1. 存储时,通过散列函数计算出要存储的值对应的散列地址
  2. 当查找时,通过同样的散列函数要查找值的散列地址,并按照散列地址访问

二、散列表的构造方式

散列表的构造过程是 根据散列函数,把对应的key映射到对应的存储地址

散列函数的好坏直接影响散列表插入和查询的效率。

好的散列函数衡量的两个指标是 计算简单、分布均匀。

常见散列函数:有直接定值法、数字分析法、平法取中法、折叠法以及除留余数法等。除留余数法的效率一般情况下优于其他几种类型的散列函数,也是最常用的散列函数。这篇来学习实践通过除留余数法的方式构造散列表

对于散列表长度为m,其对应的除留余数法散列函数计算公式如下:
f(key) = key mod p. (p<=m)
这里的key就是要插入或者查询的值,mod是除留运算,p是取模的分母,一般p的值小于等于散列表的长度m。

a={12,25,38,15,16,29,78,67,56,21,22,47}
对于这个长为12数组,我们通过除留余数法进行散列表的构造
12 % 12 = 0
25 % 12 = 1
38 % 12 = 2
15 % 12 = 3
16 % 12 = 4
29 % 12 = 5
78 % 12 = 6
67 % 12 = 7
56 % 12 = 8
21 % 12 = 9
22 % 12 = 10 
47 % 12 = 11

上述数组是比较理想的情况,除留运算后没有出现冲突,但是如果把数组改成如下值就会出现碰撞的情况

a={12,24,38,15,16,29,78,67,50,21,22,47}
对于这个长为12数组,我们通过除留余数法进行散列表的构造
12 % 12 = 0
24 % 12 = 0 --》出现了碰撞
38 % 12 = 2
15 % 12 = 3
16 % 12 = 4
29 % 12 = 5
78 % 12 = 6
67 % 12 = 7
50 % 12 = 2 --》出现了碰撞
21 % 12 = 9
22 % 12 = 10 
47 % 12 = 11

这针对这种情况,我们该怎么处理呐?

三、散列表碰撞处理

一个散列函数能够将键转为数组索引,但是会出现两个或多个键的散列值相同的情况,即发生碰撞,处理碰撞的两种常用的方法是线性探测法和拉链法,下面我们一一学习实践。

3.1 线性探测(LinearProbingHashST)

用大小为M的数组保存N个键值对,其中M>N, 然后发生碰撞时,利用空位置进行处理。这类策略称为开放地址散列表,开放地址散列表最常见的方法就是线性探索法。
当碰撞发生时【当一个键的散列值f(k1),被另外一个不同的键k2占用】, 采用直接检查散列表中下一个位置,如果该位置没有被占用,则把k1存储在f(k1)+1的位置,否则继续+1探测,他对应的公式如下:
f(key) = (f(key)+d) MOD m. (D = 1,2,…,m-1)

我们来看下如何解决上一小节的碰撞问题

a={12,24,38,15,16,29,78,67,50,21,22,47}
对于这个长为12数组,我们通过除留余数法进行散列表的构造
12 % 12 = 0
24 % 12 = 0 --》出现了碰撞 ((24 % 12)+1)% 12 = 1
38 % 12 = 2
15 % 12 = 3
16 % 12 = 4
29 % 12 = 5
78 % 12 = 6
67 % 12 = 7
50 % 12 = 2 --》出现了碰撞 ((50 % 12)+1)% 12 = 3,但是散列值为3的位置已经被15占用了,我们继续+1 探测,一直加到6时 ((50 % 12)+6)% 12 = 8
21 % 12 = 9
22 % 12 = 10 
47 % 12 = 11

3.2 拉链法 (SeparateChainingHashST)

解决冲突的方式,处理上面介绍的开放寻址线形探测法外,还有一种比较常用的方式是拉链法:将大小为M的数组中的每个元素都指向一条链表,链表中的每个结点都存储了散列值为该元素索引的键值对。
这种方式的基本思想就是 选择足够大的数组M,使得所有链表都尽可能短,以保证高效的查询。
使用拉链法存储解决冲突,对应的查找分为两步

  1. 根据散列值找到对应的链表,
  2. 沿着链表顺序查找相应的键。


四、散列表代码实现

下面我们实现开放寻址线形探索方式解决碰撞,详见代码注释

#include<iostream>

const int DefaultSize = 128;
using std::cout;
using std::endl;
 
enum NodeInfo { Empty, Active };//记录节点的状态
 
template<typename T>
class HashTable
{
public:
    HashTable(int d,int sz= DefaultSize);
    ~HashTable();
    void printValue();
 
    bool search(const T& k);//查看键k是否存在
    bool insert(const T& k);//插入键k

 
private:
 
    //寻找键k在哈希表的存储数组中对应的位置

    int findPosByLinearProbing(const T& k)const;  //使用线性探查法

 
    int curSize; //curSize存储当前哈希表存储节点的数量
    int maxSize; //maxSize为哈希表最多存储的节点的数量

    NodeInfo* info;//存储每个节点的状态数组,作为辅助数组

    int mod;//散列函数的MOD

    T* value;//哈希表的存储节点的数组
};
 
 
template<typename K>
HashTable<K>::HashTable(int d, int sz)
{
    mod = d;
    maxSize = sz;
    curSize = 0;
    value = new K[maxSize];
    info = new NodeInfo[maxSize];

    for (int i = 0; i < maxSize; i++)//所有节点状态都设为空
        info[i] = Empty;
}
 
 template<typename K>
HashTable<K>::~HashTable()
{
    if (value) delete[] value;
    if (info) delete[] info;
}

template<typename K>
void HashTable<K>::printValue()
{
    for (int i = 0; i < maxSize; i++)
    {
        if (info[i] == Active) cout << value[i] << " ";
        else cout << 0 << " ";
    }
    cout << endl;
}
 
//查看键码k是否存在
template<typename K>
bool HashTable<K>::search(const K& k)
{

    
    if (findPosByLinearProbing(k) >= 0)
    {
        return true;
    }
        
    return false;
}
 
template<typename K>
bool HashTable<K>::insert(const K& k)
{
    if (findPosByLinearProbing(k) >= 0) return false;

    int i = k % mod;
    int j = i;//j作为移动的指针
    do
    {
        if (info[j] == Empty) {//当节点状态为空时,进行插入,否则j将一直移动下去
            value[j] = k;
            info[j] = Active;
            curSize++;
            return true;
        }
        else
            j = (j + 1) % maxSize;
    } while (j != i);
    return false;
}
 
 
//寻找键k在哈希表的存储数组中对应的位置,使用线性探测法
template<typename K>
int HashTable<K>::findPosByLinearProbing(const K& k) const
{
    int i = k % mod;
    if (info[i] == Empty) return -1;//如果info状态为空,那么该键码不存在哈希表中
    int j = i;//j作为移动的指针
    do
    {
        if (info[j] == Active && value[j] == k) return j;//如果节点转态为Active,且value数组在j处值为k
        j = (j + 1) % maxSize;//位移
    } while (j != i);
    return -1;
}
 

int main()
{
    HashTable<int> myhash(12, 12);
    int a[] = { 12,38,24,15,16,29,78,47,67,50,21,22};
    for (int i = 0; i < sizeof(a) / sizeof(int); i++)
    {
        myhash.insert(a[i]);
    }
        
    myhash.printValue();
    return 1;
}

我们可以看到散列表的代码相比二叉查找树、平衡二叉树更简单,当然相比二叉树,散列表需要设计散列函数。性能对比如下


图片来源于《算法》

关于查找算法我们就先学习实践到这里,从下一片我们继续CPP的其他学习,欢迎关注公众号“音视频开发之旅”,一起学习成长。

五、 资料

《算法》
[【C语言描述】《数据结构和算法》(小甲鱼)] : https://www.bilibili.com/video/BV1jW411K7yg?p=84

[5分钟学会最经典的数据结构哈希表,C语言手写实现,不要错过呀]
https://www.bilibili.com/video/BV1cX4y1K7Tk?from=search&seid=7155283358352562628

[数据结构与算法基础--第13周10--7.4散列表的查找6--7.4.4散列表的查找及性能分析] : https://www.bilibili.com/video/BV1ot411q7SM?from=search&seid=7155283358352562628

[散列表C++实现] : https://blog.csdn.net/candand_python/article/details/107505740

六、收获

  1. 了解了散列表的实现和原理
  2. 散列表解决冲突的常用方法
  3. 代码实现散列表查询

感谢你的阅读

算法系列到这篇就结束了,从下一篇开始我们继续CPP文件操作、异常处理、线程等相关知识的学习,欢迎关注公众号“音视频开发之旅”,一起学习成长。

另外从今天开始2周的假期,做个挑战的事情,每天写一篇公众号文章,主题会分为两块,“音视频开发之旅”相关的cpp和ffmpeg,以及android的一些开源项目源码解析系列。

欢迎交流

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