您的位置:首页 > 其它

HashMap实现原理详解

2017-07-28 15:16 781 查看


1. HashMap的数据结构

http://blog.csdn.net/gaopu12345/article/details/50831631 ??看一下

数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端。


数组

数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难。


链表

链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表 的特点是:寻址困难,插入和删除容易 。


哈希表

那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构 ?答案是肯定的,这就是我们要提起的哈希表 。哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。

  哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法—— 拉链法 ,我们可以理解为“链表的数组 ” ,如图:





  从上图我们可以发现哈希表是由数组+链表 组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模 得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。

  HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组 。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。

  首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next ,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[] ,Map里面的内容都保存在Entry[]里面。

/**

* The table, resized as necessary. Length MUST Always be a power of two.

*/

transient Entry[] table;


2. HashMap的存取实现

既然是线性数组,为什么能随机存取?这里HashMap用了一个小算法,大致是这样实现:


// 存储时:

int hash = key.hashCode(); // 这个hashCode方法这里不详述,只要理解每个key的hash是一个固定的int值

int index = hash % Entry[].length;

Entry[index] = value;

// 取值时:

int hash = key.hashCode();

int index = hash % Entry[].length;

return Entry[index];


1)put

疑问:如果两个key通过hash%Entry[].length得到的index相同,会不会有覆盖的危险?

  这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A ,Entry[0] = B,如果又进来C,index也等于0,那么C.next
= B ,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素 。 到这里为止,HashMap的大致实现,我们应该已经清楚了。

public V put(K key, V value) {

if (key == null)

return putForNullKey(value); //null总是放在数组的第一个链表中

int hash = hash(key.hashCode());

int i = indexFor(hash, table.length);

//遍历链表

for (Entry<K,V> e = table[i]; e != null; e = e.next) {

Object k;

//如果key在链表中已存在,则替换为新value

if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

V oldValue = e.value;

e.value = value;

e.recordAccess(this);

return oldValue;

}

}

modCount++;

addEntry(hash, key, value, i);

return null;

}

void addEntry(int hash, K key, V value, int bucketIndex) {

Entry<K,V> e = table[bucketIndex];

table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //参数e, 是Entry.next

//如果size超过threshold,则扩充table大小。再散列

if (size++ >= threshold)

resize(2 * table.length);

}

  当然HashMap里面也包含一些优化方面的实现,这里也说一下。比如:Entry[]的长度一定后,随着map里面数据的越来越长,这样同一个index的链就会很长,会不会影响性能?HashMap里面设置一个因子,随着map的size越来越大,Entry[]会以一定的规则加长长度 。


2)get

public V get(Object key) {

if (key == null)

return getForNullKey();

int hash = hash(key.hashCode());

//先定位到数组元素,再遍历该元素处的链表

for (Entry<K,V> e = table[indexFor(hash, table.length)];

e != null;

e = e.next) {

Object k;

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

return e.value;

}

return null;

}


3)null key的存取

null key总是存放在Entry[]数组的第一个元素。

private V putForNullKey(V value) {

for (Entry<K,V> e = table[0]; e != null; e = e.next) {

if (e.key == null) {

V oldValue = e.value;

e.value = value;

e.recordAccess(this);

return oldValue;

}

}

modCount++;

addEntry(0, null, value, 0);

return null;

}

private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}


4)确定数组index:hashcode % table.length取模

HashMap存取时,都需要计算当前key应该对应Entry[]数组哪个元素,即计算数组下标;算法如下:

/**

* Returns index for hash code h.

*/

static int indexFor(int h, int length) {

return h & (length-1);

}

按位取并,作用上相当于取模mod或者取余%。

这意味着数组下标相同,并不表示hashCode相同。


5)table初始大小

public HashMap(int initialCapacity, float loadFactor) {

.....

// Find a power of 2 >= initialCapacity

int capacity = 1;

while (capacity < initialCapacity)

capacity <<= 1;

this.loadFactor = loadFactor;

threshold = (int)(capacity * loadFactor);

table = new Entry[capacity];

init();

}

注意table初始大小并不是构造函数中的initialCapacity!!

而是 >= initialCapacity的2的n次幂!!!!

————为什么这么设计呢?——


3. 解决hash冲突的办法 (下文详解)

开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
再哈希法
链地址法
建立一个公共溢出区

Java中hashmap的解决办法就是采用的链地址法。


4. 再散列rehash过程

