Hash表(Hash Table)

 

hash表实际上由size个的桶组成一个桶数组table[0...size-1] 。

当一个对象经过哈希之后。得到一个对应的value , 于是我们把这个对象放到桶table[ value ]中。当一个桶中有多个对象时。我们把桶中的对象组织成为一个链表。

这在冲突处理上称之为拉链法。

负载因子(load factor)

 

如果一个hash表中桶的个数为 size , 存储的元素个数为used .则我们称 used / size 为负载因子loadFactor
. 一般的情况下,当loadFactor<=1时,hash表查找的期望复杂度为O(1). 因此。每次往hash表中加入元素时。我们必须保证是在loadFactor <1的情况下,才可以加入。

容量扩张(Expand)& 分摊转移

 

当我们加入一个新元素时。一旦loadFactor大于等于1了,我们不能单纯的往hash表里边加入元素。

由于加入完之后,loadFactor将大于1,这样也就不能保证查找的期望时间复杂度为常数级了。这时。我们应该对桶数组进行一次容量扩张,让size增大 。

这样就能保证加入元素后 used / size 仍然小于等于1 , 从而保证查找的期望时间复杂度为O(1).可是。怎样进行容量扩张呢? C++中的vector的容量扩张是一种好方法。

于是有了例如以下思路 : Hash表中每次发现loadFactor==1时,就开辟一个原来桶数组的两倍空间(称为新桶数组),然后把原来的桶数组中元素所有转移过来到新的桶数组中。注意这里转移是须要元素一个个又一次哈希到新桶中的。原因后面会讲到。

这样的方法的缺点是,容量扩张是一次完毕的,期间要花非常长时间一次转移hash表中的全部元素。这样在hash表中loadFactor==1时。往里边插入一个元素将会等候非常长的时间。

redis中的dict.c中的设计思路是用两个hash表来进行进行扩容和转移的工作:当从第一个hash表的loadFactor=1时,假设要往字典里插入一个元素。首先为第二个hash表开辟2倍第一个hash表的容量。同一时候将第一个hash表的一个非空桶中元素所有转移到第二个hash表中。然后把待插入元素存储到第二个hash表里。继续往字典里插入第二个元素,又会将第一个hash表的一个非空桶中元素所有转移到第二个hash表中,然后把元素存储到第二个hash表里……直到第一个hash表为空。

这样的策略就把第一个hash表全部元素的转移分摊为多次转移,并且每次转移的期望时间复杂度为O(1)。

这样就不会出现某一次往字典中插入元素要等候非常长时间的情况了。

为了更深入的理解这个过程。先看看在dict.h中的两个结构体:

typedef struct dictht {

    dictEntry **table;

    unsigned long size;

    unsigned long sizemask;

    unsigned long used;

} dictht;



typedef struct dict {

    dictType *type;

    void *privdata;

    dictht ht[2];

    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    int iterators; /* number of iterators currently running */

} dict;

dictht指的就是上面说的桶数组,size用来表示容量,一般为2^n
,sizemask(一般为2^n-1,二进制表示为n个1)用来对哈希值取模 , used表示hash表中存储了多少个元素。

dict表示字典,由两个桶数组组成。type是一些函数指针(哈希函数及key。value的一些处理函数)。

d->rehashidx

 

这个变量的理解非常关键:

d->rehashidx 表明了新元素究竟是存储到桶数组0中。还是桶数组1中,同一时候指明了d->h[0]中究竟是哪一个桶转移到d->h[1]中。

当d->rehashidx==-1时,这时新加入的元素应该存储在桶数组0里边。

当d->rehashidx!=-1 时,表示应该将桶数组0中的第一个非空桶元素所有转移到桶数组1中来(中,由于d->h[1]->sizemask已经不同于d->h[0]->sizemask了。

这时新加入的元素应该存储在桶数组1里边,由于此刻的桶数组0的loadFactor为1
。而桶数组1的loadFactor小于1

当发现桶数组0中的元素所有都转移到桶数组1中,即桶数组0为空时。释放桶数组0的空间。把桶数组0的指针指向桶数组1。将d->rehashidx赋值为-1
,这样桶数组1就空了,下次加入元素时。仍然加入到桶数组0中。直到桶数组0的元素个数超过桶的个数,我们又又一次开辟桶数组0的2倍空间给桶数组1
,同一时候改动d->rehashidx=0。这样下次加入元素是就加入到桶数组1中去了。

值得注意的是。在每次删除、查找、替换操作进行之前,依据d->rehashidx的状态来推断是否须要进行桶转移。这能够加快转移速度。

以下是一份精简的伪代码,通过依次插入element[1..n]这n个元素到dict来具体描写叙述容量扩张及转移的过程:

//初始化两个hash表

d->h[0].size = 4 ; d->h[1].used = 0 ;  //分配四个空桶

d->h[1].size = 0 ; d->h[1].used = 0 ;  //初始化一个空表

 

for(i = 1 ; i <= n ; ++ i){

      if( d->rehashidx !=-1 ){

                  if(d->h[0]->used != 0){

                            把 d->h[0]中一个非空桶元素转移(又一次hash)到 d->h[1]中  。  

                            // 上一步会使得:

                            // d->h[0]->used -= 转移的元素个数 

                            // d->h[1]->used += 转移的元素个数 。

                            把 element[i] 哈希到 d->h[1]中  ;  // d->h[1]->used ++ 

                  }else{

                            //用桶数组1覆盖桶数组0; 赋值前要释放d->h[0]的空间,赋值后重置d->h[1])

                            d->h[0] = d->h[1] ; 

                            d->rehashidx = -1 ; 

                            把element[i]哈希到d->h[0]中;// d->h[0]->used ++ ; 

                 }

      }else if( d->h[0]->used >= d->h[0]->size )

                d->h[1] = new bucket[2*d->h[0]->size ];    

                // d->h[0]->size 等于d->h[0]->size的2倍 

                把element[i]哈希到d->h[1]中 ;  // d->h[1]->used ++ 

                d->rehashidx = 0 ;                             

      }else{

                把element[i]哈希到d->h[0]中;  // d->h[0]->used ++ 

      }

}

