散列表之冲突碰撞处理|Go主题月

419 阅读5分钟

假如存在这么一个情况:给定任意一个元素集合,能将每个元素映射到不同的槽,那么就永远都不会有冲突(碰撞)。

上述情况称之为完美散列函数,但是只有当集合元素数量和元素值确定的时候才会发生。其他情况下这个完美散列函数是不存在的。

所以当两个元素被分到同一个槽中时,必须通过一种系统化方法在散列表中放置第二个元素,即冲突处理。

处理冲突两大类方法

处理冲突主要有以下两种方法:

  1. 探测法
  2. 链接法

假设我们以除余函数作为散列函数,把下列元素21, 15, 16, 17, 11, 20, 22, 33, 9放入到槽数为11的散列表中,看看下面两种冲突处理方法的应用。

hash_to_allocate.png

探测法

一种方法是在散列表中找到另一个空槽,用于放置引起冲突的元素。

线性探测法

简单的做法是从起初的散列值开始,顺序遍历散列表,直到找到一个空槽。注意,为了遍历散列表,可能需要往回检查第一个槽。这个过程被称为开放定址法,它尝试在散列表中寻找下一个空槽或地址。由于是逐个访问槽,因此这个做法被称作线性探测。

使用线性探测法把21, 15, 16, 17, 11, 20, 22, 33, 9放入到槽数为11的散列表的过程如下图所示:

hash_to_allocate.png

当插入22时,应该把其放入0号槽,但是0号槽已有数据,所以寻找下一个空槽即1号槽;

当插入33时,应该把其放入0号槽,但是已存在数据,所以寻找下一个空槽即2号槽;

当插入9时,应该把其放入9号槽,但是9号槽已有数据,那么寻找下一个空槽,依次访问了10号槽,0号槽,1号槽,2号槽,直到3号槽为空槽,所以9被放入了3号槽。

最终位置: 线性探测.png

拓展线性探测法

但是上述的线性探测法会导致元素进行聚集,会占用其他元素的槽位。
例如11,22,33的散列值均为0,都应该放入到0号槽位。但是一个槽位只能放一个元素,导致22和33占用了1号槽和2号槽。
这样的话,如果此时需要插入12这个元素,其散列值为1,但1号槽已被占用,所有12只能放入到下一个空槽了。

为了解决这种元素聚集的问题,我们可以使用大于1的步长进行寻找下一个空槽,这种方法被称为拓展线性探测法。还是以把21, 15, 16, 17, 11, 20, 22, 33, 9放入到槽数为11的散列表为例。假设我们当出现冲突时,以步长为3(即跳过两个槽)进行寻找下一个空槽。那么当我们把22插入散列表时,其散列值为0,此时由于0号槽已存储了11这个元素,存在冲突,那么采用“加3”即跳过两个槽寻找空槽,即3号槽,发现3号槽为空,这时就可以把22放入3号槽了。33和9的处理过程一样。
最终结果:

hash_to_allocate.png 扩展.png

链接法

另外一种处理冲突的方法是允许一个槽位指向一个含有多个元素的集合或链表,这种方法称为链接法。发生冲突时,元素仍然被插入到其散列值对应的槽位中。还是以上述的例子为例,最终的结果入下图所示:

链接法.png

以除余函数作为散列函数,以线性探测法作为冲突处理方法的Golang实现:

package main

import "fmt"

type HashTable struct {
	ht []int
}

func (HT *HashTable) init(length int) {
	// 创建槽数为11的散列表,限定存储无符号整数
	HT.ht = make([]int, length)
	for i := 0; i <= 10; i++ {
		// 初始化,-1表示该槽尚未使用
		HT.ht[i] = -1
	}
}

func (HT *HashTable) set(item int) {
	mod := mod(item, len(HT.ht))
	// 如果不存在冲突,则直接赋值
	if HT.ht[mod] == -1 {
		HT.ht[mod] = item

	} else {
		// 否则,再散列以获得空槽
		for HT.ht[mod] != -1 {
			mod = HT.rehash(mod)
			// 如果此槽位存放的数据和要插入的元素相同,则退出循环
			if HT.ht[mod] == item {
				break
			}
		}
		HT.ht[mod] = item
	}
}

func (HT *HashTable) isExist(item int) bool {
	mod := mod(item, len(HT.ht))
	if HT.ht[mod] == item {
		return true
	} else {
		// 有可能存现冲突,被放到其他槽位中
		// 需要遍历除了散列值本身的其他槽位
		slot := mod
		mod = HT.rehash(mod)
		for mod != slot {
			if HT.ht[mod] == item {
				return true
			} else {
				mod = HT.rehash(mod)
			}
		}

		return false
	}
}

func mod(item int, htLength int) int {
	return item % htLength
}

func (HT *HashTable) rehash(mod int) int {
	if mod == (len(HT.ht) - 1) {
		return 0
	} else {
		return mod + 1
	}
}

func main() {
	ht := HashTable{}
	// 初始化为有11个槽的散列表
	ht.init(11)
	// 把10写入散列表
	ht.set(0)
	// 散列搜索10是否存在于散列表中
	fmt.Println(ht.isExist(0))
	// true
	// 把11写入散列表
	ht.set(11)
	// 散列搜索11是否存在于散列表中
	fmt.Println(ht.isExist(11))
	// true
	// 把22写入散列表
	ht.set(22)
	// 散列搜索22是否存在于散列表中
	fmt.Println(ht.isExist(22))
	// true
	// 把33写入散列表
	ht.set(33)
	// 散列搜索33是否存在于散列表中
	fmt.Println(ht.isExist(33))
	// true
	// 把33再次写入散列表
	ht.set(33)
	// 散列搜索33是否存在于散列表中
	fmt.Println(ht.isExist(33))
	// true
	fmt.Println(ht.ht)
	// [0 11 22 33 -1 -1 -1 -1 -1 -1 -1]
}