首页 > 代码库 > 聊聊Hash

聊聊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的文章来说明一下.

聊聊Hash