字典的迭代器(Iterator)

分为安全迭代器( safe Iterator )和非安全迭代器

安全迭代器可以保证在迭代器未释放之前,字典两个hash表之间不会进行桶转移。

桶转移对迭代器的影响是很大的,如果一个迭代器指向d->h[0]的某个桶中的元素实体。在一次桶转移后,这个实体被rehash到d->h[1]中。

而在d->h[1]中根本不知道哪些元素被迭代器放过过,哪些没有被訪问过,这样有可能让迭代器反复訪问或者缺失訪问字典中的一些元素。

所以安全迭代器可以保证不多不少不反复的訪问到全部的元素(当然在迭代过程中。不能涉及插入新元素和删除新元素的操作)。

Hash表的扩容(转载)的更多相关文章

  1. 【杂谈】Hash表与平衡树

    hash表与平衡树查询数据的时间复杂度是多少? hash表为O(1),平衡树为O(logn) 这个时间复杂度是如何得出的? 时间复杂度是按照最糟糕的情况来的.但即使是最糟糕的情况,hash表也只需要计 ...

  2. 【转载】一步一步写算法(之hash表)

    转载自:http://blog.csdn.net/feixiaoxing/article/details/6885657 [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaox ...

  3. Hash表题目整数hash-HDOJ1425(转载)

      哈希表(散列表)的基本原理:使用一个下标范围比较大的数组来存储元素,一般通过设计一个函数(哈希函数,即散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,然后用该数组单元来存储对应 ...

  4. 透过Redis源码探究Hash表的实现

    转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com/archives/667 本文使用的Redis 5.0源码 概述 我们在学习 Redis ...

  5. PHP数组/Hash表的实现/操作、PHP变量内核实现、PHP常量内核实现 - [ PHP内核学习 ]

    catalogue . PHP Hash表 . PHP数组定义 . PHP变量实现 . PHP常量实现 1. PHP Hash表 0x1: 基本概念 哈希表在实践中使用的非常广泛,例如编译器通常会维护 ...

  6. 一步一步写算法(之hash表)

    [ 声明:版权全部,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] hash表,有时候也被称为散列表.个人觉得,hash表是介于链表和二叉树之间的一种中间结构.链 ...

  7. MySQL的表分区(转载)

    MySQL的表分区(转载) 一.什么是表分区 通俗地讲表分区是将一大表,根据条件分割成若干个小表.mysql5.1开始支持数据表分区了. 如:某用户表的记录超过了600万条,那么就可以根据入库日期将表 ...

  8. hash表、hash算法

    概念: 散列表(Hash table.也叫哈希表),是依据关键码值(Key value)而直接进行訪问的数据结构. 也就是说,它通过把关键码值映射到表中一个位置来訪问记录,以加快查找的速度.这个映射函 ...

  9. 自己写一个 Hash 表

    项目地址:  https://github.com/kelin-xycs/HashTableLib 为什么会想要自己写一个 Hash 表, 以前也想过 Hash 表 的 原理, 觉得很神奇, 不过最近 ...

随机推荐

  1. opencv 实现进度控制

    进度控制: #include <opencv\cv.h> #include <opencv\highgui.h> #include <opencv\cxcore.h> ...

  2. 未能载入文件或程序集“DAL”或它的某一个依赖项。系统找不到指定的文件。

    这个一般出如今三层给B层与D层之间加抽象工厂-接口-映射.时候出的错.出错的地方是抽象工厂. --如图 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTA ...

  3. [Android算法] bitmap 将图片压缩到指定的大小

    Bitmap压缩到指定大小: private void imageZoom() {//图片允许最大空间 单位:KBdouble maxSize =400.00;//将bitmap放至数组中,意在bit ...

  4. spark-shell启动集群

    使用spark-shell  启动spark集群时的流程简析: spark-shell->spark-submit->spark-class 在sprk-class中根据条件会从不同的入口 ...

  5. ASP.NET优化性能方法之一禁用调试模式(转)

    若要设置 ASP.NET 应用程序的调试模式,必须编辑应用程序的 Web.config 配置文件. 通常,ASP.NET 应用程序的 Web.config 文件与应用程序位于相同的 URL 位置上. ...

  6. OpenGL ES 2.0 限定符

    限定符 说明 作用 attribute 一般用于各个顶点各不相同的量,如顶点位置.颜色等 属性限定符,修饰的变量用来接收渲染管线传递进顶点着色器的当前顶点的各种属性值. 只能用来修饰符点数标量,浮点数 ...

  7. hdu1054 树状dp

    B - 树形dp Crawling in process... Crawling failed Time Limit:2000MS     Memory Limit:10000KB     64bit ...

  8. pygame实现的黑白块游戏

    运行时需要pygame库. 下载地址:http://files.cnblogs.com/files/zzrom/white.zip 程序截图:

  9. 宏定义中使用do{}while(0)的好处 (转载)

    宏定义中使用do{}while(0)的好处   #define MACRO_NAME(para) do{macro content}while(0)   的格式,总结了以下几个原因:   1,空的宏定 ...

  10. Oracle11g R2学习系列 之六数据库链接,快照及序列

    Create public database link link_name Connect to user identified by password using 'DBName' 为'DBName ...