当哈希表的容量超过默认容量时,必须调整table的大小。当容量已经达到最大可能值时,那么该方法就将容量调整到Integer.MAX_VALUE返回,这时,需要创建一张新表,将原表的映射到新表中。

/**

* Rehashes the contents of this map into a new array with a

* larger capacity. This method is called automatically when the

* number of keys in this map reaches its threshold.

*

* If current capacity is MAXIMUM_CAPACITY, this method does not

* resize the map, but sets threshold to Integer.MAX_VALUE.

* This has the effect of preventing future calls.

*

* @param newCapacity the new capacity, MUST be a power of two;

* must be greater than current capacity unless current

* capacity is MAXIMUM_CAPACITY (in which case value

* is irrelevant).

*/

void resize(int newCapacity) {

Entry[] oldTable = table;

int oldCapacity = oldTable.length;

if (oldCapacity == MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return;

}

Entry[] newTable = new Entry[newCapacity];

transfer(newTable);

table = newTable;

threshold = (int)(newCapacity * loadFactor);

}

/**

* Transfers all entries from current table to newTable.

*/

void transfer(Entry[] newTable) {

Entry[] src = table;

int newCapacity = newTable.length;

for (int j = 0; j < src.length; j++) {

Entry<K,V> e = src[j];

if (e != null) {

src[j] = null;

do {

Entry<K,V> next = e.next;

//重新计算index

int i = indexFor(e.hash, newCapacity);

e.next = newTable[i];

newTable[i] = e;

e = next;

} while (e != null);

}

}

}

============================= 华丽的分割线 ==============


hashmap的扩容机制

1、当我们往hashmap中put元素的时候,先根据key的hash值得到这个元素在 数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的 形式存放,新加入的放在链头,比如a->b->c,新加入的d放到a的位置前面,最先加入的放在链尾,也就是c。最后变成d->a->b->c,从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元素, 然后通过key的equals方法在对应位置的链表中找到需要的元素。

2、

在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置 的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样做的,

Java代码

staticintindexFor(inth,intlength){

returnh&(length-1);

}

首 先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方, 那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap 的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap访问的性能最高。 看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的
时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是 这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了 。说到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面
annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询 的效率。

3、

当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行 扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了, 而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的 默认值为0.75, 也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那
么预设元素的个数能够有效的提高hashmap的性能。

比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

======================= 华丽的分割线 =====================================


哈希表及处理hash冲突的方法

看了ConcurrentHashMap的实现, 使用的是拉链法.

虽然我们不希望发生冲突,但实际上发生冲突的可能性仍是存在的。当关键字值域远大于哈希表的长度,而且事先并不知道关键字的具体取值时。冲突就难免会发生。

另外,当关键字的实际取值大于哈希表的长度时,而且表中已装满了记录,如果插入一个新记录,不仅发生冲突,而且还会发生溢出。

因此,处理冲突和溢出 是哈希技术中的两个重要问题。

哈希法又称 散列法、杂凑法以及关键字地址计算法 等,相应的表称为 哈希表。 这种方法的基本思想是: 首先在元素的关键字 k 和元素的存储位置 p 之间建立一个对应关系 f ,使得 p=f(k) , f 称为哈希函数 。创建哈希表时,把关键字为 k 的元素 直接存入地址为 f(k) 的单元 ;以后当查找关键字为 k 的元素时,再利用哈希函数计算出该元素的存储位置 p=f(k) ,从而达到按关键 字直接存取元素的目的。

当关键字集合很大时,关键字值不同的元素可能会映象到哈希表的同一地址上 ,即 k1 ≠ k2 ,但 H ( k1 ) =H ( k2 ),这种现象称为冲突, 此时称k1 和 k2 为同义词。 实际中,冲突是不可避免的,只能通过改进哈希函数的性能来减少冲突。

综上所述,哈希法主要包括以下两方面的内容:

1 )如何构造哈希函数

2 )如何处理冲突。


8.4.1 哈希函数的构造方法

构造哈希函数的原则是: ① 函数本身便于计算; ② 计算出来的地址分布均匀,即对任一关键字 k , f(k) 对应不同地址的概率相等,目的是尽可能减少冲突。

下面介绍构造哈希函数常用的五种方法。

1 . 数字分析法

