哈希表

对于一堆数据的操作,有插入,删除,和查找。红黑树当然是一种解决方案,这一节,我们介绍另外一种思路,那就是散列表。散列表(HashTable)的背后也是分治的思想。其实散列表本身很简单,基本的数据结构教材里都讲得很详细。但我觉得,学习散列表最重要的还是要体会它与红黑树等树形结构的本质不同。树形结构利用的是数据之间的有序关系,也就是说红黑树所组织的数据必须是可比较大小(或者是定出先后顺序的),而散列表则完全不同于树形结构,它对数据的要求仅仅是可以计算特征。

在树型结构中,查找的效率依赖于查找过程中所进行的比较次数。理想的情况是希望不经过任何比较,一次存取便能得到所查记录,那就必须在记录的存储位置和它的关键字之间建立一个确定的对应关系 f, 使每个关键字和结构中一个惟一的存储位置相对应。因而在查找时,只要根据这个对应关系 f 找到给定值 K 的函数值 f(K)。若结构中存在关键字和 K 相等的记录。在此,我们称这个对应关系 f 为哈希 (Hash) 函数,按这个思想建立的表为哈希表。

举一个例子。假如我们十个仓库,对货物都编上号码,如果货物号的个位是0,就放到0号仓库,个位是1,就放到1号仓库。这样,我们就建立了一个货物编号与仓库之间的对应关系:

f(id) = id % 10

这种哈希函数一般称为除留余数法,当然还有其他的构造方式,我们这里就不再介绍了。有兴趣的可以自己去查(这不是一个重要的知识点)

处理冲突

还是考虑仓库的例子。如果我们已经把 3 号货物送到了 3 号仓库,这时我们想从仓库中取出 3 号货物,就非常简单,只要直接去 3 号仓库中去取就好了。我们考虑使用代码来表示,用一个数组代表仓库:

    int[] warehouse = new int[10];

那么,往里面存数据的函数就可以写成:

    int hash(int id) {
        return id % 10;
    }

    void put(id) {
        warehouse[hash(id)] = id;
    }

当插入的数据是11, 13, 25, 46, 38时,一切都非常好,所有的货物都能找到自己对应的仓库。但如果这时,又来了一个21,31,就有问题了。这时就会发现,1号仓库已经被11占据了。这种情况就是发生了冲突了。

选择一个更加均匀的哈希函数可以减少冲突,但不能完全避免,所以如果处理冲突是哈希表不可缺少的一方面。

我们今天要介绍解决冲突的方法是链地址法。其他的方法就不再介绍了。链地址法的思路非常简单,那就是把原来容量为1的仓库,变成一个单链表就可以了,看下面的图:

这个数据结构所对应的代码,我们明天再来分析,今天只看一个使用哈希表来解决问题的例子。

应用举例

看一道面试题吧。

给定方程a_1{x_1}^3 + a_2{x_2}^3 + a_3{x_3}^3+ a_4{x_4}^3+ a_5{x_5}^3 = 0,请写一个程序,从控制台输入a_1,a_2,a_3,a_4,a_5,然后求共有多少整数解,其中限定-50 \le x_1,x_2, x_3, x_4, x_5 < 50

这道题目肯定不能用暴力枚举,100^5的计算量肯定是不行的。第一步,我们可以把方程 左边的第四项和第五项进行移项,则方程就变成a_1{x_1}^3 + a_2{x_2}^3 + a_3{x_3}^3 = -( a_4{x_4}^3+ a_5{x_5}^3)。如果等式的左右两部分相等,那么就找到了原方程的一个解。通过这样的转换,这道题目就从枚举变成了查找。也就是从前一项的所有结果中查找后一项的结果。前一项的结果共有一百万个数,而后一项只有一万个数,这样我们可以考虑使用散列表对数据进行组织。如果后一项的某一次计算结果是A,那么要在散列表中找出一共有多少个A。

解决这种问题,散列表比红黑树有更高的效率。

    public static void solve(int a1, int a2, int a3, int a4, int a5) {
        HashMap<Integer, Integer> m = new HashMap<>();
        for (int i = -50; i < 50; i++) {
            for (int j = - 50; j < 50; j++) {
                for (int k = -50; k < 50; k++) {
                    int t = a1 * i * i * i + a2 * j * j * j + a3 * k * k * k;
                    Integer p = m.get(t);
                    if (p == null)
                        m.put(t, 1);
                    else
                        m.put(t, p + 1);
                }
            }
        }

        int sum = 0;
        for (int i = -50; i < 50; i++) {
            for (int j = -50; j < 50; j++) {
                int t = a4 * i * i * i +  a5 * j * j * j;
                Integer p = m.get(-t);

                if (p != null)
                    sum += p;
            }
        }

        System.out.print(sum);
    }

好了,今天的讲解就到这里了。今天不留作业了。大家可以看一下java.util.HashMap中的源代码。明天我们来讲HashMap的实现。

上一节课:Unicode字符集与UTF-8编码

下一节课:HashMap源码解析

目录:课程目录

编辑于 2017-03-02

文章被以下专栏收录