zoukankan      html  css  js  c++  java
  • 3.4 散列表

    一.前言(Hash table)

    1.红黑树具有lgN的性能保障,能否做得更好?

    可以,但是需要一种不同的获取数据的方式,并且不能保证有序操作。即散列表

    2.几个需要思考的问题:

    (1)散列函数的计算

    (2)相等测试。可以使用equals来判断两个key是否相等

    (3)碰撞冲突。两个键散列以后具有相同的数组索引。(解决方法:拉链法和线性探测法)

    3.散列表是时间和空间权衡(space-time tradeoff)的经典例子。

    (1)如果没有空间限制,可以将每个key作为散列表的索引

    (2)如果没有时间限制,将所有的key散列到同一个地方,然后可以使用无序数组进行顺序查找

    (3)时间和空间均有限:hashing

    二.散列函数 hash function

    1.期望:(1)易于计算(2)能够均匀分布所有键(即每个表索引对于每个键都是相同的,等概)

    2.对于一些数据类型:Integer,Double,String,File,URL,Date,..已经内置有hash函数的实现,并且hascode()和equals()方法一致。

    例如:Integer简单的返回自己,Boolean返回2种不同的数字,String返回hash=s[i]+(31*hash);即s[0]*31L-1+...+s[L-2]*311+s[L-1]*310

    3.自定义散列函数

    package com.cx.serch;
    
    public class Transaction {
        private String who;
        private Date when;
        private double amount;
        
        public int hashCode() {
            int hash=17;
            hash=31*hash+who.hashCode();
            hash=31*hash+when.hashCode();
            hash=31*hash+((Double)amount).hashCode();
            return hash;
        }
    }
    View Code

    4.将hashCode的返回值转化为一个数组索引

    (1)HashCode.int类型是-231和231-1之间

          希望转化为0到M-1之间,使用数组的索引

    (2)一种可能的尝试:

        private int hash(Key key) {
            return Math.abs(key.hashCode())%M;
        }

    但是231会出现bug

    (3)使用位与,屏蔽符号位

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

    5.均匀散列假设:我们使用的散列函数能够均匀并独立地将所有的键散布在0到M-1之间

    三.基于拉链法的散列表 (separate chaining)

    1.使用数组和linked-list。

    (1)Hash:将key散列到0-M-1的数组

    (2)插入:放在第i条链路的前面

    (3)查询:只需要搜索第i条链路

    2.代码实现

    package com.cx.serch;
    
    public class SeparateChainingHashST<Key,Value>{
        //chain的数量
        private int M=97;
        //存放chain的数组
        private Node[] st=new Node[M];
        private static class Node{
            private Object key;
            private Object value;
            private Node next;
            private Node(Object key, Object value, Node next) {
                this.key = key;
                this.value = value;
                this.next = next;
            }
        }
        
        private int hash(Key key) {
            return (key.hashCode() & 0x7fffffff)%M;
        }
        //只用在一条链路上查找
        public Value get(Key key) {
            int i=hash(key);
            for(Node x=st[i];x!=null;x=x.next)
                if(key.equals(x.key)) return (Value)x.value;
            return null;
        }
        //在一条链路上查找,存在则替换,不存在则
        public void put(Key key,Value value) {
            int i=hash(key);
            for(Node x=st[i];x!=null;x=x.next) 
                if(key.equals(x.key)) { x.value=value; return;}
            //放在链路的最前面
            st[i]=new Node(key,value,st[i]);
        }
    }
    View Code

    3.说明:

    (1)任意链表的key的数量为N/M的概率趋近于1.

    (2)将一个查找N个元素的问题转化为查找N/M个元素的问题

    (3)一般选择:N/M=5,可以保证常数时间完成查找/插入。

    四.基于线性探测法的散列表 (linear probing)

    1.基本思想:利用大小为M的数组保存N个键值对,其中M>N(前提),使用数组中的空位解决碰撞冲突。称为开放地址散列表。

    2.散列:将key散列到0到M-1之间

    3.插入:如果可以的话,放在第i个位置,如果已有元素,尝试i+1,i+2..如果到达末尾,则折回数组的开头。

    4.查找:查找第i个位置,看是否匹配,如果不匹配,接着看i+1,i+2,,直到匹配或者到达下一个空位。

    5.算法实现:有技巧性

    package com.cx.serch;
    
    public class LinearProbingHashST<Key,Value>{
        //数组的大小
        private int M=30001;
        private Value[] values=(Value[]) new Object[M];    
        private Key[] keys=(Key[]) new Object[M];    
        private int hash(Key key) {
            return (key.hashCode() & 0x7fffffff)%M;
        }
    
        //放在第i个位置,如果已有元素,尝试i+1,i+2...,到达数组末尾,折回开头
        public void put(Key key,Value value) {
            int i;
            //(i+1)%M可以保证上述操作
            //keys[i]!=null表示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;
        }
        //在第i个位置查找,如果无元素返回null,如果有元素判断是否相等,如果不相等,i++,直到到达下一个空位或匹配
        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;
        }
    }
    View Code

    6.说明:

    (1)如果M过大,会有太多的空位置;如果M太小,查找时间会大大增加

    (2)一般选择M/N=2。此时,查找成功大约在3/2次,没查找到/插入大约在5/2次。保持数组在半满状态,需要动态的调整数组的大小。

    (3)线性探测法,类似于日常生活中,在一条街道上停车。

    (4)拉链法和线性探测法的前提都是能够均匀分布所有键(uniform hashing assumption)

    (5)上面(4)的这种前提不像快速排序那样可以由开发者避免(shuflling),如果恶意攻击者知道你的hash函数,全部插入hash值相同的元素,这将会使得性能非常的差,使用这两种方法只能期望均匀分布。

    而这种情况使用红黑树就比较好,因为它由lgN的性能保障。

    (6)对于(5)这种情况,可以使用一些加密算法,对key进行加密,使得攻击者无法轻易的找到hash值相同的key来恶意攻击。例如MD4,MD5,SHA-0,SHA-1。。。

    但是这对于符号表来说,开销过大。

    7.拉链法和线性探测法的对比

    (1)拉链法:

    ①容易实现删除

    ②对于不好的hash函数,簇不是很敏感

    ③performance degrades gracefully

    (2)线性探测法:

    ①空间浪费少

    ②有更好的缓存性能

    8.散列表和平衡查找树的对比

    hash table:

    (1)代码容易

    (2)对于无序key,不需要改变

    (3)对于简单的key更快

    (4)像String等已有较好hash的java实现更方便。

    balanced search tree:

    (1)更强的性能保障,不需要有任何假设的前提

    (2)支持有序的符号表操作

    (3)compareTo()的实现比equals()和hashCode()更容易

    在java系统都都有实现:

    (1)红黑树:java.util.TreeMap,java.util.TreeSet

    (2)散列表:java.util.HashMap,java.util.IdentityHashMap

  • 相关阅读:
    结构化编程:确定迭代循环控制for
    为什么就业难,因为信息化的威力
    结构化编程:单支条件选择控制
    C/C++ 标准输入输出重定向
    C语言中用scanf连续输入两个字符类型的问题
    void value not ignored as it ought to be
    不要做浮躁的嵌入式系统工程师
    INI配置文件的格式
    Linux 下串口编程入门
    时钟周期及秒(s) 毫秒(ms) 微秒(μs) 纳秒(ns) 皮秒(ps)之间转换
  • 原文地址:https://www.cnblogs.com/sunnyCx/p/8250106.html
Copyright © 2011-2022 走看看