散列表及哈希冲突

一,哈希结构

1.什么是哈希

我们知道数组这种数据结构的查询效率是高的,知道数组下标的查询时间复杂度能到O(1),哈希,也叫作散列,散列在查询上有点类似数组查询的思想,根据下标直接定位到元素位置,获取元素值,与数组下标的固定是从0开始不断递增1不同的是,散列的下标是通过散列函数根据元素映射得到,最理想的散列是,每个位置只对应一个元素,且表的大小刚好合适,哈希是一种在插入,删除,查询都很高效的数据结构

2.为什么需要哈希

相信大家多多少少肯定会对查找算法有些了解,比如顺序查找、二分查找、利用“二叉搜索树”查找等等。顺序查找适用于数据量不大的时候;二分查找虽然时间复杂度较为乐观,但前提是数据元素已经按照关键字排序并且存储在连续的地址空间中;二叉搜索树查找具有相当不错的时间复杂度,但有时需要附加条件。那这样我们就需要一种适应性广而速度又快的查找算法。那这种查询算法就是基于哈希这种数据结构

二,哈希函数

哈希函数编写

  • 1.直接定址法
  • 关键字或关键字的一个线性函数值作为地址

    f(key) = a × key + b

    f(key) = key

  • 2.除余法
  • 一般是大于20的质数,p通常要小于表大小m,取余数为地址

    H(key) = key % p, p < m

  • 3.折叠法
  • 将关键字分为位数相同的几部分,然后取这几部分的叠加和(舍去进位)作为散列地址。用于关键字位数较多,并且关键字中每一位上数字分布大致均匀。

  • 4.提取法
  • 取关键字的某些部分作为地址

    h(key) = x

  • 5.平方取中法
  • 关键字被平方后去中间部分作为地址

    f(key) = h(key^2) h(x): 取中间部分

    三,哈希冲突

    刚刚我也提到了,最理想的散列是一个下标对应一个元素,像数组那样,但是,现实中肯定会存在两个元素经过散列函数得到下标值一样,即在散列表中的位置是一样的,那这样不就产生冲突了吗?对,这种就称为哈希冲突。

    解决哈希冲突的方法

    常用的解决哈希冲突的方法可以分为两大类:

    • 开放定址法

      • 线性探测法
      • 平方探测法
      • 双散列法
      • 再散列法
    • 分离链接法

    开放定址法

    所谓开放地址法,就是一旦产生了冲突,即该地址已经存放了其他数据元素,就去寻找另一个空的散列地址。在没有装满的散列表中,空的散列地址是有的,但是怎么去找,这是我们应该考虑的因素之一

    一般来说,如果关键词经过散列函数得出散列地址后发现有冲突,那么就需要试探性散列函数去试试其他附近的散列地址是否是空的。如果还发生冲突的话就再次使用试探性散列函数再去试试其他散列地址

    这些试探性函数就是上面提到的那些,在这里重点说下线性探测法

    线性探测法

    基本思想是:

    探查时从地址 d 开始,首先探查 T[d],然后依次探查 T[d+1],…,直到 T[m-1],此后又循环到 T[0],T[1],…,直到探查到 有空余地址 或者到 T[d-1]为止。

    代码实现:

    散列表每一个位置的情况

        //存放元素
        private static class HashEntry{
            public Object element;
            public boolean isActive;//false:该元素已被删除
    
            public HashEntry(Object element, boolean isActive) {
                this.element = element;
                this.isActive = isActive;
            }
    
            public HashEntry(Object element){
                this(element,true);
            }
        }
    

    构造散列表,其实就是很多个hashEntry连接起来,形成数组结构

        private static int HASHTABLE_SIZE = 11;
        //散列表
        private HashEntry[] array;
        private int currentSize;
        
        public QuadraticProbingHashTable(int currentSize) {
            //分配数组大小
            allocateArray(currentSize);
            //每一项置空,现在是空的散列表
            makeEmpty();
        }
    
        public QuadraticProbingHashTable() {
            this(HASHTABLE_SIZE);
        }
    
        /**
         * 分配数组大小
         * @param currentSize
         */
        private void allocateArray(int currentSize) {
            array = new HashEntry[nextPrime(currentSize)];
        }
    
        /**
         * 散列表变为空散列表
         */
        private void makeEmpty() {
            currentSize = 0;
            for(int i = 0;i<array.length;i++){
                array[i] = null;
            }
        }
    

    接下来进行各种操作,包括插入,删除,即哈希函数

    哈希函数

        /**
         * 返回哈希表位置(相当于之前的散列函数)
         * @param x
         * @return
         */
        public int findPos(Object x) {
            //先取一下哈希值
            int currentPos = hash(x);
            //偏移
            int offset = 1;
            //线性探测法
            while(array[currentPos] != null && !array[currentPos].element.equals(x)){
                currentPos += offset;
                offset += 2;
                if(currentPos >= array.length){
                    currentPos -= array.length;
                }
            }
            return currentPos;
        }
    
        /**
         * 哈希函数
         * @param key
         * @return
         */
        private int hash(Object key){
            int hashCode = key.hashCode();
            hashCode %= array.length;
            if(hashCode <= 0){
                hashCode += array.length;
            }
            return hashCode;
        }
    

    在这里的findPos方法中的while循环其实就是实现线性探测法,不断向前探测是否有空闲位置,必要时可以回到散列表开始处,继续向前

    插入

    
        /**
         * 插入元素到散列表
         * @param x
         */
        public void insert(Object x){
            //获取在散列表的下标,相当于之前的散列函数
            int current = findPos(x);
            //看是否被删除过,如果还存在,就什么都不做,直接返回
            if(isActive(current)){
                return;
            }
            array[current] = new HashEntry(x);
            //装填因子不宜大于0.5
            if(currentSize >= array.length/2){
                //大于则扩容哈希表
                grow();
            }
        }
    
        /**
         * 判断该元素是否被删除过,false:已被删除
         * @param currentPos
         * @return
         */
        public boolean isActive(int currentPos) {
            return array[currentPos] != null && array[currentPos].isActive;
        }
    
        /**
         * 扩容
         */
        private void grow() {
            HashEntry[] oldArray = array;
            allocateArray(nextPrime(array.length * 2));
            currentSize = 0;
            for(int i = 0;i<array.length;i++){
                if(oldArray[i]!=null && oldArray[i].isActive){
                    insert(oldArray[i]);
                }
            }
        }
    
        /**
         * 散列表是否包含该元素
         * @param x
         * @return
         */
        public boolean contains(Object x){
            //找到在散列表的位置
            int currentPos = findPos(x);
            //判断元素是否被删除过
            return isActive(currentPos);
        }
    
        /**
         * 获取下一个素数
         * @param num
         */
        private static int nextPrime(int num){
            for(int i = num + 1;; i++)
            {
                boolean isPrime = true;
                for(int j = 2; j < i; j++)
                {
                    if(i % j == 0)
                    {
                        isPrime = false;
                        break;
                    }
                }
                if(isPrime)
                {
                    System.out.println(num + " 后面的第一个素数是: " + i);
                    System.out.println();
                    break;
                }
            }
            return num;
        }
    

    删除

        /**
         * 删除
         * @param x
         */
        public void remove(Object x){
            int current = findPos(x);
            if(!isActive(current)){
                array[current].isActive = false;
            }
        }
    

    分离链接法

    分离链接法(Separate Chaining)是解决冲突的另一种方法,其做法是将所有关键词为同义词(散列地址一致的关键词)的数据对象通过结点链接存储在同一个单链表中。

    在这里插入图片描述

    代码实现:

    基于linkedList构造哈希表

        //默认表大小
        private static int HASHTABLE_SIZE = 101;
    
        //开放定址法实际上就是链表的一个数组集合
        private LinkedList[] myLists;
    
        //哈希表大小
        private int size;
    
        public SeparateChainingHashTable(){
            this(HASHTABLE_SIZE);
        }
    
        /**
         * 基于linkedList构造哈希表
         * @param size
         */
        public SeparateChainingHashTable(int size){
            myLists = new LinkedList[nextPrime(size)];
            for(int i = 0;i < myLists.length;i++){
                myLists[i] = new LinkedList<>();
            }
        }
    

    插入:

        /**
         * 插入指定元素到哈希表中
         * @param key
         */
        public void insert(Object key){
            int hashCode = hash(key);
            if(hashCode > myLists.length){
                grow();
            }
            LinkedList linkedList = myLists[hashCode];
            if(!contains(key)){
                linkedList.add(key);
                if(++size > myLists.length){
                    grow();
                }
            }
        }
    
        /**
         * 哈希表扩容(数组扩容)
         */
        private void grow() {
            LinkedList[] oldList = myLists;
            myLists = new LinkedList[nextPrime(2 * myLists.length)];
            for(int i = 0;i < myLists.length;i++){
                myLists[i] = new LinkedList();
            }
            size = 0;
            for(int i = 0;i < myLists.length;i++){
                for (Object item : oldList[i]){
                    insert(item);
                }
            }
        }
    
        /**
         * 哈希函数
         * @param key
         * @return
         */
        private int hash(Object key){
            int hashCode = key.hashCode();
            hashCode %= myLists.length;
            if(hashCode <= 0){
                hashCode += myLists.length;
            }
            return hashCode;
        }
    
        /**
         * 获取下一个素数
         * @param num
         */
        private static int nextPrime(int num){
            for(int i = num + 1;; i++)
            {
                boolean isPrime = true;
                for(int j = 2; j < i; j++)
                {
                    if(i % j == 0)
                    {
                        isPrime = false;
                        break;
                    }
                }
                if(isPrime)
                {
                    System.out.println(num + " 后面的第一个素数是: " + i);
                    System.out.println();
                    break;
                }
            }
            return num;
        }
    

    删除:

        /**
         * 删除指定元素
         * @param key
         */
        public void remove(Object key){
            LinkedList linkedList = myLists[hash(key)];
            if(linkedList.contains(key)){
                linkedList.remove(key);
                size--;
            }
        }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值