KV-存储 之 Hash算法

生活中你应该遇到这样的事情: 你要去隔壁班级拿本书给某位同学, 不巧的是他当时不在,你也不知道他的座位, 你会选择问离你最近的同学,他坐哪. 他会毫不犹豫的回答你: 第几排第几个座位. 这是个很常见的例子, 但是里面却蕴含了极其伟大的Hash算法. 试想 那位同学能在近似常数时间内帮你指出你要知道的那位同学的位置是因为所有同学的位置他已经记在脑子中了(假设全部). 他不会去从第一排第一个位置慢慢去搜索.而是直接定位. ---- 哈希 又叫 散列

计算机中内存访问速度很快,假如有个64G大小的内存,我需要访问内存中的cache的一段, 想在O(1)时间内进行定位,就需要Hash的帮助。


继续通过上面的例子反面得出:

(1).要想进行Hash搜索 必须先记住每个元素的位置

(2).要想利用计算机存储器记住每个元素的位置,必须分配一定的空间来存储这些位置.

(3).要想知道每个元素的位置在哪,必须有个好的Hash算法进行计算每个元素的位置.

这三点可以说浓缩了Hash的精髓, 下面主要讲Hash算法.因为是它决定了Hash的好坏.


Hash算法(v1~v8为整数)


如图所示, v1~v8 ,8个值 通过Hash Alg 得出Hash 桶, 这里Hash Table就像个数组, v1在 索引 0 位置 ...v8在索引 7 位置. 以后 就可以通过0~7(key) 对其进行常数时间内的查找了.

这是一个比较简单的Hash,也是一个理想的Hash,为什么说是理想呢? 请看下面的这张图.

如图所示,假如有10W个值,甚至更大,按照前面的Hash算法就需要开辟10W个大小的空间(Hash Table).如果元素更多,开辟的空间就更多,显然这不是一个好算法,但是 这又是速度最快的算法,因为查询任意元素都在O(1)内. 这是个空间换时间的思想,但是空间也是有限的,如何折中这是个值得思考的问题.

当HashTable一定, 哈希的元素个数 <= HashTable的空间大小 , 则可以在有限的HashTable内 哈希的元素对应不同的索引.

当HashTable一定, 哈希的元素个数 > HashTable的空间大小, 则会有2个或多个以上的元素被映射到 相同的索引上. 如下图:

需要哈希的值v5 和 v9发生了冲突, v6和v10发生了冲突. 这个冲突就是我们要解决的,冲突的频率决定了一个Hash算法的好坏. 冲突越多,Hash算法效率越低.

其实冲突是不可避免的,比如说,刚才这个例子,我们需要存放10个元素, 而HashTable中只有8个, 理想情况是需要 10 个HashTable的位置 , 我们可以 对 8 进行取模H(v) = V mod 8 生成的结果会都在0~7之间.

这种Hash算法可以称为 取模Hash ,这种算法很简单,Hash速度很快, 但是 缺陷很大,1. 比较适合Key为整数情况 2. 如果HashTable的元素数目选的不够好 则容易造成分布不均匀,比如 HT(HashTable)的元素数目为 2 的倍数, 需要哈希的元素都为偶数,那么所有 !(X % 2 == 0) 的位置都为空. 为了避免这种情况,通常保证表的大小为素数.

如果Key为字符串,通常做法是把字符串中的字符的ASCII码加起来.然后把累加的值与HT的大小进行取模 .

#define HTSIZE 10007 //为素数 int htV (char* key) { int i = 0; int sum = 0; for (; key[i]!='\0'; i++) { sum += (int)key[i]; } return sum % htSize; }上面就是对字符串进行Hash的一段代码.但是这个Hash函数分布也不是很均匀,假设key最大为8字节长. char的最大值为127, 因此可以得出Hash函数能在0 ~ 127*8(1016) 之间获得. 冲突率非常高.继续看一段代码.

#define HTSIZE 10007 int htV_better (char* key) { u16 sum= 0; while (*key!='\0') { sum= (sum<<5) + *key++; } return sum % htSize; }

这段代码使用了Horner法则:

假设有n+2个实数a0,a1,…,an,和x的序列,要对多项式Pn(x)= anx ^n+a(n-1)x^(n-1)+…+a1x+a0求值,直接方法是对每一项分别求值,并把每一项求的值累加起来,这种方法十分低效,它需要进行n+(n-1)+…+1=n(n+1)/2次乘法运算和n次加法运算。有没有更高效的算法呢?答案是肯定的。通过如下变换我们可以得到一种快得多的算法,即Pn(x)= anx ^n+a(n-1)x^(n-1)+…+a1x+a0=((…(((anx +an-1)x+an-2)x+ an-3)…)x+a1)x+a0,这种求值的安排我们称为霍纳法则

