哈希表的设计

文章介绍了哈希表的概念,通过哈希函数实现快速查找,对比了顺序查找和平衡树的效率。哈希冲突及其解决方法,如闭散列(线性探测)和开散列(链表解决冲突)被详细阐述。此外,还讨论了几种常见的哈希函数设计方法,并提供了一个简单的哈希表实现示例,包括添加元素、扩容和解决冲突的策略。
摘要由CSDN通过智能技术生成

概念

顺序结构以及平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素时,必须要经过关键 码的多次比较 顺序查找时间复杂度为 O(N) ,平衡树中为树的高度,即 O( ) ,搜索的效率取决于搜索过程中 元素的比较次数。
理想的搜索方法:可以 不经过任何比较,一次直接从表中得到要搜索的元素 如果构造一种存储结构,通过某种函 (hashFunc) 使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快 找到该元素
向该结构中插入元素:
  根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放 。
查询元素:
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若 关键码相等,则搜索成功。
该方式即为哈希 ( 散列 ) 方法, 哈希方法中使用的转换函数称为哈希 ( 散列 ) 函数,构造出来的结构称为哈希表 (Hash Table)( 或者称散列表 )

哈希冲突

什么是哈希冲突

对于两个数据元素的关键字 和 (i != j) ,有 != ,但有: Hash( ) == Hash( ) ,即: 不同关键字通过相同哈 希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

怎样解决哈希冲突

1、闭散列(线性探测):当冲突发生时,找到冲突旁边的空闲位置放入冲突元素

缺点:通过这种方式解决哈希冲突,当被冲突元素所占位置需要插入元素时,该位置已经不能被插入,那就需要继续查找空闲位置,这样的话,不仅插入的效率会降低,查询的效率也会降低,而且还会影响其他元素的插入与查询。

2、开散列:当冲突发生时将冲突位置变为一个链表。

缺点:当某一个为值发生的冲突过多,会导致链表长度过长,由此带来插入和查询效率降低。

哈希函数

常用的哈希函数

1. 直接定制法
取关键字的某个线性函数为散列地址: Hash Key = A*Key + B 优点:简单、均匀 缺点:需要事先知道关 键字的分布情况 使用场景:适合查找比较小且连续的情况 。
2. 除留余数法
设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址。
3. 平方取中法
假设关键字为 1234 ,对它平方就是 1522756 ,抽取中间的 3 227 作为哈希地址; 再比如关键字为 4321 ,对 它平方就是18671041 ,抽取中间的 3 671( 710) 作为哈希地址 平方取中法比较适合:不知道关键字的分 布,而位数又不是很大的情况。
4. 折叠法
折叠法是将关键字从左到右分割成位数相等的几部分 ( 最后一部分位数可以短些 ) ,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。 折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。
5. 随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key), 其中 random 为随机数函数。 通常应用于关键字长度不等时采用此法
6. 数学分析法
设有 n d 位数,每一位可能有 r 种不同的符号,这 r 种不同的符号在各位上出现的频率不一定相同,可能在某 些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

设计哈希表

在哈希表的设计中我采用开散列来解决哈希冲突,而哈希函数采用的是除留余数法。

哈希表模块设计

1、哈希函数

private int hash(int key) {
        return key % this.M;
    }

哈希函数是哈希表中最重要的一部分,一个好的哈希函数可以大大降低哈希冲突产生的概率,我采用的是哈希函数是对哈希桶的长度进行取余操作。

2、添加元素

public int put(int key,int value) {
        // 1.首先计算出当前新元素的下标
        int index = hash(key);
        // 2.在当前的子链表中判断key值是否存在,若存在,只需要更新value即可
        for (Node x = data[index];x != null;x = x.next) {
            if (x.key == key) {
                // 存在,只需要更新value
                int oldValue = x.value;
                x.value = value;
                return oldValue;
            }
        }
        // 3.此时key第一次出现,头插到当前的子链表中
        Node node = new Node(key,value);
        node.next = data[index];
        data[index] = node;
        size ++;
        // 4.判断当前哈希表的冲突情况,是否需要扩容
        if (size >= this.data.length * LOAD_FACTOR) {
            resize();
        }
        return -1;
    }

先计算出元素hash后的值,作为索引下标,在判断当前索引链表是否存在key值,若不存在直接插入(头插到链表中),若存在返回旧的value值,并更新value值。并判断是否需要对哈希表进行扩容。

3.扩容

private void resize() {
        this.M = data.length << 1;
        Node[] newData = new Node[data.length << 1];
        // 搬移原数组的所有节点
        for (int i = 0; i < data.length; i++) {
            for (Node x = data[i];x != null;) {
                Node next = x.next;
                // 当前x搬移到新数组的对应位置
                int newIndex = hash(x.key);
                // 头插到新数组的对应位置
                x.next = newData[newIndex];
                newData[newIndex] = x;
                // 继续搬移原数组的下一个节点
                x = next;
            }
        }
        // 更新data的指向
        data = newData;
    }

当哈希表需要扩容时,先将哈希桶扩容为原来的二倍,之后因为原来哈希表中的所以是根据哈希桶的长度取余所得,所以需要对之前的元素进行搬移。

哈希表的实现

public class MyHashMap {
    private class Node{
        int key;
        int value;
        Node next;

        public Node(int key,int value) {
            this.key = key;
            this.value=value;
        }
    }
    private int size;
    private int m;
    private Node[] data;
    private static final double LOACL_FACTOR = 0.75;

    public MyHashMap() {
        this(16);
    }

    public MyHashMap(int capacity){
        this.data=new Node[capacity];
        this.m=capacity;
    }

    public int put(int key,int value){
        int index=hash(key);
        for(Node x=data[index];x!=null;x=x.next){
            if(x.key==key){
                int oldval=x.value;
                x.value=value;
                return oldval;
            }
        }
        Node node=new Node(key,value);
        node.next=data[index];
        data[index]=node;
        size++;
        if(size>=data.length*LOACL_FACTOR){
            resize();
        }
        return -1;
    }

    private void resize(){
        this.m= data.length<<1;
        Node[] newData=new Node[data.length<<1];
        for(int i=0;i<data.length;i++){
            for(Node x=data[i];x!=null;){
                Node next=x.next;
                int newIndex=hash(x.key);
                x.next=newData[newIndex];
                newData[newIndex]=x;
                x=next;
            }
        }
        data=newData;
    }

    public boolean removeKey(int key){
        int index=hash(key);
        if(data[index]==null){
            return false;
        }
        if (data[index].key == key) {
            data[index] = data[index].next;
            size --;
            return true;
        }
        Node prev=data[index];
        while(prev.next!=null){
            if(prev.next.key==key){
               prev.next=prev.next.next;
               size--;
               return true;
            }
        }
        return false;
    }


    public boolean cntainsKey(int key){
        int index=hash(key);
        for(Node x=data[index];x!=null;x=x.next){
            if(x.key==key){
                return true;
            }
        }
        return false;
    }

    public boolean containsValue(int value){
        for(int i=0;i<data.length;i++){
            for(Node x=data[i];x!=null;x=x.next){
                if(x.value==value){
                    return  true;
                }
            }
        }
        return false;
    }




    private int hash(int k){
        return k%m;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值