如果事先知道关键字集合,并且每个关键字的位数比哈希表的地址码位数多时,可以从关键字中选出分布较均匀的若干位,构成哈希地址。例如,有 80个记录,关键字为 8 位十进制整数 d1 d2 d3 …d7 d8 ,如哈希表长取 100 ,则哈希表的地址空间为: 00~99 。假设经过分析,各关键字中 d4 和 d7 的取值分布较均匀,则哈希函数为: h(key)=h(d1 d2 d3 …d7 d8 )=d4 d7 。例如, h(81346532)=43 , h(81301367)=06 。相反,假设经过分析,各关键字中 d1 和 d8 的取值分布极不均匀, d1 都等于 5 , d8 都等于 2 ,此时,如果哈希函数为: h(key)=h(d1 d2 d3 …d7 d8 )=d1 d8 ,则所有关键字的地址码都是 52 ,显然不可取。

2 . 平方取中法

当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。

例:我们把英文字母在字母表中的位置序号作为该英文字母的内部编码。例如 K 的内部编码为 11 , E 的内部编码为 05 , Y 的内部编码为 25 , A 的内部编码为 01,
B 的内部编码为 02 。由此组成关键字“ KEYA ”的内部代码为 11052501 ,同理我们可以得到关键字“ KYAB ”、“ AKEY ”、“ BKEY”的内部编码。之后对关键字进行平方运算后,取出第 7 到第 9 位作为该关键字哈希地址,如图 8.23 所示。

关键字
内部编码
内部编码的平方值
H(k) 关键字的哈希地址
KEYA
11050201
122157778355001
778
KYAB
11250102
126564795010404
795
AKEY
01110525
001233265775625
265
BKEY
02110525
004454315775625
315
图 8.23 平方取中法求得的哈希地址

3 . 分段叠加法

这种方法是按哈希表地址位数将关键字分成位数相等的几部分(最后一部分可以较短),然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。具体方法有折叠法 与移位法。 移位法是将分割后的每部分低位对齐相加,折叠法是从一端向另一端沿分割界来回折叠(奇数段为正序,偶数段为倒序),然后将各段相加。例如: key=12360324711202065, 哈希表长度为 1000 ,则应把关键字分成 3 位一段,在此舍去最低的两位 65 ,分别进行移位叠加和折叠叠加,求得哈希地址为 105 和 907 ,如图 8.24 所示。

1 2 3 1 2 3

6 0 3 3 0 6

2 4 7 2 4 7

1 1 2 2 1 1

+ ) 0 2 0 + ) 0 2 0

———————— —————————

1 1 0 5 9 0 7

( a )移位叠加 (b) 折叠叠加

图 8.24 由叠加法求哈希地址

4 . 除留余数法

假设哈希表长为 m , p 为小于等于 m 的最大素数,则哈希函数为

h ( k ) =k % p ,其中 % 为模 p 取余运算。

例如,已知待散列元素为( 18 , 75 , 60 , 43 , 54 , 90 , 46 ),表长 m=10 , p=7 ,则有

h(18)=18 % 7=4 h(75)=75 % 7=5 h(60)=60 % 7=4

h(43)=43 % 7=1 h(54)=54 % 7=5 h(90)=90 % 7=6

h(46)=46 % 7=4

此时冲突较多。为减少冲突,可取较大的 m 值和 p 值,如 m=p=13 ,结果如下:

h(18)=18 % 13=5 h(75)=75 % 13=10 h(60)=60 % 13=8

h(43)=43 % 13=4 h(54)=54 % 13=2 h(90)=90 % 13=12

h(46)=46 % 13=7

此时没有冲突,如图 8.25 所示。

0 1 2 3 4 5 6 7 8 9 10 11 12

54
43
18
46
60
75
90
图 8.25 除留余数法求哈希地址

5 . 伪随机数法

采用一个伪随机函数做哈希函数,即 h(key)=random(key) 。

在实际应用中,应根据具体情况,灵活采用不同的方法,并用实际数据测试它的性能,以便做出正确判定。通常应考虑以下五个因素 :

l 计算哈希函数所需时间 (简单)。

l 关键字的长度。

l 哈希表大小。

l 关键字分布情况。

l 记录查找频率

8.4.2 处理冲突的 方法

通过构造性能良好的哈希函数,可以减少冲突,但一般不可能完全避免冲突,因此解决冲突是哈希法的另一个关键问题。创建哈希表和查找哈希表都会遇到冲突,两种情况下解决冲突的方法应该一致。下面以创建哈希表为例,说明解决冲突的方法。常用的解决冲突方法有以下四种:

1. 开放定址法

