• 查找系列合集-散列表


    一、散列表

    【问题】之前我们的用红黑树实现了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
    •   因此一般要使α小一些,通常使它一直小于二分之一  
  • 相关阅读:
    【转载】Android IntentService使用全面介绍及源码解析
    【转载】作为Android开发者,你真的熟悉Activity吗?
    【转载】从使用到原理学习Java线程池
    Linux 基础知识+常见命令
    Python中的字典合并
    Python的__hash__函数和__eq__函数
    C#中实现Hash应用
    深入理解python元类
    Docker搭建Gitlab服务器
    python的加密方式
  • 原文地址:https://www.cnblogs.com/czsharecode/p/10605724.html
Copyright © 2020-2023  润新知