Hash表的扩容(转载)
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中的两个结构体:
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来具体描写叙述容量扩张及转移的过程:
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表的扩容(转载)的更多相关文章
- 【杂谈】Hash表与平衡树
hash表与平衡树查询数据的时间复杂度是多少? hash表为O(1),平衡树为O(logn) 这个时间复杂度是如何得出的? 时间复杂度是按照最糟糕的情况来的.但即使是最糟糕的情况,hash表也只需要计 ...
- 【转载】一步一步写算法(之hash表)
转载自:http://blog.csdn.net/feixiaoxing/article/details/6885657 [ 声明:版权所有,欢迎转载,请勿用于商业用途. 联系信箱:feixiaox ...
- Hash表题目整数hash-HDOJ1425(转载)
哈希表(散列表)的基本原理:使用一个下标范围比较大的数组来存储元素,一般通过设计一个函数(哈希函数,即散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,然后用该数组单元来存储对应 ...
- 透过Redis源码探究Hash表的实现
转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com/archives/667 本文使用的Redis 5.0源码 概述 我们在学习 Redis ...
- PHP数组/Hash表的实现/操作、PHP变量内核实现、PHP常量内核实现 - [ PHP内核学习 ]
catalogue . PHP Hash表 . PHP数组定义 . PHP变量实现 . PHP常量实现 1. PHP Hash表 0x1: 基本概念 哈希表在实践中使用的非常广泛,例如编译器通常会维护 ...
- 一步一步写算法(之hash表)
[ 声明:版权全部,欢迎转载,请勿用于商业用途. 联系信箱:feixiaoxing @163.com] hash表,有时候也被称为散列表.个人觉得,hash表是介于链表和二叉树之间的一种中间结构.链 ...
- MySQL的表分区(转载)
MySQL的表分区(转载) 一.什么是表分区 通俗地讲表分区是将一大表,根据条件分割成若干个小表.mysql5.1开始支持数据表分区了. 如:某用户表的记录超过了600万条,那么就可以根据入库日期将表 ...
- hash表、hash算法
概念: 散列表(Hash table.也叫哈希表),是依据关键码值(Key value)而直接进行訪问的数据结构. 也就是说,它通过把关键码值映射到表中一个位置来訪问记录,以加快查找的速度.这个映射函 ...
- 自己写一个 Hash 表
项目地址: https://github.com/kelin-xycs/HashTableLib 为什么会想要自己写一个 Hash 表, 以前也想过 Hash 表 的 原理, 觉得很神奇, 不过最近 ...
随机推荐
- Eclipse+Java+OpenCV246环境搭建和代码测试
1.首先下载OpenCV2.4.6,下载的时候,选择windows版的.然后安装 2.其实安装的过程就是解压的过程,并没有什么安装向导之类的,安装完成后,我们最关心的是这个目录:opencv\buil ...
- 在CentOS 7上利用systemctl加入自己定义系统服务
CentOS 7继承了RHEL 7的新的特性,比如强大的systemctl,而systemctl的使用也使得以往系统服务的/etc/init.d的启动脚本的方式就此改变,也大幅提高了系统服务的执行效率 ...
- VCS仿真生成vpd文件(verilog)
VCS仿真生成vpd文件(verilog) 一.环境与文件 Linux平台 csh环境 VCS 64bit 代码文件请参考<一个简单的Verilog计数器模型> 二.开始仿真 1.com ...
- Stm32高级定时器(二)
Stm32高级定时器(二) 1 主从模式:主?从? 谈论主从,可知至少有两个以上的触发或者驱动信号,stm32内部有多个定时器,可以相互之间驱动或者控制. 主模式:定时器使能只受驱动时钟控制或者输出控 ...
- css画下图
通常我看到这种效果,都是直接ps解决,但是不断重申性能的今天,显然不适应时代的需求啊! 今天看到群里有人问这种效果怎么做了,我在思考的时候,有人已经给出答案了: 我就测试了一下,发现确实可以实现,总结 ...
- C/C++中的空类及抽象类大小
代码: #include <iostream> using namespace std; struct A{ }; struct B{ int i; }; class C:B{ ; }; ...
- 洛谷 P1066 2^k进制数
P1066 2^k进制数 题目描述 设r是个2^k 进制数,并满足以下条件: (1)r至少是个2位的2^k 进制数. (2)作为2^k 进制数,除最后一位外,r的每一位严格小于它右边相邻的那一位. ( ...
- 解决django关于图片无法显示的问题
http://python.usyiyi.cn/django/index.html http://m.blog.csdn.net/blog/qingyuanluofeng/44877399 http: ...
- hadoop集群中的日志文件
hadoop存在多种日志文件,其中master上的日志文件记录全面信息,包括slave上的jobtracker与datanode也会将错误信息写到master中.而slave中的日志主要记录完成的ta ...
- js实现选项卡切换的三种方式
前两种主要实现一个选项卡的切换,第三种使用闭包看书,构造函数实现多个选项卡的切换: 1.第一种实现实现效果为: 实现代码为: <!doctype html> <!DOCTYPE ht ...