提到hash,相信大多数同学都不会陌生,之前很火现在也依旧很火的技术区块链背后的底层原理之一就是hash散列值,下面就从hash算法的原理和实际应用等几个角度,对hash算法进行一个讲解。

1、什么是hash

散列算法(Hash Algorithm),又称哈希算法,杂凑算法,是一种从任意文件中创造小的数字「指纹」的方法。与指纹一样,散列算法就是一种以较短的信息来保证文件唯一性的标志,这种标志与文件的每一个字节都相关,而且难以找到逆向规律。因此,当原有文件发生改变时,其标志值也会发生改变,从而告诉文件使用者当前的文件已经不是你所需求的文件。

Hash 算法能将将任意长度的二进制明文映射为较短的二进制串的算法,并且不同的明文很难映射为相同的 Hash 值。

也可以理解为空间映射函数,是从一个非常大的取值空间映射到一个非常小的取值空间,由于不是一对一的映射,Hash 函数转换后不可逆,意思是不可能通过逆操作和 Hash 值还原出原始的值。

散列方法的主要思想是根据结点的关键码值来确定其存储地址:以关键码值K为自变量,通过一定的函数关系h(K)(称为散列函数),计算出对应的函数值来,把这个值解释为结点的存储地址,将结点存入到此存储单元中。检索时,用同样的方法计算地址,然后到相应的单元里去取要找的结点。通过散列方法可以对结点进行快速检索。散列(hash,也称“哈希”)是一种重要的存储方式,也是一种常见的检索方法。

2、Hash的特点

Hash 值又称为指纹或者摘要,具有以下特点:

正向快速:给定明文和 Hash 算法,在有限时间和有限资源内能计算得到 Hash 值。

逆向困难:给定 Hash 值,在有限时间内很难逆推出明文。

散列通多少钱_散列值_太极散列通 半衰期

输入敏感:原始输入信息发生任何变化,新的 Hash 值都应该出现很大变化。

冲突避免:很难找到两段内容不同的明文,使得它们的 Hash 值一致。

一致性。

Hash 函数可以接受任意大小的数据,并输出固定长度的散列值,同时输出不同值的概率应该尽可能一致。如 CityHash128,不管原始数据有多大,计算得到的 hash 值总是 128 bit。

雪崩效应。

原始数据哪怕只有一个字节的修改,得到的 hash 值都会发生巨大的变化。

单向。

只能从原始数据计算得到 hash 值,不能从 hash 值计算得到原始数据。所以散列算法不是加密解密算法,加密解密是可逆的,散列算法是不可逆的。

避免冲突。

几乎不可能找到一个数据和当前计算的这个数据计算出一样的 hash 值,因此散列函数能够确保数据的唯一性。在 Hash 函数保证不同值出现的概率一致的情况下,CityHash128 出现碰撞的概率只有 2 ^ -128。因为不同 Key 的碰撞概率很小,所以在某些情况下我们可以直接使用较短的 Hash 值代替较长原始数据存储。

太极散列通 半衰期_散列值_散列通多少钱

太极散列通 半衰期_散列通多少钱_散列值

3、hash碰撞的解决方案

3.1 哈希碰撞的理解

先来看看哈希表的定义,在概念上哈希表可以定义为是一个根据键(Key)而直接访问在内存中存储位置的值(Value)的数据结构。这里所说的键就是指哈希函数的输入,而这里的值并不是指哈希值,而是指我们想要保存的值。

在现实中, 想要有一个完美的哈希函数,将输入值转换成哈希值而不产生哈希碰撞基本是不可能的,所以哈希表在通过键来访问存储位置的值的时候,是根据我们处理哈希碰撞来决定它自身操作的。那下面我们就以一个具体例子来说明一下,不同的哈希碰撞其解决方式所带来的底层存储键值对操作的差异。 我们假设存储哈希表的底层数据结构是一个大小为 3 的数组,我们还是以保存好友电话号码为例,这个哈希表的键是我们好友的名字,哈希表的值是这个好友的电话号码。刚开始的时候,因为这个数据结构并没有存储任何信息,所以数据结构内存结构图如下图所示:

太极散列通 半衰期_散列通多少钱_散列值

散列值_太极散列通 半衰期_散列通多少钱

假设第一个输入的键值对是(Tom:123456),表示好友的名字叫 Tom,电话号码为 123456。我们同时假设 Tom 这个字符串在通过哈希函数之后的所产生的哈希值是 0,此时可以把 123456 这个值放在以哈希值为索引的地方,内存结构如下图所示:

散列值_太极散列通 半衰期_散列通多少钱

紧接着,我们输入的第二个键值对是(Jack:456789),同时假设 Jack 这个字符串在通过哈希函数之后所产生的哈希值也是 0,而因为索引为 0 的位置已经存放一个值了,也就表示这时候产生了哈希碰撞。

3.2 hash 碰撞产生的概率

无论 hash table 有多大,hash 冲突都很容易出现。典型的例子是生日悖论, 23 个人中两个人生日相同的概率高达 50%。

3.3 hash碰撞的解决方案

3.3.1 开放地址法

开放定址法也称为再散列法,基本思想就是,如果p=H(key)出现冲突时,则以p为基础,再次hash,p1=H(p),如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi。 因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点。

缺点:容易产生堆积问题;不适合大规模的数据存储;插入时会发生多次冲突的情况;删除时要考虑与要删除元素互相冲突的另一个元素,比较复杂。

3.3.2 再哈希法(双重散列,多重散列)

提供多个不同的hash函数,当R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。 这样做虽然不易产生堆集,但增加了计算的时间。

3.3.3 链地址法(拉链法)

链表地址法是使用一个链表数组,来存储相应数据,当hash遇到冲突的时候依次添加到链表的后面进行处理。

散列通多少钱_散列值_太极散列通 半衰期

链地址在处理的流程如下:

添加一个元素的时候,首先计算元素key的hash值,确定插入数组中的位置。如果当前位置下没有重复数据,则直接添加到当前位置。当遇到冲突的时候,添加到同一个hash值的元素后面,行成一个链表。这个链表的特点是同一个链表上的Hash值相同。java的数据结构HashMap使用的就是这种方法来处理冲突,JDK1.8中,针对链表上的数据超过8条的时候,使用了红黑树进行优化。

3.3.4 建立公共溢出区

将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。

3.3.5 HashMap中的处理冲突

下面是HashMap的put方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                boolean evict) {
    Node[] tab; Node p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0) //如果hash数组为空,初始化一下
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)  //计算落在hash桶的位置,如果当前桶为空,直接新增节点
        tab[i] = newNode(hash, key, value, null);
    else {  //当前桶存在元素
        Node e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k)))) 
            //如果key已经存在,替换元素
            e = p;
        else if (p instanceof TreeNode) //如果当前是树结构了(不是链表了),向树上添加元素
            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
        else {  //当前结构依然时链表,遍历链表,直到末尾或者找到key相同的元素替换
            for (int binCount = 0; ; ++binCount) {
                //到达末尾,新增元素,如果链表长度达到8,转为红黑树
                if ((e = p.next) == null) { 
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //遍历链表的过程中,发现了有key相同的元素,直接替换,然后break
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { //如果是已经存在的元素,判断是否替换(onlyIfAbsent)
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //如果容量超过阈值,扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

3.3.6 HashMap中的扩容机制

当HashMap中元素的个数达到了阈值,(capacity * load factor),HashMap中散列值,元素存放的位置与hash数组的个数是有关系的(tab[i = (n – 1) & hash]),所以当发生扩容是,hash数组的个数发生了变化,这个时候,元素也需要重新进行hash计算。

final Node[] resize() {
    Node[] oldTab = table;
    //老的容量和阈值
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    //计算新的容量和阈值
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) = DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr < 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    //构建新的hash桶
    @SuppressWarnings({"rawtypes","unchecked"})
        Node[] newTab = (Node[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        //遍历原由的hash桶,copy元素到新的里面
        for (int j = 0; j < oldCap; ++j) {
            Node e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;   //原来的hash桶置空
                if (e.next == null) //原来位置上只有一个元素(没有链表和树),直接放到新的位置
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode) //如果是树状结构,单独处理
                    ((TreeNode)e).split(this, newTab, j, oldCap);
                else { // preserve order 这里的是链表元素
                    Node loHead = null, loTail = null; //原来index位置
                    Node hiHead = null, hiTail = null; //新的index位置
                    Node next;
                    //循环处理链表元素,判断元素是放在原index位置还是放在新index位置
                    do {
                        next = e.next;
                        //放在原index的位置的元素,loHead拿到链表头,loTail拿到链表尾
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        //放在新index位置的元素,hiHead拿到链表头,hiTail拿到链表尾
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                     //放入原index处
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //放入新index处
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

其中比较有意思的是,由于put时计算hash数组角标是通过i = (n – 1) & hash计算的,其中n是的2的x次幂,扩容时,容量变为原来的两倍,n-1 == (n-1)

限 时 特 惠: 本站每日持续更新海量各大内部创业教程,加站长微信免费获取积分,会员只需38元,全站资源免费下载 点击查看详情
站 长 微 信: thumbxmw