zoukankan      html  css  js  c++  java
  • 哈希表(一)

    哈希表是一种数据结构,它可以提供快速的插入和删除操作。无论哈希表有多少数据,插入、删除只需要接近常量的时间,即 O(1) 的时间级。明显比树还快,树的操作通常需要O(N)的时间级。

    缺点:它是基于数组的,数组创建之后难以维护。某些哈希表被基本填满时,性能下降非常严重。而且也没有提供一种方法可以以任何一种顺序(例如从大到小)遍历表中数据项。

    若需把单词当做key(数组下标)获取value(数据),可以把单词分解成字母组合,把字母转化为它们的数字代码(a-1,b-2,c-3……z-26,空格-27),每个数字乘以相应的27(因为字母有27种可能,包括空格)的幂,然后结果相加,就可以每个单词对应一个独一无二的数字。

    例如 cats 转换数字:3*273 + 1*272 + 20*271 + 19*270 = 60337

    这种方案会使得数组的长度太大,而且只有很少的一部分下标是有数据的。

    哈希化

    arrayIndex = hugeNumber % arraySize,这是一种哈希函数,它把一个大范围的数字哈希(转化)成一个小数字的范围。

    使用取余操作符(%),把巨大的整数范围转换为两倍于要存储内容的数组下标范围。下面是哈希函数的例子:

    arraySize = wordNumber * 2; 
    arrayIndex = hugeNumber % arraySize;

    期待的数组应该有这样的特点:平均起来,每两个数组单元,就有一个数值,有些单元没有数值,但有些单元可能有多个数值。


    冲突

    把巨大的数字空间压缩为较小的数字空间,必然要付出代价,即不能保证每个单词都映射到数组的空白单元。假设在数组中需要插入单词zoo,哈希化之后得到它的下标,发现该单元已经有了其它另一个不同的单词,这个情况叫做“冲突”。

    解决方案1 - 开放地址法

    前面已经提过指定的数组大小两倍于需要存储的数据量,因此还有一半单元是空白的。当发生冲突的时候,通过系统的方法找到数组的一个空位,并把单词放进去,而不再用哈希函数得到的数组下标,这种方法叫做“开放地址法”。

    解决方案2 - 链地址法

    创建一个存放单词链表的数组,数组内不直接存储单词,这样,但发生冲突的时候,新的数据项直接接到这个数组下标所指的链表当中。这种方法叫做“链地址法”。


    开放地址法

    寻找数组的其它位置有三种方法:线性探测、二次探测、再哈希法。

    1)线性探测

    线性查找空白单元,如果 21 是要插入数据的位置,它已经被占用了。那么就使用 22 ,然后是 23 ,以此类推,数组下标一直递增,直到找到空位为止。

    插入(insert)

    当数据项的数目占哈希表的一半,或最多三分之二时,哈希表的性能是最好的。可以看出已填充的单元分布不均匀,有时一串空白单元,有时有一串已填充的单元。

    在哈希表中,一串连续的已填充单元叫做“填充序列”。增加越来越多的数据项时,填充序列变得越来越长,这叫做“聚集”。

    删除(Delete)

    在哈希表中,查找算法是以哈希化的关键字开始,沿着数组一个一个寻找,如果在寻找到关键字之前遇到一个空白单元,说明查找失败。

    delete不是简单地把某个单元的数据项变为空白(null),因为在一个填充序列中间有个空白,查找算法就会中途放弃查找。因此需要一个有特殊关键字的数据项代替要被delete的数据项。标记数据项不存在。

    public class DataItem {
    
        private int i;
    
        public DataItem(int i) {
            this.i = i;
        }
    
        public int getKey() {
            return i;
        }
    
        public void printf() {
            System.out.println("data -> " + i);
        }
    
    }
    public class HashTable {
    
        private DataItem[] itemArray;
    
        private int arraySize;
    
        private DataItem nonItem; // for deleted items
    
        public HashTable(int size) {
            this.arraySize = size;
            itemArray = new DataItem[arraySize];
            nonItem = new DataItem(-1);
        }
    
        public void display() {
            for (DataItem data : itemArray) {
                if (data != null) {
                    data.printf();
                }
            }
        }
    
        public int hashFuc(int key) {
            return key % arraySize;
        }
    
        public void insert(DataItem item) {
            int key = item.getKey();
            int hashVal = hashFuc(key);
            DataItem tItem;
            while ((tItem = itemArray[hashVal]) != null && tItem.getKey() != -1) {
                if (tItem.getKey() == key) {
                    itemArray[hashVal] = item;
                    return;
                }
                hashVal++; // go to next cell
                hashVal %= arraySize; // wraparound if necessary
            }
            itemArray[hashVal] = item;
        }
    
        public DataItem delete(int key) {
            int hashVal = hashFuc(key);
            DataItem item;
            while ((item = itemArray[hashVal]) != null) { // until empty cell
                if (item.getKey() == key) {
                    itemArray[hashVal] = nonItem;
                    return item;
                }
                hashVal++;
                hashVal %= arraySize;
            }
            return null;
        }
    
        public DataItem find(int key) {
            int hashVal = hashFuc(key);
            DataItem item;
            while ((item = itemArray[hashVal]) != null) { // until empty cell
                if (item.getKey() == key) {
                    return item;
                }
                hashVal++;
                hashVal %= arraySize;
            }
            return null;
        }
    
    }
        public static void main(String[] args) {
            HashTable t = new HashTable(10);
            t.insert(new DataItem(39));
            t.insert(new DataItem(51));
            t.insert(new DataItem(23));
            t.insert(new DataItem(25));
            t.insert(new DataItem(23));
            t.insert(new DataItem(10));
            t.insert(new DataItem(9));
            t.delete(25);
            t.insert(new DataItem(79));
            t.insert(new DataItem(81));
            t.display();
        }

    打印结果:
    data -> 10
    data -> 51
    data -> 9
    data -> 23
    data -> 79
    data -> 81
    data -> 39


    扩展数组

    当哈希表太满,需要扩展数组。只能创建一个新的更大的数组,然后把旧的数组所有数据项插入到新的数组。由于哈希函数是根据数组的大小计算数据项的位置,所以不能简单把一个数据项插入新的数组,需要按顺序遍历旧的数组,然后调用 insert()向新的数组插入每个数据项。这叫做“重新哈希化”。

    扩展后的数组容量通常是原来的两倍,实际上数组的容量应该是一个质数,所以新的数组要比两倍容量多一点。

    好的HASH函数需要把原始数据均匀地分布到HASH数组里,比如大部分是偶数,这时候如果HASH数组容量是偶数,容易使原始数据HASH后不会均匀分布:

    2 4 6 8 10 12这6个数,如果对 6 取余 得到 2 4 0 2 4 0 只会得到3种HASH值,冲突会很多。如果对 7 取余 得到 2 4 6 1 3 5 得到6种HASH值,没有冲突。

    同样地,如果数据都是3的倍数,而HASH数组容量是3的倍数,HASH后也容易有冲突,用一个质数则会减少冲突的概率,更分散。

    以下是求质数的代码:

        private int getPrime(int min) {
            for (int j = min;; j++) {
                if (isPrime(j)) {
                    return j;
                }
            }
        }
    
        private boolean isPrime(int num) {
            for (int j = 2; j * j <= num; j++) {
                if (num % j == 0) {
                    return false;
                }
            }
            return true;
        }


    3)二次探测

    在线性探测中会发生聚集,一旦聚集形成,它会越来越大,哈希化后的落在聚集范围内的数据项都要一步步移动,性能越差。

    装填因子:已填入哈希表的数据项和表长的比率叫做装填因子。loadFactor = nItems / arraySize ;

    二次探测是防止聚集的产生,思想是探测相隔较远的单元,而不是相邻的单元。

    步骤是步数的平方:假设哈希表中原始下标是x,那么线性探测是:x+1,x+2,x+3……;而在二次探测中,探测过程是:x+12,x+22,x+32……。

    二次探测消除了在线性探测产生的聚集问题,这种聚集问题叫做“原始聚集”。然而二次探测产生了另外一种更细的聚集问题。之所以会发生,是因为所有映射到同一个位置的关键字在寻找空位时,探测的单元都是一样的(步长总是固定的,都是:1、4、9、16、25、36……)。


    4)再哈希法

    为了消除原始聚集和二次聚集,可使用另一种方法:再哈希法。现在需要的一种方法产生一种依赖关键字的探测序列,而不是每个关键字都一样,那么,不同的关键字即使映射到相同的数组下标,也可以使用不同的探测序列。

    方法是把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长。对于指定的关键字,步长在整个探测是不变的,不过不同关键字使用不同步长。

    经验说明,第二哈希函数必须具备以下条件:

    1. 与第一个哈希函数不同
    2. 不能输入0(否则没有步长,每次探测都原地踏步,死循环。)

    stepSize = constant - (key % constant),其中 constant 是质数,且小于数组容量。例如:stepSize = 5 - key % 5 ;

    public class HashTable2 {
    
        private DataItem[] itemArray;
    
        private int arraySize;
    
        private DataItem nonItem; // for deleted items
    
        public HashTable2(int size) {
            this.arraySize = size;
            itemArray = new DataItem[arraySize];
            nonItem = new DataItem(-1);
        }
    
        public void display() {
            for (DataItem data : itemArray) {
                if (data != null) {
                    data.printf();
                }
            }
        }
    
        public int hashFuc1(int key) {
            return key % arraySize;
        }
    
        public int hashFuc2(int key) {
            /*
             * non-zero, less than array size, different from hashFuc1. array size
             * must be relatively prime to 5, 4, 3, 2
             */
            return 5 - key % 5;
        }
    
        public void insert(DataItem item) {
            int key = item.getKey();
            int hashVal = hashFuc1(key);
            int stepSize = hashFuc2(key);
            DataItem tItem;
            while ((tItem = itemArray[hashVal]) != null && tItem.getKey() != -1) {
                if (tItem.getKey() == key) {
                    itemArray[hashVal] = item;
                    return;
                }
                hashVal += stepSize; // add the step
                hashVal %= arraySize; // wraparound if necessary
            }
            itemArray[hashVal] = item;
        }
    
        public DataItem delete(int key) {
            int hashVal = hashFuc1(key);
            int stepSize = hashFuc2(key);
            DataItem item;
            while ((item = itemArray[hashVal]) != null) { // until empty cell
                if (item.getKey() == key) {
                    itemArray[hashVal] = nonItem;
                    return item;
                }
                hashVal += stepSize;
                hashVal %= arraySize;
            }
            return null;
        }
    
        public DataItem find(int key) {
            int hashVal = hashFuc1(key);
            int stepSize = hashFuc2(key);
            DataItem item;
            while ((item = itemArray[hashVal]) != null) { // until empty cell
                if (item.getKey() == key) {
                    return item;
                }
                hashVal += stepSize;
                hashVal %= arraySize;
            }
            return null;
        }
    
    }

    表的容量必须是一个质数

    再哈希法要求表的容量是一个质数。为什么会有这个限制,假设表的容量不是质数,表长是15(坐标 0 - 14),有一个特别关键字映射到0,步长为5,探测序列为0、5、10、0、5……,一直循环下去,算法只会尝试这三个单元,不可能找到其它空白单元,算法崩溃。

    如果数组容量是13,即一个质数,那么探测序列会访问到所有单元。即0、5、10、2、7、12、4、9、1、6、11、3,一直下去,只要表中有一个空位,就可以探测到它。用质数作为数组容量使得任何数想整除它是不可能的,因此探测序列最终会检查到所有单元。

  • 相关阅读:
    GIT基础详解
    JS进阶解析
    JS基础解析
    CSS布局模型解析
    02.CentOS Linux 7.7 系统配置文档
    docker 创建bridge网络和修改默认网段
    selenium浏览器自动化测试工具 进阶使用
    前端导出Excel和打印介绍
    stm32使用gmtime()转换timestamp为日期,出的结果是乱的,不符合预期。改为localtime正常输出
    .net core api action 不能用作 httpget注释的参数名
  • 原文地址:https://www.cnblogs.com/xuekyo/p/2911203.html
Copyright © 2011-2022 走看看