尾插法实现哈希表

目录

1、尾插法实现哈希表

2、概念

1、哈希表的存储和取出

2、哈希冲突

3、如何避免哈希冲突

1、设计合理的哈希函数

2、降低负载因子。

4、 解决冲突

1、闭散列

2、开散列

5、hashcode()和equals()方法

 3、实现

1、扩容

2、尾插法

1、尾插法实现哈希表

JDK1.8之后源码中的HashMap都变成通过尾插法添加元素的,我们如何使用尾插法实现一个哈希表呢?

2、概念

要实现哈希表我们就要知道哈希表的几个个概念

1、哈希表的存储和取出

哈希表是通过哈希函数计算key值得出的哈希值,存入相应的地址中。

2、哈希冲突

当我们通过哈希函数去计算哈希值时往往会出现,不同的key出现相同的哈希值的情况。例如,哈希函数为hash=key%capacity(哈希表的长度)。那么当我们的哈希表长度为10,key为2,12,22时得到的hash都是2。这就是哈希冲突,也叫哈希碰撞。

3、如何避免哈希冲突

1、设计合理的哈希函数

哈希函数需要满足使所有的key得到的哈希值尽可能地线性分布。当然,哈希函数还需要尽可能的简单。

2、降低负载因子。

负载因子 Load Factor – 负载因子是决定何时增加Map容量的度量。 默认负载系数为容量的 75%。 临界点 threshold – 临界点是当前容量和负载因子的乘积。

也就是说当我们的负载因子达到0.75时我们就应该去扩大哈希表的长度。因为当我们的负载因子增大到一定程度时冲突率就会指数状上升。

查看源图像

4、 解决冲突

哈希冲突是客观存在的,是不可避免的。我们需要想办法解决哈希冲突。一般解决哈希冲突的方法有两种:

1、闭散列

也被叫做开放地址法,也就是在发生哈希冲突时把该元素放在其他为空的位置。选取这个空位置我们一般有两种方法:

1、线性探测法

也即是放在该地址(冲突发生的地址)的下一位空位置。

例如:将2 12 22 32 存入一张使用线性探测法的闭散列。结果就是这样

 这样很容易导致大量元素堆积在一个位置。为了优化我们还有下面的方式

2、二次探测法

找下一个空位置的方法为:新的偏移量 = (冲突偏移量 +i^2 )% m, 或者 新的偏移量 = (冲突偏移量 -i^2 )% m。其中:i = 1,2,3…,

2、开散列

又叫做哈希桶,通过把哈希表上的每一个节点都制成一个链表,实现在发生哈希冲突时把哈希地址相同的元素添加到链表上。

我们的集合类HashMap的底层就是实现的哈希桶。

5、hashcode()和equals()方法

我们在实际使用哈希表的时候是通过泛型来规定key和val的类型的,那么我们就不能只依靠这么一个,简单的哈希函数(如下),来计算我们的哈希地址,因为如果我们的key是String或者是其他自定义类型的时候我们是不能通过key%capacity来计算出我们的哈希地址。

hash=key%capacity(哈希表的长度)

这时我们就要借助hashcode()方法计算。也就是hash=key.hashcode()%capacity。所以我们在自定义类型中要重写hashcode()方法。当我们使用int来实现哈希表时添加元素的方法一般这样写

//不用泛型的写法演示
public void put(int key,int val){
        //1、找到key的位置
        int index=key%this.elem.length;
        //2、判断index位置是否已经有key。
        Node cur=this.elem[index];
        while(cur!=null){
            if (cur.Key==key){
                cur.val=val;
                return;
            }
            cur=cur.next;
        }
        //3、尾插法
        Node node=new Node(key, val);
        if (this.elem[index]==null){
            this.elem[index]=node;
        }else{
            cur=this.elem[index];
            while (cur.next!=null){
               cur=cur.next;
            }
            cur.next=node;
        }
        this.useSize++;
        //4、插入元素成功之后,检查当前散列表的负载因子
        //如果负载因子>=0.75时我们要进行扩容
        if(loadFactor()>=DEFAULT_LOAD_FACTOR){
            resize();
        }
    }

我们在添加元素时要判断是否已经有key元素在哈希表中,如果有我们需要将新的val值去覆盖掉原来的val,但是在使用自定义类型时我们无法在使用==来判断key是否重复。这时我们就应该去重写equals()方法来进行判断。

所以,当我们在使用HashMap存放自定义类型时,我们在自定义类型中就要重写hashcode()和equals()方法。当然当我们使用包装类时,包装类中已经为我们重写好了这两个方法。

 3、实现

全部代码如下:

package HashBuck;

import java.util.Arrays;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 东莞呵呵
 * Date:2022-07-03
 * Time:17:21
 */
public class HashBuck <K,V>{
    class Node<K,V>{
        public K key;
        public V val;
        public Node<K,V> next;

        public Node(K key, V val) {
            this.key = key;
            this.val = val;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "key=" + key +
                    ", val=" + val +
                    '}';
        }
    }
    public Node<K,V>[] elem;
    public int useSize;
    public static final double DEFAULT_LOAD_FACTOR=0.75;

