散列表原理及实现

散列表原理及实现

散列表原理


散列表:使用算术操作将键转化为数组的索引来访问数组中的键值对, 使用散列表,可以实现常数级别的查找和插入.

使用散列的查找算法主要要解决的两个问题:


  1. 散列函数的设计(即如何用散列函数将被查找的键转化为数组的一个索引).
  2. 处理碰撞冲突的过程(即处理两个或多个键的散列值相同的情况).
    PS:处理碰撞冲突的方法主要有拉链法线性探测法.

散列函数的设计


实现散列函数的指导思想 :

设计的散列函数能够均匀并独立地将所有的键散布于0 ~ M-1之间.(其中M为存放键值的数组的大小)

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

  • 一致性 : 等价的键必然产生相等的散列值
  • 高效性 : 计算简便
  • 均匀性 : 均匀地散列所有的键

散列方法举例—hashCode()方法结合除留余数法

将默认的hashCode()方法与除留余数法结合起来产生一个0~M-1的整数,一般会将数组的大小M取为素数以充分利用原散列值的所有位

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

处理碰撞冲突

碰撞处理就是处理两个或多个键的散列值相同的情况

拉链法


将大小为M的数组中的每个元素指向一条链表,链表的每个节点都存储了散列值为该元素的索引的键值对.

使用拉链法查找键值的过程 :

  1. 根据散列值找到对应链表
  2. 沿着对应链表查找相对应的键

拉链法实现
1. 拉链法基础方法
public class SeparateChainingHashST<Key extends Comparator<? super Key>,Value> {
    private int N;//键值对总数
    private int M;//散列表使用的数组大小
    private SequentialSearchST<Key,Value>[] st;//存放链表对象的数组,实现可参照<算法第四版SequentialSearchST.class>

    public SeparateChainingHashST(){
        this(997);
    }
    public SeparateChainingHashST(int M){
        /** 创建M条链表 */
        this.M=M;
        st=(SequentialSearchST<Key,Value>[])new SequentialSearchST[M];
        for(int i=0;i<M;i++){
            st[i]=new SequentialSearchST();
        }
    }
 }
2. 拉链法中的hash函数实现
    /**
     * hash函数
     * @param key 要插入的键
     * @return hash值
     */
    private int hash(Key key){
        return (key.hashCode()&0x7fffffff % M);
    }
3. 拉链法的put()和get()方法
    public void put(Key key,Value value){
        st[hash(key)].put(key,value);
    }

    public Value get(Key key){
        return (Value)st[hash(key)].get(key);
    }

线性探测法


开放地址散列表 : 用大小为M的数组保存N个键值对,其中M>N,依靠数组中的空位来解决碰撞冲突(线性探测法是最简单的开放地址散列表)

线性探测法原理 :
当碰撞发生时(当一个键的散列值已经被另外一个不同的键占用),直接检查散列表的下一个位置(索引值+1)直到遇到相同的键或者遇到了空缺的位置
遇到相同的键则替换其值,遇到空缺位置则把键值写入即可.

线性探测法实现
1. 线性探测法基础方法
public class LinearProbingHashST <Key,Value>{
    private int N;//存放的键值对的总数
    private int M;//线性检测表的大小
    private Key[] keys;//键
    private Value[] values;//值
    public LinearProbingHashST(){
        keys=(Key[]) new Object[M];
        values=(Value[])new Object[M];
    }
    private LinearProbingHashST(int size){
        this.M=size;
        keys=(Key[]) new Object[M];
        values=(Value[])new Object[M];
    }
    /**
     * resize()方法就是新建一个线性探测表,然后将原表的数据插入
     * @param size 线性探测表的大小
     */
    private void resize(int size){
        LinearProbingHashST newTable=new LinearProbingHashST<Key,Value>(size);
        for(int i=0;i<M;i++){
            if(keys[i]!=null){
                newTable.put(keys[i],values[i]);
            }
        }
        keys=(Key[])newTable.keys;
        values=(Value[])newTable.values;
        M=newTable.M;
    }
}
2. 线性检测法中的hash函数实现(与拉链法相同)
    private int hash(Key key){
        return (key.hashCode()&0x7fffffff)%M;
    }
3. 线性检测法中的put()和get()方法实现
    public void put(Key key,Value value){
        if(N>=M){
            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;
    }

4. 线性检测法中的delete()方法实现
    public void delete(Key key){
        /** 没有找到key */
        if(get(key)==null){
            return;
        }
        /** 找到key对应的数组索引位置,并将其键与值删除 */
        int i=hash(key);
        while(!keys[i].equals(key)){
            i=(i+1)%M;
        }
        keys[i]=null;
        values[i]=null;
        /** 将被删除键的右侧连续的所有键重新插入 */

        while(keys[i]!=null){
            Key keyToRedo=keys[i];
            Value valueToRedo=values[i];
            keys[i]=null;
            values[i]=null;
            N--;
            put(keyToRedo,valueToRedo);
            i=(i+1)%M;
        }
        N--;
        if(N>0&&N==M/8) resize(M/2);
    }

拉链法与线性检测法的性能差距

拉链法为每个键值对都分配了一小块内存,而线性探测法则为整张表使用了两个很大的数组,但是两者的性能差距还是因场景不同而有所变化,最好的方法还是去实践一下.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值