小橙书阅读指南(十一)——散列表

算法描述:散列表是一种在时间和空间上做出权衡的查找算法。使用查找算法分为两步。第一步是通过散列函数将被查找的键转化未数组的一个索引。理想情况下,不同的键都能转为不同的索引值。当然,这只是理想情况,所以我们需要面对两个或多个键都被散列到相同索引值的情况。因此,散列查找的第二部就是处理碰撞冲突的过程。

一个比较令人满意的散列函数能够均匀并独立地将所有键散布于0到M-1之间。

一、基于拉链法的散列表

算法图示:

拉链散列表算法的本质是将哈希值相同的键保存在一个普通链表中,当我们需要调整数组长度的时候,需要将所有键在新的数组中重新散列。

代码示例:

import java.util.ArrayList;
import java.util.List;

public class SeparateChainingHashSymbolTable<Key, Value> {
    private int initCapacity; // 初始散列数组的长度
    private int size; // 键值总数
    private int len; // 散列数组的长度
    private SequentialSearchSymbolTable<Key, Value>[] st; // 散列数组

    public SeparateChainingHashSymbolTable(int len) {
        this.len = len;
        this.initCapacity = len;
        st = (SequentialSearchSymbolTable<Key, Value>[]) new SequentialSearchSymbolTable[len];

        for (int i = 0; i < len; i++) {
            st[i] = new SequentialSearchSymbolTable<>();
        }
    }

    public Value get(Key key) {
        int h = hash(key);
        return st[h].get(key);
    }

    public boolean contains(Key key) {
        return get(key) != null;
    }

    public void put(Key key, Value val) {
        // 当包含元素的数量大于散列数组长度10倍时,扩展容量
        if (size > 10 * len) {
            resize(2 * len);
        }
        int h = hash(key);
        if (!contains(key)) {
            size++;
        }
        st[h].put(key, val);
    }

    public void delete(Key key) {
        int h = hash(key);
        if (contains(key)) {
            st[h].delete(key);
            size--;
        }

        if (size > initCapacity && size <= 2 * len) {
            resize(len / 2);
        }
    }

    public Iterable<Key> keys() {
        List<Key> keys = new ArrayList<>();
        for (int i = 0; i < len; i++) {
            for (Key key : st[i].keys()) {
                keys.add(key);
            }
        }
        return keys;
    }

    private void resize(int capacity) {
        SeparateChainingHashSymbolTable<Key, Value> nst = new SeparateChainingHashSymbolTable<>(capacity);
        // 遍历原先散列表中保存的元素,并重新散列进新的散列表
        for (int i = 0; i < len; i++) {
            for (Key key : st[i].keys()) {
                nst.put(key, st[i].get(key));
            }
        }

        this.size = nst.size;
        this.len = nst.len;
        this.st = nst.st;
    }

    /**
     * 散列算法
     *
     * @param key
     * @return
     */
    private int hash(Key key) {
        return (key.hashCode() & 0x7fffffff) % len;
    }
}

二、基于探测法的散列表

这种算法在处理碰撞的时候并非将所有相同哈希键的对象保存在一条链表中而是沿数组向后查找并插入到一个空槽中。

算法图示:

探测法哈希算法的一个比较重要的特征是:当我们需要删除一个键的时候,不能仅仅将数组中对应的位置设置未null,因为这会使得在此位置之后的元素无法被查找。因此,我们需要将簇中被删除的键的右侧的所有键重新散列计算并插入散列表。这个过程会比较复杂。

代码示例:

import java.util.ArrayList;
import java.util.List;

public class LinearProbingHashSymbolTable<Key, Value> {
    private int size;
    private int len;
    private Key[] keys;
    private Value[] vals;

    public LinearProbingHashSymbolTable(int capacity) {
        len = capacity;
        size = 0;
        keys = (Key[]) new Object[capacity];
        vals = (Value[]) new Object[capacity];
    }

    public void put(Key key, Value val) {
        // 始终保证元素数量只占数组长度的50%
        if (size > len / 2) {
            resize(2 * len);
        }
        // 线性碰撞检测
        int h;
        for (h = hash(key); keys[h] != null; h = (h + 1) % len) {
            if (key.equals(keys[h])) {
                vals[h] = val;
                return;
            }
        }
        keys[h] = key;
        vals[h] = val;
        size++;
    }

    public Value get(Key key) {
        for (int h = hash(key); keys[h] != null; h = (h + 1) % len) {
            if (key.equals(keys[h])) {
                return vals[h];
            }
        }
        return null;
    }

    public boolean contains(Key key) {
        return get(key) != null;
    }

    public void delete(Key key) {
        if (!contains(key)) {
            return;
        }

        int h = hash(key);
        while (!keys[h++].equals(key)) {
            h = h % len;
        }
        keys[h] = null;
        vals[h] = null;

        // 由于在删除了一个键之后可能造成查询的不连续,因此需要对一些键重新散列
        h = (h + 1) % len;
        while (keys[h] != null) { // 在被删除的键后至空键前的所有键重新散列保存
            Key nkey = keys[h];
            Value nval = vals[h];
            keys[h] = null;
            vals[h] = null;
            put(nkey, nval);
            h = (h + 1) % len;
            // 每次循环size--的目的时抵消put中的size++
            size--;
        }
        size--;
        // 当包含的元素数量小于数组长度的12.5%时,按照1/2的比例收缩数组
        if (size > 0 && size < len / 8) {
            resize(len / 2);
        }
    }

    public Iterable<Key> keys() {
        List<Key> keyList = new ArrayList<>();
        for (int i = 0; i < len; i++) {
            if (keys[i] != null) {
                keyList.add(keys[i]);
            }
        }
        return keyList;
    }

    private int hash(Key key) {
        return (key.hashCode() & 0x7fffffff) % len;
    }

    private void resize(int capacity) {
        LinearProbingHashSymbolTable<Key, Value> nst = new LinearProbingHashSymbolTable<>(capacity);
        for (int i = 0; i < size; i++) {
            if (keys[i] != null) {
                nst.put(keys[i], vals[i]);
            }
        }

        this.size = nst.size;
        this.keys = nst.keys;
        this.vals = nst.vals;
    }
}

相关链接:

Algorithms for Java

Algorithms for Qt

原文地址:https://www.cnblogs.com/learnhow/p/9693519.html

时间: 2024-10-08 16:45:26

小橙书阅读指南(十一)——散列表的相关文章

小橙书阅读指南(五)——归并排序的两种实现

算法描述:将两个较小的有序数组合并成为一个较大的有序数组是比较容易的事情.我们只需要按照相同的顺序依次比较最左侧的元素,然后交替的放进新数组即可.这就是自顶向下的归并排序的实现思路.与之前的算法不同的是,归并排序需要使用额外的存储空间,用空间换时间的做法也是在排序算法中经常需要做的选择. 算法图示: 算法解释:把一个较大的数组不断划分为较小的两个数组,直到无法再切分之后再做逆向合并,并再合并的过程中调整顺序.归并算法的难点是如何尽可能的减少额外存储空间的使用. Java代码示例: package

小橙书阅读指南(九)——红黑平衡树(2)

从标准二叉树的极端情况我们推导出2-3树这样的数据结构具备自平衡的特性,但是要实现这个特性在算法上相当复杂.考虑在大部分情况下,对于检索的指数级时间消费O(lgN)要求并不严格.因此,我们会看到如何将一颗标准的2-3树转变成红黑树的过程. 一.局部变换 考虑如果在2-节点上挂新的键并不会破坏2-3树的平衡结构.可是在3-节点上挂新的键,可能的变化却多达6种.这个临时的4-节点可能是根节点,可能是一个2-节点的左子节点或者右子节点,也可能是3-节点的左子节点.中子节点或者右子节点.2-3树插入算法

小橙书阅读指南(二)——选择排序

算法描述:一种最简单的排序算法是这样的:首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置.再次,再剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置.如此往复,知道将整个数组排序.这种方法叫做选择排序,因为它在不断地选择剩余元素之中的最小者. 算法图示: Java代码示例: import common.ArraysGenerator; import common.Sortable; import java.io.IOException; import java.uti

小橙书阅读指南(三)——插入排序

算法描述:通常人们在整理扑克的方法是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置.在算法的实现中,为了给要插入的元素腾出1个空间,我们需要将其余所有元素在插入之前都向右移动1位.这种算法叫插入算法. 算法图示: 算法解释:在基础版本中通常的做法是,当新元素需要被插入有序数组的时候,从右向左依次交换.直到新元素到达它合适的位置. Java代码示例: import common.ArraysGenerator; import common.Sortable; import java.i

小橙书阅读指南(六)——快速排序和三向切分快速排序

算法描述:快速排序是一种分治的排序算法.它将数组分为两个子数组,并将两部分独立的排列.快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将子数组归并以将整个数组排序:而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了. 算法图示: 算法解释:选择标的元素(5)并且便利数组,将素有小于5的元素都安排在它的左侧,而大于5的元素都安排在它的右侧.之后再通过递归的方法分别处理左边的子数组和右边的子数组. 快速排序的算法难点在于尽量不要使用额外的存储空间(即保证原地

小橙书阅读指南(七)——优先队列和索引优先队列

算法描述:许多应用程序都需要按照顺序处理任务,但是不一定要求他们全部有序,或是不一定要一次就将他们排序.很多情况下我们只需要处理当前最紧急或拥有最高优先级的任务就可以了.面对这样的需求,优先队列算法是一个不错的选择. 算法图示: 算法解释:上图所展示的是最大优先队列(大顶堆)的算法逻辑,在这个标准的二叉树中,任意节点的元素都大于其叶子节点的元素.利用数组表示该二叉树即Array[2]和Array[3]是Array[1]的叶子节点,Array[4]和Array[5]是Array[2]的叶子节点,A

小橙书阅读指南(十)——二叉查找树

算法描述:二叉查找树时一种能够将链表插入的灵活性和有序数组查找的高效性结合起来的符号表(SymbolTable)实现.具体来说,就是使用每个节点含有两个链接的二叉树来高效地实现符号表.一颗二叉查找树时一颗二叉树,其中每个节点都含有一个Comparable的键且每个节点的键都大于其左子树中的任意节点的键而小于右子树的任意节点的键. 一.查找 一般来说,在符号表中查找一个键只可能出现命中和未命中两种情况.一般通过递归算法在二叉树中查找,如果树时空的则查找未命中:如果被查找的键和根节点相等,查找命中,

小橙书阅读指南(十二)——无向图、深度优先搜索和路径查找算法

在计算机应用中,我们把一系列相连接的节点组成的数据结构,叫做图.今天我们将要介绍它的一种形式--无向图,以及针对这种结构的深度优先搜索和路径查找算法. 一.无向图数据结构 接口: /** * 图论接口 */ public interface Graph { /** * 顶点数 * * @return */ int vertexNum(); /** * 边数 * * @return */ int edgeNum(); /** * 向图中添加一条v-w的边 * * @param v * @param

第十一章 散列表

摘要: 本章介绍了散列表(hash table)的概念.散列函数的设计及散列冲突的处理.散列表类似与字典的目录,查找的元素都有一个key与之对应,在实践当中,散列技术的效率是很高的,合理的设计散函数和冲突处理方法,可以使得在散列表中查找一个元素的期望时间为O(1).散列表是普通数组概念的推广,在散列表中,不是直接把关键字用作数组下标,而是根据关键字通过散列函数计算出来的.书中介绍散列表非常注重推理和证明,看的时候迷迷糊糊的,再次证明了数学真的很重要.在STL中map容器的功能就是散列表的功能,但