散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键
码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN ),搜索的效率取决于搜索过程中
元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函
数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快
找到该元素。
当向该结构中:
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash
Table)(或者称散列表)
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity为存储元素底层空间总的大小。
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快 问题:按照上述哈希方式,向集合中插入元
素44,会出现什么问题?
哈希冲突是区块链网络中,两个节点间存在的一种特殊的数据交换方式。在区块链系统中,一个节点的状态会同步地传递给其他节点(或区块),每个区块都包含上一个区块的哈希值和本区块的哈希值。因此,当两个或多个节点之间存在数据交换时就会发生碰撞,这种碰撞称为"冲突"。
首先,我们需要明确一点,由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一 个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率。
1.开放地址法
这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
就是说当发生冲突时,就去寻找下一个空的地址把数据存入其中,只要哈希表足够大,就总能找到这样一个空的地址。
2.拉链法
将所有关键字为同义字的记录存储在一个单链表中
3.再哈希法
在发生冲突的时候再用另外一个哈希函数算出哈希值,直到算出的哈希值不同为止。
4.建立公共溢出区
在创建哈希表的同时,再额外创建一个公共溢出区,专门用来存放发生哈希冲突的元素。查找时,先从哈希表查,查不到再去公共溢出区查。
哈希函数设计原则:
常见的哈希函数:
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况.
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
散列表的载荷因子定义为: α =填入表中的元素个数 / 散列表的长度
α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,表明填入表中的元素越多,产生冲突的可能性就越大 ; 反之,α越小,标明填入表中的元素越少,产生冲突的可能性就越小。
实际上,散列表的平均查找长度是载荷因子α的函数,只是不同处理冲突的方法有不同的函数。
对于开放定址法,荷载因子是特别重要因素,应严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中(cachemissing)按照指数曲线上升。因此,一些采用开放定址法的hash库,如Java的系统库限制了荷载因子为0.75,超过此值将resize散列表。
在java的HashMap类中,HashMap的底层就是哈希表,在jdk8中,默认的负载因子为0.75。
负载因子和冲突率的关系粗略演示:
所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。
已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。
**闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以 把key存放到冲突位置中的“下一个” 空位置中去。**那如何寻找下一个空位置呢?
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
插入:
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:
其中:i = 1,2,3…(这里的i表示第几次冲突, 直到计算出不冲突的位置),H0 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置, m是表的大小。
如果求得的地址冲突,就会以哈希值 + i ^ 2 , 哈希值 − i ^2 … 的顺序进行探测新的位置
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情 况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。
public class HashBuck {
static class Node {
public int key;
public int val;
public Node next;
public Node(int key, int val) {
this.key = key;
this.val = val;
}
}
public Node[] array;
public int usedSize;
public static final double DEFAULT_LOADFACTOR = 0.75;
// 桶
public HashBuck() {
this.array = new Node[10];
}
}
遍历数组中对应下标的链表是否有重复的元素,如果有则更新value值。,采用头插或者尾插的方法插入到链表中。JDK1.7默认是头插法,JDK1.8默认是尾插法,那么我们这里使用头插法来演示。
/**
* put函数
*
* @param key
* @param val
*/
public void put(int key, int val) {
// 1. 找key所在的位置
int index = key % this.array.length;
// 2. 遍历下标的链表,看是不是有相同的key,有,要更新val值
Node cur = array[index];
while (cur != null) {
if (cur.key == key) {
cur.val = val;
}
cur = cur.next;
}
// 3. 没有key这个元素,头插法
Node node = new Node(key, val);
node.next = array[index];
array[index] = node;
this.usedSize++;
// 4. 插入元素成功之后,检查当前散列表的负载因子
if (this.usedSize > DEFAULT_LOADFACTOR) {
resize();
}
}
扩容我们采用2倍的方式进行扩容,但是不能直接拷贝原数组的内容,因为数组长度发生改变,元素通过哈希函数计算的下标不一定和原来相等,所以我们需要遍历原来的所有元素,全部重新通过哈希函数计算对应扩容后数组映射的下标,并插入到新数组当中。
private void resize() {
Node[] newArray = new Node[array.length * 2];
for (int i = 0; i < array.length; i++) {
Node cur = array[i];
while (cur != null) {
int index = cur.key % newArray.length; // 获取新的下标
// 就是把cur这个节点,以头插/尾插的形式 插入到新的数组对应下标的链表中
Node curNext = cur.next;
cur.next = newArray[index]; // 先绑定后面
newArray[index] = cur; // 绑定前面
cur = curNext;
}
}
}
通过哈希函数和所给的key计算出对应数组的下标,然后遍历对象下标的链表,找到key相同的结点返回即可。
public int get(int key) {
// 1. 找key所在的位置
int index = key % this.array.length;
// 2. 遍历下标的链表
Node cur = array[index];
while (cur != null) {
if (cur.key == key) {
return cur.val;
}
cur = cur.next;
}
return -1;
}
public class HashBuck {
static class Node {
public int key;
public int val;
public Node next;
public Node(int key, int val) {
this.key = key;
this.val = val;
}
}
public Node[] array;
public int usedSize;
public static final double DEFAULT_LOADFACTOR = 0.75;
// 桶
public HashBuck() {
this.array = new Node[10];
}
/**
* put函数
*
* @param key
* @param val
*/
public void put(int key, int val) {
// 1. 找key所在的位置
int index = key % this.array.length;
// 2. 遍历下标的链表,看是不是有相同的key,有,要更新val值
Node cur = array[index];
while (cur != null) {
if (cur.key == key) {
cur.val = val;
}
cur = cur.next;
}
// 3. 没有key这个元素,头插法
Node node = new Node(key, val);
node.next = array[index];
array[index] = node;
this.usedSize++;
// 4. 插入元素成功之后,检查当前散列表的负载因子
if (this.usedSize > DEFAULT_LOADFACTOR) {
resize();
}
}
private void resize() {
Node[] newArray = new Node[array.length * 2];
for (int i = 0; i < array.length; i++) {
Node cur = array[i];
while (cur != null) {
int index = cur.key % newArray.length; // 获取新的下标
// 就是把cur这个节点,以头插/尾插的形式 插入到新的数组对应下标的链表中
Node curNext = cur.next;
cur.next = newArray[index]; // 先绑定后面
newArray[index] = cur; // 绑定前面
cur = curNext;
}
}
}
// 负载因子
private double loadFactor() {
return 1.0 * this.usedSize / this.array.length;
}
/**
* 根据key获取val值
*
* @param key
* @return
*/
public int get(int key) {
// 1. 找key所在的位置
int index = key % this.array.length;
// 2. 遍历下标的链表
Node cur = array[index];
while (cur != null) {
if (cur.key == key) {
return cur.val;
}
cur = cur.next;
}
return -1;
}
}
class Person {
public String ID;
@Override
public String toString() {
return "Person{" +
"ID='" + ID + '\'' +
'}';
}
public Person(String ID) {
this.ID = ID;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(ID, person.ID);
}
@Override
public int hashCode() {
return Objects.hash(ID);
}
}
public class HashBuck2<K, V> {
static class Node<K, V> {
public K key;
public V val;
public Node<K, V> next;
public Node(K key, V val) {
this.key = key;
this.val = val;
}
}
public Node<K, V>[] array = (Node<K, V>[])new Node[10];
public int usedSize;
}
首先我们这里肯定不能直接用key % length,引用类型需要转换为整型,这里用到了hashCode(),将引用类型转换为哈希码,再用hash%length。
public void put(K key, V val) {
int hash = key.hashCode();
int index = hash % array.length;
Node<K, V> cur = array[index];
while (cur != null) {
if (cur.key.equals(key)) {
cur.val = val;
return;
}
cur = cur.next;
}
Node<K, V> node = new Node<>(key, val);
node.next = array[index];
array[index] = node;
this.usedSize++;
}
public V get(K key) {
int hash = key.hashCode();
int index = hash % array.length;
Node<K, V> cur = array[index];
while (cur != null) {
if (cur.key.equals(key)) {
return cur.val;
}
cur = cur.next;
}
return null;
}
class Person {
public String ID;
@Override
public String toString() {
return "Person{" +
"ID='" + ID + '\'' +
'}';
}
public Person(String ID) {
this.ID = ID;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(ID, person.ID);
}
@Override
public int hashCode() {
return Objects.hash(ID);
}
}
public class HashBuck2<K, V> {
static class Node<K, V> {
public K key;
public V val;
public Node<K, V> next;
public Node(K key, V val) {
this.key = key;
this.val = val;
}
}
public Node<K, V>[] array = (Node<K, V>[])new Node[10];
public int usedSize;
public void put(K key, V val) {
int hash = key.hashCode();
int index = hash % array.length;
Node<K, V> cur = array[index];
while (cur != null) {
if (cur.key.equals(key)) {
cur.val = val;
return;
}
cur = cur.next;
}
Node<K, V> node = new Node<>(key, val);
node.next = array[index];
array[index] = node;
this.usedSize++;
}
public V get(K key) {
int hash = key.hashCode();
int index = hash % array.length;
Node<K, V> cur = array[index];
while (cur != null) {
if (cur.key.equals(key)) {
return cur.val;
}
cur = cur.next;
}
return null;
}
public static void main(String[] args) {
Person person1 = new Person("1234");
Person person2 = new Person("1234");
HashBuck2<Person,String> hashBuck2 = new HashBuck2<>();
hashBuck2.put(person1, "bit");
System.out.println(hashBuck2.get(person2));
}
public static void main1(String[] args) {
// hashcode
Person person1 = new Person("1234");
Person person2 = new Person("1234");
System.out.println(person1.hashCode());
System.out.println(person2.hashCode());
}
}
关系: