聊聊Hash

最近近在看java里面ThreadLocal里面对于ThreadMap的查找使用的是Hash算法,还提到了神奇数字,于是深入的看了hash这个东西,下面就来掰扯一下hash的原理,搞懂原理了,其实对于hashmap或者hashset这种类也就很好理解,无非就是算法选择的不同而已.

首先一个问题:hash是什么?

其实hash应该叫哈希函数,就是从给定的一个key开始,根据一定的算法来计算出一个唯一值即:f(Key)=hashCode,要求是通过f的函数计算后key和这个hash值是保持一对一的唯一关系.很多地方说适用于寻址,这也是用法之一,像MD5这种摘要算法就是仅仅用于校验的.

用程序来说明一下hash是做什么用的:

先定义一个接口,就两个方法,一个是生成hash值一个是通过hash值获取对象.

	public interface HashInterface {
		/* 生成hash */
		public int hash(Object value);
		/* 通过hash获取对象,可以看成是寻址的过程 */
		public Object get(int hash);
	}

现在实现一个采用直接寻址法的hash方法:

public class Hash implements HashInterface {

        private final Object[]  tables;

        public Hash1(int tablesSize) {
            tables = new Object[tablesSize];
        }
        /*此处是其实下标,如果数组要的前几位需要被预留,则seed射为预留的个数*/
        private int hashSeed    = 0;

        @Override
        public int hash(Object value) {
            if (hashSeed == tables.length - 1) {
                throw new IndexOutOfBoundsException("已经超出hash范围");
            }
            int hash = hashSeed++;
            tables[hash] = value;
            return hash;
        }

        @Override
        public Object get(int hash) {
            return tables[hash];
        }
    }

是不是看了代码以后觉得这就是数组里面按照顺序放进去,然后返回的hash值就是数组下标嘛.基本上最简单的hash就是这样的,它的数学原型就是f(x)=a*x+b,简单吧.举这个例子,只是想说明一下hash主要是保证在规定的范围内Key对应hash值不重复,这里要注意的一个是"限定的存储(哈希表)范围",还有就是"不重复".原理就是这样的,基本上你的算法实现在hash(Object value)里面随意变换算法,只要保证在限定范围内计算出的hash是唯一的就可以了.具体可以google一下hash,有很多种实现算法.

但是要保证计算出来的hash值不重复貌似真的很难,数学家也没法保证f(x)!=f(x1),我们把这种不一样的key但是最后算出来一样的hash值的现象叫做碰撞,为了解决碰撞又有很多种方式,什么开地址发,什么线性探测再散列,反正我不是研究数学的,只要知道是怎么做的就行了,看下面的例子:

public class Hash2 implements HashInterface {

        private final Object[]  tables;

        private int             mod;
        /**
         * 再探测地址增量
         */
        private int             d;

        private int             hashSeed    = 0;

        public Hash2(int tablesSize) {
            tables = new Object[tablesSize];
            mod = tablesSize;
        }

        @Override
        public int hash(Object value) {
            int index = (value.hashCode() + d) % mod;
            if (tables[index] != null) {
                if (d == mod) {
                    throw new IndexOutOfBoundsException("已经超出hash范围");
                }
                d++;
                return hash(value);
            }
            return index;
        }

        @Override
        public Object get(int hash) {
            return tables[hash];
        }

    }

这个例子里面计算hash的方式是采用取余的方式计算hash,但是不能保证每次取余的结果都不一样,比如32和52对10取余都是2,这个时候怎么办,我们增加一个增量地址,如果发现2这个hash位置上已经有值了我就把d++,看看3上面有没有值,没有的话就直接填入,并返回3为hash值.其他的避免碰撞的方法很多,如果要深入的研究的话请自己去查资料.

基本上讲到这里最简单的hash都将完了,大家对hash应该有一个初步的概念了,而不是单纯的理论,在java的ThreadLocal里面有一个神奇的hash数字0x61c88647,这个有点类似黄金分割的味道,可以保证hash值是均匀散列的,看下面的代码:

static int  HASH_INCREMENT  = 0x61c88647;
    static int  hash            = 0;

    private static int nextHash() {
        int i = hash;
        hash = i + HASH_INCREMENT;
        return i;
    }
    public static void main(String[] args) {
        for (int j = 0; j < 4; j++) {
            int size = 2 << j;
            // hash = 0;
            int[] indexArray = new int[size];
            for (int i = 0; i < size; i++) {
                indexArray[i] = nextHash() & (size - 1);
            }
            System.out.println(Arrays.toString(indexArray));
        }
    }

结果是:

```

[0, 1]

[2, 1, 0, 3]

[2, 1, 0, 7, 6, 5, 4, 3]

[2, 9, 0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11]

```

我没有看到重复的索引值(你完全可以把这个看作是hash值),只要哈希表的大小是2的N次方,那么基本上可以保证每次计算出的hash值都不会重复,但是我不保证例外,哈哈

其实上面的代码里面还有一个bug,就是在hash表动态扩展大小的时候,很大的可能导致hash碰撞,所以再每次计算出hash值以后需要再判断一下hash对应下标的位置有没有值,如果有值的话需要再此计算hash.

总的说来,hash的效率还是比较高的,总比线性的一个一个判断是否有值要高效很多,就算发生碰撞了,再计算一次也很容易找到位置为空的地方存放数据.

基本上关于hash的东西就这么些,还有hashMap,hashSet这些也都是用到到hash的原理来处理数据的索引,下次再看到的时候我再补充一篇专门针对hashMap和HashSet的文章来说明一下.

时间: 2024-10-23 17:39:52

聊聊Hash的相关文章

聊聊高并发(三十二)实现一个基于链表的无锁Set集合

Set表示一种没有反复元素的集合类,在JDK里面有HashSet的实现,底层是基于HashMap来实现的.这里实现一个简化版本号的Set,有下面约束: 1. 基于链表实现.链表节点依照对象的hashCode()顺序由小到大从Head到Tail排列. 2. 如果对象的hashCode()是唯一的.这个如果实际上是不成立的,这里为了简化实现做这个如果.实际情况是HashCode是基于对象地址进行的一次Hash操作.目的是把对象依据Hash散开.所以可能有多个对象地址相应到一个HashCode.也就是

聊聊MySQL、HBase、ES的特点和区别

互联网时代各种存储框架层出不穷,眼花缭乱,比如传统的关系型数据库:Oracle.MySQL:新兴的NoSQL:HBase.Cassandra.Redis:全文检索框架:ES.Solr等.如何为自己的业务选取合适的存储方案,相信大家都思考过这个问题,本文简单聊聊我对MySQL.HBase.ES的理解,希望能和大家一起探讨进步,有不对的地方还请指出. MySQL:关系型数据库,主要面向OLTP,支持事务,支持二级索引,支持SQL,支持主从.group replication架构模型(本文全部以Inn

一致性hash算法 - consistent hashing

1.背景 我们都知道memcached服务器是不提供分布式功能的,memcached的分布式完全是由客户端来实现的.在部署memcached服务器集群时,我们需要把缓存请求尽可能分散到不同的缓存服务器中,这样可以使得所有的缓存空间都得到利用,而且可以降低单独一台缓存服务器的压力.     最简单的一种实现是,缓存请求时通过计算key的哈希值,取模后映射到不同的memcahed服务器.这种简单的实现在不考虑集群机器动态变化的情况下也是比较有效的一种方案,但是,在分布式集群系统中,简单取模的哈希算法

BZOJ3198 SDOI2013 spring HASH+容斥原理

题意:给定6个长度为n的数列,求有多少个数对(i,j)((i,j)≡(j,i))使得i和j位置恰好有K个数相同,其中0≤K≤6 题解: 设fi=至少有K个数相同的位置对的数量,用2^6枚举每一种可能,根据容斥原理,答案就是\[\sum\limits_{i = K}^N {{f_i}C_i^K{{\left( { - 1} \right)}^{i - K}}} \] 至于多乘一个组合数,是因为当前枚举到有x个数相同,一个位置对有i个相同的数,那么累计的时候就会算成$C_x^i$,因此实际上这个位置

hash算法搜索获得api函数地址的实现

我们一般要获得一个函数的地址,通常采用的是明文,例如定义一个api函数字符串"MessageBoxA",然后在GetProcAddress函数中一个字节一个字节进行比较.这样弊端很多,例如如果我们定义一个杀毒软件比较敏感的api函数字符串,那么可能就会增加杀毒软件对我们的程序的判定值,而且定义这些字符串还有一个弊端是占用的字节数较大.我们想想如何我们的api函数字符串通过算法将它定义成一个4字节的值,然后在GetProcAddress中把AddressOfNames表中的每个地址指向的

BZOJ_3207_花神的嘲讽计划1_(Hash+主席树)

描述 http://www.lydsy.com/JudgeOnline/problem.php?id=3207 给出一个长度为\(n\)的串,以及\(m\)个长度为\(k\)的串,求每个长度为\(k\)的串在原串\([x,y]\)区间是否出现过. 分析 这道题要求对比长度为\(k\)的串,于是我们把这些串的Hash值都算出来,问题就转化成了求\([x,y]\)的区间中是否出现过某Hash值. 求区间中某一个值出现了多少次,可以用主席树. p.s. 1.学习了主席树指针的写法,比数组慢好多啊...

Hash算法专题

1.[HDU 3068]最长回文 题意:求一个字符串(len<=110000)的最长回文串 解题思路:一般解法是manacher,但是这一题用hash也是可以ac的 假设当前判断的是以i为中心偶数最长回文串,那么s[2*i+1-k……i]与s[i+1……k]的哈希值必定相同 假设当前判断的是以i为中心奇数最长回文串,那么s[2*i-k……i-1]与s[i+1……k]的哈希值必定相同 用二分求出相应的k 1 #include <iostream> 2 #include <algori

Salted hash password

参考文档 http://www.cnblogs.com/richardlee/articles/2511321.html https://en.wikipedia.org/wiki/Salt_%28cryptography%29 https://www.91ri.org/7593.html 密码存储为什么不能是明文? 当账户密码是明文存储的话, 万一本网站给黑客攻破获取了数据, 则用户的账户被泄露.(术语叫 拖库) 当黑客知道了你的账户后, 其可以使用此账户,到其他网站尝试访问, 例如有厉害关系

Golang Hash MD4

//Go标准包中只有MD5的实现 //还好,github上有MD4实现. package main import (     "golang.org/x/crypto/md4"     "encoding/hex"     "fmt" ) func get_md4(buf []byte) ([] byte) { ctx := md4.New() ctx.Write(buf) return ctx.Sum(nil) } func main() {