    public HashBuck() {
        this.elem = new Node[10];
        this.useSize = 0;
    }
    public void put(K key,V val){
        //1、找到key的位置
        int index=key.hashCode()%this.elem.length;
        //2、判断index位置是否已经有元素。
        Node<K, V> cur=this.elem[index];
        while(cur!=null){
            if (key.equals(cur.key)){
                cur.val=val;
                return;
            }
            cur=cur.next;
        }
        //3、尾插法
        Node<K,V> node=new Node<K,V>(key, val);
        if (this.elem[index]==null){
            this.elem[index]=node;
        }else{
            cur=this.elem[index];
            while (cur.next!=null){
                cur=cur.next;
            }
            cur.next=node;
        }
        this.useSize++;
        //4、插入元素成功之后,检查当前散列表的负载因子
        if(loadFactor()>=DEFAULT_LOAD_FACTOR){
            resize();
        }
    }

    //扩容之后每个元素都要重新哈希
    private void resize(){
        Node<K,V>[] newElem=new Node[this.elem.length*2];
        for (int i = 0; i < this.elem.length; i++) {
            Node<K,V> cur=this.elem[i];
            while(cur!=null){
                int index= cur.key.hashCode()%this.elem.length;
                Node<K,V> newCur=newElem[index];
                if(newCur==null){
                    newElem[index]=cur;
                    cur=cur.next;
                    this.elem[i]=cur;
                    newElem[index].next=null;
                }else {
                    while(newCur.next!=null){
                        newCur=newCur.next;
                    }
                    newCur.next=cur;
                    cur=cur.next;
                    this.elem[i]=cur;
                    newCur.next.next=null;
                }

            }
        }
        this.elem=newElem;
    }

    private double loadFactor(){
        return 1.0*this.useSize/this.elem.length;
    }
    public V get(K key){
        int hash = key.hashCode();
        int index = hash % elem.length;
        Node<K,V> cur = elem[index];
        while (cur != null) {
            if(cur.key.equals(key)) {
                //更新val值
                return cur.val;
            }
            cur = cur.next;
        }
        return null;
    }

    @Override
    public String toString() {
        return "HashBuck{" +
                "elem=" + Arrays.toString(elem) +
                ", useSize=" + useSize +
                '}';
    }
}

我们有几点需要注意:

1、扩容

当我们的添加的元素过多,也就是负载因子达到0.75时我们就需要对哈希表进行扩容。扩容时我们的数组长度发生了改变,

int index=key.hashCode()%this.elem.length;

也就是每个元素的index都有可能发生了改变,我们需要对整张表进行重新哈希。

 //扩容之后每个元素都要重新哈希
    private void resize(){
        Node<K,V>[] newElem=new Node[this.elem.length*2];
        for (int i = 0; i < this.elem.length; i++) {
            Node<K,V> cur=this.elem[i];
            while(cur!=null){
                int index= cur.key.hashCode()%this.elem.length;
                Node<K,V> newCur=newElem[index];
                if(newCur==null){
                    newElem[index]=cur;
                    cur=cur.next;
                    this.elem[i]=cur;
                    newElem[index].next=null;
                }else {
                    while(newCur.next!=null){
                        newCur=newCur.next;
                    }
                    newCur.next=cur;
                    cur=cur.next;
                    this.elem[i]=cur;
                    newCur.next.next=null;
                }

            }
        }
        this.elem=newElem;
    }

2、尾插法

这个相信大家也没有什么问题,在数据结构阶段一般链表的尾插法是最初讲的内容。需要注意的是我们要先判断当前链表是否为空,如果为空就直接添加新节点,如果不为空则遍历链表到最后,再添加新节点。

//3、尾插法
        Node<K,V> node=new Node<K,V>(key, val);
        if (this.elem[index]==null){
            this.elem[index]=node;
        }else{
            cur=this.elem[index];
            while (cur.next!=null){
                cur=cur.next;
            }
            cur.next=node;
        }
        this.useSize++;

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
哈希表(HashMap)是一种常用的数据结构,用于存储键值对,它通过哈希函数将键转换为数组索引,提供常数时间的插入、删除和查找操作。在实现哈希表时,有多种插入策略,其中头插法(Head Insertion)和尾插法(Tail Insertion)是两种常见的方法。 **头插法(Head Insertion):** - 在哈希表的头部插入元素:这是最常见的插入策略,每次新元素插入时,都会被添加到当前哈希桶的第一个位置。 - 插入过程:对于给定的键,计算哈希值确定插入位置,然后直接将元素插入到该位置的链表头部,链表头部通常是第一个插入元素的位置。 - 优点:搜索性能较好,因为新插入的元素总是链表的第一个节点,所以查找最近插入的元素较快。 - 缺点:可能会导致插入顺序依赖于哈希函数,如果哈希函数不好,可能会导致链表长度不均衡,影响整体性能。 **尾插法(Tail Insertion):** - 在哈希表的尾部插入元素:与头插法相反,尾插法会将新元素添加到链表的末尾。 - 插入过程:同样计算哈希值确定插入位置,然后将元素添加到链表的最后一个节点之后。 - 优点:链表长度均衡,不会因为新元素总是插入头部而导致链表过长。 - 缺点:查找最近插入的元素可能需要遍历整个链表,性能不如头插法理想,尤其是当元素按顺序插入时。 **相关问题--:** 1. 除了头插法和尾插法,还有哪些哈希表的插入策略? 2. 在什么情况下会选择使用尾插法而不是头插法? 3. 哈希表的负载因子是什么,它与插入策略有何关系?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

东莞呵呵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值