下面是该讨论下解决Hash冲突的问题了,总结一下有下面几种解决Hash冲突的方法.

链地址算法:

这个是解决冲突最常见的方法,所谓开放是指每个桶都可以进行"开放", 通过把桶起冲突元素用链表链接在一起. 如下图. 这样空间比较随意


Index = 4 这个位置 ,有3个元素起了冲突,按照时间插入顺序依次排列.

检索过程其实就是Hash+List的过程.先定位到H(v5) 若是则返回.不是则继续读下一个.插入数据时,如果按照"顺序思维"需要一个元素一个元素的遍历下去直到下一个指针为NULL,虽然链表进行的是一个线性操作,但是如果冲突较少,也就是链表较短,效率也是很高的, 其实也可以考虑下链表的特性,我们可以插入在头部,这样插入就为O(1), 效率也就提高上去了. SGI STL中hash table用的就是这种算法。

再通过下面的图一步一步认识下 开放链地址法

Hash Table初始化为 13 个桶 (从0开始 0~12),Hash算法为对Hash table 取模

Step 1:

向hash Table中插入 1, 2, 3, 4, 5 这5个元素

插入位置分别为 1%13, 2%13, 3%13, 4%13, 5%13

Step 2:

向Hash Table中插入 13, 100

插入的位置为 13%13, 100%13


如图所示,进行模运算后 100位置在 第10个位置,那么现在我插入 9 再看下:


可以看出,在第10个位置生成了一个链表,元素位置是根据时间来入的,就像一个Stack!

现在我们来查找 100 这个元素:

------>形象的描述了 链表的遍历过程


开放定址算法

如果h(k)已经被占用,则按如下序列探查:(h(k)+p(1))%TSize, (h(k)+p(2))%TSize, …,(h(k)+p(i))%TSize,…

其中,h(k)为哈希函数,TSize为哈希表长,p(i)为探查函数。在 h(k)+p(i-1))%TSize的基础上,若发现冲突,则使用增量 p(i) 进行新的探测,直至无冲突出现为止。其中,根据探查函数p(i)的不同,开放定址法又分为线性探查法(p(i) = i : 1 , 2 , 3 , …),二次探查法(p(i)=(-1)i-1((i+1)/2)2: 12 , -12 , 22 , -22 …),随机探查法(p(i): 随机数 ),双散列函数法(双散列函数h(key) ,hp (key)若h(key)出现冲突,则再使用hp (key)求取散列地址。探查序列为:h(k), h(k)+ hp(k),…, h(k)+ i*hp(k)). --- <百科>

线性探测法:

p(i) = 1, 2, 3 (<TSize-1) 也就是 通项式为 p(i) = i

当发现冲突时 使用p(i)函数进行新的探测,每次移动一个元素,直到无冲突为止.来形象的看下具体过程:

初始化Hash为29个,取模Hash

Step 1: 插入 1


Step 2: 再插入 1

可以看到 1 被放置于 2 的位置,假如2的位置已经有2了 那么1会被放置在哪呢? 我们返回后再来看下结果。

1被放置了2的后面。这个根据(1+ p(2)) % 29 可以得出。

二次探查法

通项式为:p(i)= 1,1,4,-4,9,-9 (i*i,-i*i) [<Tsize/2]

我们插入1,1,1 来看下元素分配:


如果再插入两个1后插入一个5 再插入一个1呢,应该还是再次按照二次探查法来进行下一步的操作.


随机探查法

把p(i)从线性探测改成随机数(伪随机) p(i) = Random ,其它类似。


KV-存储

Hash在存储中运用,也就是KV(key-value)存储,由于Hash本身的限制,KV存储也只能支持Put, Get, Del几个原语操作。 但是也已经足够了。

KV存储实现不难,但是有一些东西需要注意,可以让效率提升一个阶级。

(1). Put操作时如果你进行实时写,效率很一般,但是你如果使用顺序读入内存再批量写入磁盘,速度会提升很多,也就是所谓的延迟写操作和批处理化,这点可以参看Google开源的LevelDB

http://code.google.com/p/leveldb/

LogTree算法

http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.44.2782

(2).Get操作建议使用mmap,因为文件映射就似直接读内存差不多(原理是把文件调入进程的地址空间,本博有详细分析)

(3).Del操作建议直接做标记,因为数据量大,不建议直接做真正删除操作,因为这样会很容易形成内存碎片. 当然如果你承认自己机制做的好的话可以进行直接删除,然后一定时候 进行碎片整理。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值