散列表

一、拉链法

  • 使用原始的链表数据类型,根据散列值找到对应的链表,然后沿着链表顺序查找相应的键;或者为M个元素分表构建符号表来保存散列到这里的键,根据符号表找键
  • 在一张含有M条链表和N个键的散列表中,任意一条链表中的键的数量均在N/M的常数因子范围内的概率无线趋近于1
  • 在一条含有M条链表和N个键的散列表中,未命中查找和插入操作所需的比较次数为~N/M
package 散列表;

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

public class SequentialSearchST<Key, Value> {
    private int n;           // 键值对数量
    private Node first;      // 键值对链表
    
    private class Node {
        private Key key;
        private Value val;
        private Node next;

        public Node(Key key, Value val, Node next)  {
            this.key  = key;
            this.val  = val;
            this.next = next;
        }
    }
  
    public SequentialSearchST() {
    }`

  //键值对数量
    public int size() {
        return n;
    }

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

    //含有该键的值是否为空
    public boolean contains(Key key) {
        return get(key) != null;
    }

    /**
     获取键值
     */
    public Value get(Key key) {
        for (Node x = first; x != null; x = x.next) {
            if (key.equals(x.key))
                return x.val;
        }
        return null;
    }

    /**插入或更新键值对
     */
    public void put(Key key, Value val) {
        if (val == null) {
            delete(key);
            return;
        }
//更新
        for (Node x = first; x != null; x = x.next) {
            if (key.equals(x.key)) {
                x.val = val;
                return;
            }
        }
        //头部插入
        first = new Node(key, val, first);
        n++;
    }

  //删除该键
    public void delete(Key key) {
        first = delete(first, key);
    }

 
    private Node delete(Node x, Key key) {
        if (x == null) return null;
        if (key.equals(x.key)) {
            n--;
            return x.next;
        }
        x.next = delete(x.next, key);
        return x;
    }


    
    public Iterable<Key> keys()  {
        Queue<Key> queue = new LinkedList<>();
        for (Node x = first; x != null; x = x.next)
            queue.offer(x.key);
        return queue;
    }

    
}
package 散列表;

public class SeparateChainingHashST <Key,Value>{
    private int N;//键值对总数
    private int M;//散列表大小
    private  SequentialSearchST<Key,Value>[] st;//存放链表对象的数组
    public SeparateChainingHashST(){
        this(997);
    }
    public SeparateChainingHashST(int M){
        this.M=M;
        st=(SequentialSearchST<Key,Value>[]) new SequentialSearchST[M];
        for (int i = 0; i <st.length ; i++) {
            st[i]=new SequentialSearchST<>();
        }
    }
    private int hash(Key key){
        return (key.hashCode()&0x7fffffff)%M;
    }
    public Value get(Key key){
        return (Value) st[hash(key)].get(key);
    }
    public void put(Key key,Value value){
        st[hash(key)].put(key,value);
    }
}

二、线性探测法

  • 使用一个大小为M的数组保存N个键值对,M>N,当碰撞发生时,直接检查散列表的下一个位置,将索引值加1
    • 命中,该位置的键和被查找的键相同
    • 未命中,键为空
    • 继续查找,该位置的键和被查找的键不同
  • 采用并行数组,一条保存键,一条保存值
  • 删除:需要将被删除键的右侧的所有键重新插入散列表
package 散列表;

public class LinearProbingHashST<Key, Value> {
    private int N;
    private int M = 16;
    private Key[] keys;
    private Value[] values;

    public LinearProbingHashST() {
        keys = (Key[]) new Object[M];
        values = (Value[]) new Object[M];
    }

  

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

    public void resize(int cap) {
        LinearProbingHashST<Key, Value> t;
        t = new LinearProbingHashST<>(cap);
        for (int i = 0; i < M; i++) {
            if (keys[i] != null) {
                t.put(keys[i], values[i]);
            }
        }
        keys = t.keys;
        values = t.values;
        M = t.M;
    }

    private void put(Key key, Value value) {
        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)) {
                values[i] = value;
                return;
            }
        }
        keys[i] = key;
        values[i] = value;
        N++;
    }

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

    
    /**
     * Initializes an empty symbol table with the specified initial capacity.
     *
     * @param capacity the initial capacity
     */
    public LinearProbingHashST(int capacity) {
        M = capacity;
        N = 0;
        keys = (Key[])   new Object[M];
        values = (Value[]) new Object[M];
    }


    public int size() {
        return N;
    }


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


    public boolean contains(Key key) {
        if (key == null) throw new IllegalArgumentException("argument to contains() is null");
        return get(key) != null;
    }
    
    
//删除
    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;
        values[i] = null;
        //将后面的都重新复插入
        i = (i + 1) % M;
        while (keys[i] != null) {
            Key   keyToRehash = keys[i];
            Value valToRehash = values[i];
            keys[i] = null;
            values[i] = null;
            N--;
            put(keyToRehash, valToRehash);
            i = (i + 1) % M;
        }

        N--;
        if (N > 0 && N <= M/8) resize(M/2);

    }
}

三、总结

  • 散列表相比二叉查找树而言,查找时间最优,只要键的数据类型是标准的或者简单到我们可以写出满足均匀性假设的高效散列函数即可
  • 二叉查找树抽象结构更简单,红黑树可以保证最坏情况下的性能且能支持更多操作,如排名,选择,排序和范围查找等
  • 在查找上一般优先选择散列表,在其他因素更重要时选择红黑树
  • 当键都是长字符串时,优先选择二叉树
posted @ 2022-01-16 16:09  forever_fate  阅读(53)  评论(0)    收藏  举报