查找系列合集-散列表

一、散列表

【问题】之前我们的用红黑树实现了O(logN)的查找算法,那么理论上有没有O(1)的查找算法呢?

【分析】除非我们能够单凭键值key就能确定该元素在集合中的位置,直接将其取出

【解决方法】不妨采取映射的方法,将键值k1 k2 ......kn映射到 0 1 2 3 ......n-1,也就是数组下标的位置,那么如何完成这种映射呢?

【哈希】

  • 哈希方法也就是对给定的某一个值,采用一定的换算方法,将其映射为一个整数
  • Java内部为每一个内置数据类型都设计了hash函数,能够返回一个32bit整数

二、 散列函数

1. 散列函数即哈希函数

2. 可以自己为某种数据类型设计一种哈希函数代替内置的hash函数

  • 如一种对象包含3个域 x1 x2 x3 ,则可以设计其hash函数为 int hash = (x1 * r + x2) * r + x3
  • 为了不使数据溢出,可以把常数r设置得比较小,并在每一步运算时加上对M取模

3. 基于内置hash函数定义

  • 可以对任意key先返回其系统定义的hash值,然后再对M取模,在基础上将其范围限定到0 ~ M-1,防止数组越界

4. 碰撞

  • 当两个不同的key所得出的hash值相同,则认为发生了碰撞,因为他们都想要占据这个位置
  • 当发生碰撞时,后来者只能再寻求其他位置,解决方法有线性探测(即往后继续查找空位,见缝插针),二次探测再散列(加减一个值的平方后再看看有没有空位)
  • 具体解决碰撞的方案

三、基于拉链法的散列表

1. 核心思想是避开了解决hash碰撞的需要,对于每一个hash值(0~M-1),其都拉出了一个链表,把所有hash值都等于它的键存在了这个链表之中

2. 当要寻找一个key时,只要先求出hash值,然后在该hash值所对应的链表里顺序查找

【实现】

package search;

public class SeparateChainingHashST<Key , Value> {
    private int N; //键值对总数
    private int M;//散列表的大小
    private SequentialSearchST<Key, Value>[] st;
    
    public SeparateChainingHashST() {
        // TODO Auto-generated constructor stub
        this(997);
    }

    //初始化,为每一个hash值都创建一个链表 M个
    public SeparateChainingHashST(int M) {
        // TODO Auto-generated constructor stub
        this.M = M;
        st = (SequentialSearchST<Key, Value>[]) 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 Value get(Key key) {
        return (Value)st[hash(key)].get(key);
                
    }
    
    public void put(Key key, Value val) {
        st[hash(key)].put(key, val);
    }
    
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub

    }

}
基于拉链法的散列表
package search;


//链表,内部类
public class SequentialSearchST<Key, Value>{
    
    public SequentialSearchST() {
        super();
        // TODO Auto-generated constructor stub
    }
    private Node head;//链表头指针
    //结点类 内部类
    private class Node{
        Key key;
        Value val;
        Node next;//下一个结点指针
        public Node() {}
        public Node(Key key, Value val, Node next) {
            this.key = key;
            this.val = val;
            this.next = next;
        }
        
    }
    
    //顺序查找
    public Value get(Key key) {
        Node x = head;
        while(x != null) {
            if(x.key.equals(key)) {
                return x.val;
            }
            x = x.next;
        }
        return null;
    }
        //尾插法
        public void put(Key key, Value val) {
            //先考虑key存在的情况
            Node x = head;
            while(true) {
                if(x.key.equals(key)) {
                    x.val = val;
                    return;
                }
                //如果下一个结点不为空,则继续向下迭代。为空,则直接把新节点插入即可
                if(x.next != null)
                    x = x.next;
                else {
                    x.next = new Node(key, val, null);
                }
                
            }
        }
        
        //头插法
        public void putt(Key key, Value val) {
            Node x = head;
            //先搜索一遍防止键 存在
            while(x != null) {
                if(x.key.equals(key)) {
                    x.val = val;
                    return;
                }
                x = x.next;
            }
            //如果 不存在则将该新节点指向旧的头指针, 新头指针指向 它
            head = new Node(key, val, head);
        }
    
}
辅助对象链表

 

四、基于线性探测法的散列表

1.主要思想: 当前位置发生了碰撞便找下一个位置,以此类推,直到找到空位置

2. ADT

public class LinearProbingHashST<Key , Value> {
    
    private int N = 0;//当前使用量
    private int M = 16;//默认容量大小为M
    //并行数组
    private Key[] keys;
    private Value[] vals;
}

 

3. hash函数 

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

 

4. 插入操作

    //插入操作 冲突 循环后移再探测
    public void put(Key key, Value val) {
        //保证使用量不超过额定容量的一半
        if(N > M/2) {
            resize(2*M);
        }
        
        int index = hash(key);
        for(; keys[index] != null;  index = (index+1) % M) {
            if(keys[index].equals(key)) {
                vals[index] = val;
                return ;//如果该键已经存在则改值后 return
            }
        }
        keys[index] = key;
        vals[index] = val;
        N++; 
    }

 

5. 扩容操作

  • 当散列表大部分被填充之后,所造成的碰撞概率会大大增加,而且所带来的查找 删除操作成本也会很高
  • 为什么不直接拷贝数组而选择重新put呢?因为在新容量下的散列表这些key的hash值已经发生了变化。
  • 每当使用容量N到达总容量M的一半时,扩容一倍,这样即使对大规模数据的插入也不会很多次调用resize
private void resize(int cap) {
        LinearProbingHashST<Key , Value> t;
        t = new LinearProbingHashST<Key , Value>(cap);
        
        for(int i=0; i<M; i++) {
            if(keys[i] != null)
                t.put(keys[i], vals[i]);
        }
        this.keys = t.keys;
        this.vals = t.vals;
        this.M = t.M;
    }

 

6. 查找操作

  • 引入“长键”的概念,长键简单来说就是连续的键,或者叫键簇,散列表可以看成是多个长键组成的,每个长键之间间隔若干个空值(首尾长键算一个)
  • 由于采用的是线性探测法,可以有如下定理:hash值相等的键必然处于同一个长键簇之中
public Value get(Key key) {
        int index = hash(key);
        //首先算出hash值得到预期位置,如果正好这个位置的键等于key,那么命中,直接返回对应的val
        //如果发现key不相等,则说明发生冲突,由插入算法可知,要查找的键必然是和冲突键位于同一组长键之中
        //继续查找直到遇到空为止 遇到空说明该键不存在
        for(int i = index; keys[i] != null; i = (i+1) % M) {
            if(keys[i].equals(key)) {
                return vals[i];
            }
        }
        return null;
    }

 

7. 删除操作

  • 根据插入查询定理 目标键与hash值所在的键处于同一个长键之中,中间不能有缝隙
  • 如果因为删除一个键后导致中间断裂,会导致查询失效
  • 示例   G  H  KLOMN  U      其中 M、L hash值相等 K、N hash值不等  长键簇是KLOMN,删除O后会导致M值无法被查询到
  • 所以删除O(置空)后,如果O右边的键和O左边的键有hash值相等的情况时,右边的键必然查询不到
//只能将该【长键中】所删除的键置为空然后将其右边的所有键重新插入
    public void delete(Key key) {
        int index = hash(key);
        int pos = index;
        boolean flag = false;
        for(; keys[index] != null; index = (index + 1) % M) {
            if(keys[index].equals(key)) {
                //找到这个键了 把位置记录下来一会用,index继续增加得到边界
                flag = true;
                pos = index;
                keys[index] = null;
                vals[index] = null;
                N--;
            }
        }
         //没找到需要删除的键 就算了
        if(!flag)
            return ;
        //从pos+1的位置 到 index-1的位置所有键需要重新弄插入 注意位置序号需要取模
        pos = pos + 1;
        pos %= M;
        while(pos != index) {
            Key k = keys[pos];
            Value v = vals[pos];
            keys[pos] = null;
            vals[pos] = null;
            put(k , v);//重新插入
            pos = (pos + 1) % M;//后移pos位置
        }
    }

 

package search;

import java.util.Random;

public class LinearProbingHashST<Key , Value> {
    
    private int N = 0;//当前使用量
    private int M = 16;//默认容量大小为M
    //并行数组
    private Key[] keys;
    private Value[] vals;
    
    public LinearProbingHashST() {
        // TODO Auto-generated constructor stub
        alloc();
        
    }
    public LinearProbingHashST(int cap) {
        M = cap;
        alloc();
    }
    
    private void alloc() {
        keys = (Key[])new Object[M];
        vals = (Value[]) new Object[M];
    }
    
    private int hash(Key key) {
        return (key.hashCode() &  0x7fffffff) % M;
    }
    
    //插入 查询 定理:  目标键与hash值所在的键  位于同一条长键之中,中间不可能有空隙
    public void show() {
        for(int i=0; i<M; i++) {
            if(keys[i] != null)
                System.out.println(keys[i].toString() + ":" +vals[i].toString());
        }
    }
    //插入操作 冲突 循环后移再探测
    public void put(Key key, Value val) {
        //保证使用量不超过额定容量的一半
        //System.out.println("put :" + N + " " + M/2);
        if(N > M/2) {
            resize(2*M);
//            System.out.println("*************************resize*************************" + M);
//            show();
//            System.out.println("**************************************************");
        }
        
        int index = hash(key);
        for(; keys[index] != null;  index = (index+1) % M) {
            if(keys[index].equals(key)) {
                vals[index] = val;
                return ;//如果该键已经存在则改值后 return
            }
        }
        keys[index] = key;
        vals[index] = val;
        N++;
    }
    
    //这种赋值方式不行 思考一下为什么  从hash值的模出发
    private void resizes(int cap) {
        // TODO Auto-generated method stub
        Key[] ks =  (Key[]) new Object[2*cap];
        Value[] vs =  (Value[]) new Object[2*cap];
        
        for(int i=0; i<M; i++) {
            ks[i] = keys[i] ;
            vs[i] = vals[i] ;
        }
        keys =  ks;
        vals = vs;
        M = cap;
        
    }
    
    private void resize(int cap) {
        LinearProbingHashST<Key , Value> t;
        t = new LinearProbingHashST<Key , Value>(cap);
        
        for(int i=0; i<M; i++) {
            if(keys[i] != null)
                t.put(keys[i], vals[i]);
        }
        this.keys = t.keys;
        this.vals = t.vals;
        this.M = t.M;
    }
    
    public Value get(Key key) {
        int index = hash(key);
        //首先算出hash值得到预期位置,如果正好这个位置的键等于key,那么命中,直接返回对应的val
        //如果发现key不相等,则说明发生冲突,由插入算法可知,要查找的键必然是和冲突键位于同一组长 键之中
        //继续查找直到遇到空为止 遇到空说明该键不存在
        for(int i = index; keys[i] != null; i = (i+1) % M) {
            if(keys[i].equals(key)) {
                return vals[i];
            }
        }
        return null;
    }
    
    //根据插入查询定理 目标键与hash值所在的键处于同一个长键之中,中间不能有缝隙
    //如果因为删除一个键后导致中间断裂,会导致查询失效
    
    //G H KLOMN  U  ML hash值相等  KN hash值不等
    //只能将该【长键中】所删除的键置为空然后将其右边的所有键重新插入
    
    public void delete(Key key) {
        int index = hash(key);
        int pos = index;
        boolean flag = false;
        for(; keys[index] != null; index = (index + 1) % M) {
            if(keys[index].equals(key)) {
                //找到这个键了 把位置记录下来一会用,index继续增加得到边界
                flag = true;
                pos = index;
                keys[index] = null;
                vals[index] = null;
                N--;
            }
        }
         //没找到需要删除的键 就算了
        if(!flag)
            return ;
        //从pos+1的位置 到 index-1的位置所有键需要重新弄插入 注意位置序号需要取模
        pos = pos + 1;
        pos %= M;
        while(pos != index) {
            Key k = keys[pos];
            Value v = vals[pos];
            keys[pos] = null;
            vals[pos] = null;
            put(k , v);//重新插入
            pos = (pos + 1) % M;//后移pos位置
        }
    }
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        LinearProbingHashST<Integer , Integer> hashST;
        hashST = new LinearProbingHashST<Integer, Integer>();
        Random r = new Random();
        for(int i=0; i<1500000; i++) {
            Integer key = new Integer(r.nextInt(10000000));
            Integer value = new Integer(r.nextInt(10000000));
            //System.out.println("key = " + key + " value = " + value);
            hashST.put(key, value);
            //System.out.println("N = " + hashST.N + " M = " + hashST.M);
        }
        //System.out.println("put 操作完毕");
        hashST.put(88888, 88888);
        hashST.put(88888, 99999);
        
        //System.out.println(hashST.get(88888));
        System.out.println("删除前*****************");
        //hashST.show();
        
        //hashST.delete(88888);
        
        
        System.out.println("删除后*****************");
        
        //hashST.show();
        System.out.println(hashST.get(88888));
        System.out.println("OK");
    }

}
线性探测再散列

 

五、 关于散列表的均匀散列假设

  • 我们使用的散列函数能够均匀并且独立地将所有的键散布于0到M-1之间
  • 在一张大小为M且含有N = α M个键的基于线性探测的散列表中,基于上述假设,查找所需要的探测次数为
    • 查找命中时:½( 1+1/(1-α) )
    • 查找未命中时: ½( 1+1/(1-α)2
  •   因此一般要使α小一些,通常使它一直小于二分之一  
posted @ 2019-03-27 13:09  西风show码  阅读(203)  评论(0编辑  收藏  举报