这种方法也称 再散列法 , 其基本思想是:当关键字 key 的哈希地址 p=H ( key )出现冲突时,以 p 为基础,产生另一个哈希地址 p1 ,如果 p1 仍然冲突,再以 p 为基础,产生另一个哈希地址 p2 , … ,直到找出一个不冲突的哈希地址 pi , 将相应元素存入其中。这种方法有一个通用的再散列函数形式:

Hi= ( H ( key ) +di ) %
m i=1 , 2 , …, n

其中 H ( key )为哈希函数, m 为表长, di 称为增量序列。 增量序列的取值方式不同,相应的再散列方式也不同。主要有以下三种:

l 线性探测再散列

di i=1 , 2 , 3 , … , m-1

这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。

l 二次探测再散列

di =12 , -12 , 22 , -22 , … , k2 , -k2 (
k<=m/2 )

这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。

l 伪随机探测再散列

di = 伪随机数序列。

具体实现时,应建立一个伪随机数发生器,(如 i=(i+p) % m ),并给定一个随机数做起点。

例如,已知哈希表长度 m=11 ,哈希函数为: H ( key ) =
key % 11 ,则 H ( 47 ) =3 , H ( 26 ) =4 , H ( 60 ) =5 ,假设下一个关键字为 69 ,则 H ( 69 ) =3 ,与 47 冲突。如果用线性探测再散列处理冲突,下一个哈希地址为 H1= ( 3
+ 1 ) % 11 = 4 ,仍然冲突,再找下一个哈希地址为 H2= ( 3
+ 2 ) % 11 = 5 ,还是冲突,继续找下一个哈希地址为 H3= ( 3
+ 3 ) % 11 = 6 ,此时不再冲突,将 69 填入 5 号单元,参图 8.26
(a) 。如果用二次探测再散列处理冲突,下一个哈希地址为 H1= ( 3
+ 12 ) % 11 = 4 ,仍然冲突,再找下一个哈希地址为 H2= ( 3
- 12 ) % 11 = 2 ,此时不再冲突,将 69 填入 2 号单元,参图 8.26
(b) 。如果用伪随机探测再散列处理冲突,且伪随机数序列为: 2 , 5 , 9 , …….. ,则下一个哈希地址为 H1= ( 3
+ 2 ) % 11 = 5 ,仍然冲突,再找下一个哈希地址为 H2= ( 3
+ 5 ) % 11 = 8 ,此时不再冲突,将 69 填入 8 号单元,参图 8.26
(c) 。

0 1 2 3 4 5 6 7 8 9 10

47
26
60
69
( a ) 用线性探测再散列处理冲突

0 1 2 3 4 5 6 7 8 9 10

69
47
26
60
( b ) 用二次探测再散列处理冲突

0 1 2 3 4 5 6 7 8 9 10

47
26
60
69
( c ) 用伪随机探测再散列处理冲突

图 8.26 开放地址法处理冲突

从上述例子可以看出,线性探测再散列容易产生“二次聚集”,即在处理同义词的冲突时又导致非同义词的冲突。例如,当表中 i, i+1 ,i+2 三个单元已满时,下一个哈希地址为 i, 或 i+1
, 或 i+2 ,或 i+3 的元素,都将填入 i+3 这同一个单元,而这四个元素并非同义词。线性探测再散列的优点是:只要哈希表不满,就一定能找到一个不冲突的哈希地址,而二次探测再散列和伪随机探测再散列则不一定。

2. 再哈希法

这种方法是同时构造多个不同的哈希函数:

Hi =RH1 ( key ) i=1 , 2 , … , k

当哈希地址 Hi =RH1 ( key )发生冲突时,再计算 Hi =RH2 ( key )……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

3. 链地址法

这种方法的基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链 的单链表 ,并将单链表的头指针存在哈希表的第 i 个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

例如,已知一组关键字( 32 , 40 , 36 , 53 , 16 , 46 , 71 , 27 , 42 , 24 , 49 , 64 ),哈希表长度为 13 ,哈希函数为: H ( key ) =
key % 13 ,则用链地址法处理冲突的结果如图 8.27 所示:


图 8.27 链地址法处理冲突时的哈希表

本例的平均查找长度 ASL=(1 * 7+2 * 4+3 * 1)=1.5

(2)拉链法的优点

与开放定址法相比,拉链法有如下几个优点:

①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在
用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

(3)拉链法的缺点

 拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。??????

4 、 建立公共溢出区

这种方法的基本思想是:将哈希表分为基本表 和溢出表 两部分,凡是和基本表发生冲突的元素,一律填入溢出表
点击打开链接
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: