数据结构之散列表

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

为什么需要散列表?

如果忽略内存,我们将键作为数组的索引,那么所有的查找查找操作只需要访问一次内存即可。当键很多时,这需要太大的内存。

 

散列的主要目的是将键均匀分布,因此散列后键就是无序的

 

散列算法分为两步:

第一步用散列函数将键转化为数组的索引。这可能导致多个键都得到相同的索引。

第二部就是处理碰撞冲突。

 

 

 

散列的查找算法:

散列函数:如果我们有一个能保存M个键值对的数组,就需要一个可以将任意键转化为该数组范围内的索引([0,M-1]范围的整数)的散列函数。

散列函数和键的类型有关,严格说,对于每种类型的键我们都需要一个与之对应的散列函数。

优秀的散列方法需要满足三个条件:

  1. 一致性(等价的键必然产生相等的散列值)
  2. 高效性(计算简便)
  3. 均匀性(均匀地散列所有的键)

 

解决碰撞的方法:

  1. 拉链法
  2. 线性探测法

 

拉链法

基于拉链法的散列表:它是将大小为M的数组中的每一个元素都指向一条链表,链表中的每个节点都存储了散列值为该元素的索引的键值对。

查找算法:先根据散列值找到对应的链表,然后沿链表顺序查找相应的值。

我们使用M条链表来保存N个键,那么链表的平均长度为N/M。

hash:这里使用默认的hashCode()方法

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

& 0x7fffffff (第一位是0,后面31个1)可以屏蔽符号位。 

为什么需要屏蔽符号位,而不是之间key.hashCode % m :因为Java取余的结果可能为负数。(例如-4 % 3 = -1)

% m 保证索引值在[0,M-1]之间。

 

为了防止链表过长,导致查找和插入成本过高,这里我们把链表的平均长度(N/M)限制在2~10之间。

我们使用重用之前实现的链表(SequentialSearchST)。无序链表

实现代码:


import java.util.LinkedList;
import java.util.Queue;

/**
 * @author yuan
 * @date 2019/2/28
 * @description 基于拉链法的散列表
 */
public class SeparateChainingHashST<Key, Value> {

    /**
     * 键值对总数
     */
    private int n;
    /**
     * 散列表大小
     */
    private int m;

    /**
     * 存放链表对象的数组
     */
    private SequentialSearchST<Key, Value>[] st;

    private static final int DEFAULT_CAPACITY = 4;


    public SeparateChainingHashST(){
        this(DEFAULT_CAPACITY);
    }

    public SeparateChainingHashST(int m) {
        this.m = m;
        st = new SequentialSearchST[m];
        for (int i = 0; i < m; i++) {
            st[i] = new SequentialSearchST<>();
        }
    }

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

    public int size(){
        return n;
    }

    public boolean isEmpty(){
        return size() == 0;
    }


    public Value get(Key key) {
        if (key == null) {
            throw new IllegalArgumentException("argument to get() is null");
        }
        return st[hash(key)].get(key);
    }

    public void put(Key key, Value value) {
        if (key == null) {
            throw new IllegalArgumentException("first argument to put() is null");
        }
        if (value == null) {
            delete(key);
            return;
        }
        // 如果 n / m (链表的平均长度) >= 10
        if (n >= 10 * m) {
            resize(2 * m);
        }

        int i = hash(key);
        if (!contains(key)) {
            ++n;
        }
        st[i].put(key, value);
    }

    private boolean contains(Key key) {
        if (key == null) {
            throw new IllegalArgumentException("argument to contains() is null");
        }
        return get(key) != null;
    }

    private void resize(int chains) {
        SeparateChainingHashST<Key, Value> temp = new SeparateChainingHashST<>(chains);
        for (int i = 0; i < m; i++) {
            for (Key key : st[i].keys()) {
                temp.put(key, st[i].get(key));
            }
        }
        this.m = temp.m;
        this.n = temp.n;
        this.st = temp.st;
    }

    private void delete(Key key) {
        if (key == null) {
            throw new IllegalArgumentException("argument to delete() is null");
        }
        int i = hash(key);
        if (contains(key)) {
            --n;
        }
        st[i].delete(key);

        // 如果 链表平均长度(n / m) <= 2
        if (m > DEFAULT_CAPACITY && n <= 2 * m) {
            resize(m / 2);
        }
    }

    public Iterable<Key> keys(){
        Queue<Key> queue = new LinkedList<>();
        for (int i = 0; i < m; i++) {
            for (Key key : st[i].keys()) {
                queue.offer(key);
            }
        }
        return queue;
    }

    public static void main(String[] args) {
        SeparateChainingHashST<String, Integer> st = new SeparateChainingHashST<>();

        st.put("ccc", 2);
        st.put("bbb", 3);
        st.put("aaa", 1);
        st.put("ddd", 1);


        for (String s : st.keys()) {
            System.out.println(s + " " + st.get(s));
        }
        System.out.print("keys = ");
        st.keys().forEach(s -> System.out.print(s + ","));
        System.out.println();

        System.out.println(st.contains("aaa")); // true
        System.out.println(st.contains("cda")); // false
    }


}

 

线性探测法

 

线性探测法就是用大小为M的数组保存N个键值对,其中M>N。依靠数组中的空位解决碰撞冲突。

基本思想:当键的散列值发生冲突时,直接检查散列表中的下一个位置(将索引值加1),检查其中的键和被查找的键是否相同,如果不同则继续查找(索引增大),直到找到该键或遇到一个空元素。

 

删除操作

注意,不能直接将该键设为null,因为这样会导致后面的键无法被查找。

我们需要将被删除的键的右侧的所有键重新插入散列表。

 

键簇(cù)

就是元素插入数组后形成的一条连续的条目。

 

调整数组大小

当数组使用率(N/M)小于1/2时,查找次数只在1.5到2.5之间。(具体参考算法第4版)

这里我们使散列表的使用率不超过1/2。

 

代码:


import java.util.LinkedList;
import java.util.Queue;

/**
 * @author yuan
 * @date 2019/2/28
 * @description 基于线性探测法的散列表
 */
public class LinearProbingHashST<Key , Value> {
    private static final int DEFAULT_CAPACITY = 4;

    /**
     * 符号表中键值对总数
     */
    private int n;
    /**
     * 线性探测表大小
     */
    private int m;
    /**
     * 键
     */
    private Key[] keys;
    /**
     * 值
     */
    private Value[] vals;


    public LinearProbingHashST(){
        this(DEFAULT_CAPACITY);
    }

    public LinearProbingHashST(int capacity) {
        m = capacity;
        n = 0;
        keys = (Key[]) new Object[m];
        vals = (Value[]) new Object[m];
    }

    public int size(){
        return n;
    }


    public boolean isEmpty(){
        return size() == 0;
    }

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

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

    /**
     * 插入
     * @param key
     * @param val
     */
    public void put(Key key, Value val) {
        if (key == null) {
            throw new IllegalArgumentException("first argument to put() is null");
        }

        if (val == null) {
            // 值为null,则删除对应的键
            delete(key);
            return;
        }
        // 如果使用率大于1/2,扩大数组
        if (n >= m / 2) {
            resize(2 * m);
        }
        int i;
        for (i = hash(key); keys[i] != null; i = (i + 1) % m) {
            if (keys[i].equals(key)) {
                // 如果键存在,更新
                vals[i] = val;
                return;
            }
        }
        // 键不存在
        keys[i] = key;
        vals[i] = val;
        ++n;
    }

    /**
     * 删除,需要将被删除键的右侧的所有键重新插入散列表
     * @param key
     */
    public void delete(Key key) {
        if (key == null) {
            throw new IllegalArgumentException("argument to delete() is null");
        }
        if (!contains(key)) {
            return;
        }
        int i = hash(key);
        while (!key.equals(keys[i])) {
            i = (i + 1) % m;
        }
        // 找到要删除的键
        keys[i] = null;
        vals[i] = null;
        i = (i + 1) % m;
        // 将被删除的键的右侧重新插入
        while (keys[i] != null) {
            Key keyToRehash = keys[i];
            Value valToRehash = vals[i];
            keys[i] = null;
            vals[i] = null;
            --n;
            put(keyToRehash, valToRehash);
            i = (i + 1) % m;
        }
        // 减去被删除的键
        --n;
        // 如果使用率为1/8,缩小数组
        if (n > 0 && n == m / 8) {
            resize(m / 2);
        }
    }

    /**
     * 获取
     * @param key
     * @return
     */
    public Value get(Key key) {
        if (key == null) {
            throw new IllegalArgumentException("argument to get() is null");
        }
        for (int i = hash(key); keys[i] != null; i = (i + 1) % m) {
            if (keys[i].equals(key)) {
                return vals[i];
            }
        }
        return null;
    }

    public boolean contains(Key key) {
        if (key == null) {
            throw new IllegalArgumentException("argument to contains() is null");
        }
        return get(key) != null;
    }

    public Iterable<Key> keys(){
        Queue<Key> queue = new LinkedList<>();
        for (int i = 0; i < m; i++) {
            if (keys[i] != null) {
                queue.offer(keys[i]);
            }
        }
        return queue;
    }

    public static void main(String[] args) {

        LinearProbingHashST<String, Integer> st = new LinearProbingHashST<>();
        st.put("ccc", 2);
        st.put("bbb", 3);
        st.put("aaa", 1);
        st.put("ddd", 1);


        for (String s : st.keys()) {
            System.out.println(s + " " + st.get(s));
        }
        System.out.print("keys = ");
        st.keys().forEach(s -> System.out.print(s + ","));
        System.out.println();

        System.out.println(st.contains("aaa")); // true
        System.out.println(st.contains("cda")); // false
    }

}

 

Java的TreeMap就是基于红黑树实现的。

Java的HashMap是基于拉链法的散列表实现的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章