通过散列函数(也叫哈希函数)将元素的键映射为数组下标(转化后的值叫做散列值或哈希值),然后在对应下标位置存储记录值。当我们按照键值查询元素时,就是用同样的散列函数,将键值转化数组下标,从对应的数组下标的位置取数据
散列冲突指的是 key1 != key2 的情况下,通过散列函数处理,hash(key1) == hash(key2),这个时候,我们说发生了散列冲突,设计再好的散列函数也无法避免散列冲突,原因是散列值是非负整数,总量是有限的
如果不考虑散列冲突,散列表的查找效率是非常高的,时间复杂度是 O(1),比二分查找效率还要高,但是因为无法避免散列冲突,所以散列表查找的时间复杂度取决于散列冲突,最坏的情况可能是 O(n),退化为顺序查找。
散列函数
- 从哈希值不能反向推导出原始数据
- 对输入数据非常敏感,哪怕原始数据只修改了一个比特,最后得到的哈希值也大不相同
- 散列冲突的概率要很小,对于不同的原始数据,哈希值相同的概率非常小
- 哈希算法的执行效率要尽量高效,针对较长的文本,也能快速地计算出哈希值
1.除数留余法:即 f(key) = key % p,p 表示容器数量,这种方式通常用在将数据存放到指定容器中,如何决定哪个数据放到哪个容器
如何处理散列冲突
1.开放寻址法
- 线性寻址:出现散列冲突之后,就去寻找下一个空的散列地址;线性寻址步长是1
- 二次探测:步长是线性寻址步长的2次方
- 随机探测:每次步长随机
散列表中空闲位置不多的时候,散列冲突的概率就会提高,为了保证操作效率,我们会尽可能保证散列表中有一定比例的空闲槽位,我们用装载因子来表示空位的多少,装载因子=填入元素/散列表长度,装载因子越大,表明空闲位置越少,冲突越多,散列表性能降低。
2.再散列函数法
发生散列冲突后,换一个散列函数计算散列值
3.链地址法
发生散列冲突后,将对应数据链接到该散列值映射的上一个值之后,即将散列值相同的元素放到相同槽位对应的链表中。链地址法即使在散列冲突很多的情况下,也可以保证将所有数据存储到散列表中,但是也引入了遍历单链表带来性能损耗。
总结
1.散列函数设置合理,不能太过复杂,成为性能瓶颈;
2.设置装载因子阈值,支持动态扩容,装载因子阈值设置要充分权衡时间、空间复杂度;
3.如果一次性扩容耗时长,可采取分批扩容的策略,达到阈值后只申请空间,不搬移数据,以后每插入一条数据,搬移一个旧数据,最后逐步完成搬移,期间为了兼容新老散列表查询,可以先查新表,再查老表;
4.散列冲突解决办法:开发寻址法在数据量较小、装载因子小的时候(小于1)选用;链表法可以容忍装载因子大于1,适合存储大对象、大数据量的散列表,且更加灵活,支持更多优化策略
PHP
PHP 中的数组除了具备散列表的基本特点之外,还有一个特别的地方,那就是它是有序的(与Java中的HashMap的无序有所不同):数组中各元素的顺序和插入顺序一致 为了实现 PHP 数组的有序性,PHP 底层的散列表在散列函数与元素数组之间加了一层映射表,这个映射表也是一个数组,大小和存储元素的数组相同,存储元素的类型为整型,用于保存元素在实际存储的有序数组中的下标 —— 元素按照先后顺序依次插入实际存储数组,然后将其数组下标按照散列函数散列出来的位置存储在新加的映射表中
class ListNode
{
public $key;
public $val;
public $next;
public function __construct($key, $val, $next = null)
{
$this->key = $key;
$this->val = $val;
$this->next = $next;
}
}
class hashTable
{
protected $buckets; //数据容器
protected $size; //表长度
public function __construct(int $size)
{
$this->size = $size;
//SplFixedArray 更接近于C的数组,长度固定,索引必须为数字
$this->buckets = new SplFixedArray($size);
}
//哈希算法
protected function hash($key)
{
$len = strlen($key);
$hash = 0;
for ($i = 0; $i < $len; $i++) {
$hash += ord($key[$i]); //将每一位字符的ACSII值相加
}
return $hash % $this->size; //相对表长度取余
}
public function add($key, $val)
{
$index = $this->hash($key);
//索引已存在则遍历链表,若匹配到则直接修改值,无匹配则新建节点添加到头部
if (isset($this->buckets[$index])) {
$head = $this->buckets[$index];
$current = $head;
while ($current) {
if ($current->key == $key) {
break;
}
$current = $current->next;
}
if ($current) {
$current->val = $val;
return;
}
$node = new ListNode($key, $val, $head);
} else {
$node = new ListNode($key, $val);
}
$this->buckets[$index] = $node;
}
public function find($key)
{
$index = $this->hash($key);
$current = $this->buckets[$index];
while ($current) {
if ($current->key == $key) {
return $current->val;
}
$current = $current->next;
}
return null;
}
public function delete($key)
{
$index = $this->hash($key);
if (isset($this->buckets[$index])) {
$current = $this->buckets[$index];
//第一个数据匹配成功则指向下一个数据
if ($current->key == $key) {
$this->buckets[$index] = $current->next;
return;
}
//匹配到数据后将该数据的前后相连
while ($current) {
if ($current->next->key == $key) {
$current->next = $current->next->next;
return;
}
}
}
}
}
$hashTable = new hashTable(10);
$hashTable->add('aa', 'asdsad');
$hashTable->add('aaa', 'asdsad1123213');
$hashTable->add('aaa', '123213213213');
$hashTable->delete('aa');
echo $hashTable->find('aaa');
var_dump($hashTable);
GO
package main
import (
"fmt"
)
type ListNode struct {
key string
value string
next *ListNode
}
type HashTable struct {
buckets [10] *ListNode
}
func main() {
var htable HashTable
htable.add("a", "a")
htable.add("a", "aa")
fmt.Println(htable.find("a"))
htable.del("a")
fmt.Println(htable.find("a"))
}
func (htable *HashTable) add(key string, val string) {
var newNode ListNode
index := htable.hash(key)
node := htable.buckets[index]
if node == nil {
newNode.key = key
newNode.value = val
} else {
head := node
current := node
for {
if current == nil {
break
}
if current.key == key {
break
}
current = current.next
}
if current != nil {
current.value = val
return
}
newNode.key = key
newNode.value = val
newNode.next = head
}
htable.buckets[index] = &newNode
}
func (htable *HashTable) find(key string) string {
index := htable.hash(key)
current := htable.buckets[index]
for {
if current == nil {
break
}
if current.key == key {
return current.value
}
current = current.next
}
return ""
}
func (htable *HashTable) del(key string) {
index := htable.hash(key)
head := htable.buckets[index]
current := head
if current.key == key {
if current.next != nil {
current = current.next
} else {
current = nil
}
htable.buckets[index] = current
return
}
for {
if current.next == nil {
break
}
if current.next.key == key {
if current.next.next != nil {
current.next = current.next.next
} else {
current.next = nil
}
return
}
current = current.next
}
htable.buckets[index] = head
}
func (htable *HashTable) hash(key string) int {
hash := 0
for _, str := range key {
hash += int(str)
}
return hash % len(htable.buckets)
}