【数据结构】哈希表以及HashMap(基本原理、手动实现)

目录

一、上集回顾

二、基本原理

三、闭散列方法

四、开散列方法(哈希桶)

五、HashMap的手动实现

六、进一步分析


一、上集回顾

        我们已经了解了Set和Map的基本性质,并且我们使用二叉搜索树实现了简单的TreeSet的功能,其实TreeMap的实现方法和TreeSet是类似的,我们不再介绍。接下来我们会引入一个新的数据结构哈希表(HashTable),并以此来实现HashMap类,同样,HashSet类似,就不介绍了。

二、基本原理

        什么是哈希表呢?哈希表是我们构造的一种通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一 一映射的关系的数据结构,那么在查找时通过该函数可以很快找到该元素。

        举个例子,我有好多件衣服,每天出门的时候需要挑一件来穿,那么我要怎样放衣服才能比较快的找到我要穿的衣服呢?我可以买几个衣柜,然后按照衣服的类别(T恤、外套等)将衣服放进不同的衣柜里。这样我在找衣服的时候就可以快速定位到衣服在哪个衣柜里,这样效率提高了。所以,关键在于建立一种元素(Key)到位置的关系

于是,这种建立映射的函数称为哈希函数,构造出来的结构称为哈希表(HashTable)。这个函数往往需要经过特殊的设计,保证每个衣柜中的衣服尽可能的均匀。但是我们发现,”衣柜“的数量总会比”衣服“少,这样两件不同的衣服就有可能放在同一个衣柜内。这种hash(k1)=h1、hash(k2)=h2并且h1==h2的现象我们称为哈希冲突(哈希碰撞)

        显然,我们知道哈希冲突这种现象会影响到我们查找效率,并且现实中这种冲突几乎不可避免,于是我们应当做如下思考:

        1、尽可能的把冲突率降低

        2、真的冲突了,应该怎么办?

对于问题1:

1、我们应当让hash函数的结果尽可能的均匀

2、我们可以通过适当的扩容来降低冲突率

对于问题2:

解决办法这里介绍两种比较今典的方法:

1、闭散列方法

2、开散列方法(哈希桶)

三、闭散列方法

        我们首先明确hash函数,我们采用比较经典的除留余数法。即hash(Key)=key % P。此处我们选择p为存储元素底层空间总的大小。

        存在数据集合{1,7,6,5,9,4,15}。现在将该集合插入到储存空间中

1%10 等于1,将1放在下标为1处

7%10 等于7,将1放在下标为7处

.......

15%10等于5,下标5处已经存在元素5,于是我们往后移,发现6和7也有元素,继续后移,下标为8处为空,于是将15填入。

        查找的时候按照哈希hash函数算出所在下标,然后查看是否相等,如果相等则我们查找到了该元素,否则我们往后查找,如果在查找到该元素之前遇到空格,说明我们查找失败,并没有找到该Key。

        删除的时候,我们首先要进行查找,如果查找到该元素?我们能否直接删除该元素呢?很明显不能直接删除。例如上面的例子我们要删除6元素,如果直接进行删除,当我们要找到15的时候,就会发现,下标为6的地方为空,直接返回查找失败,但是15 还在集合中。所以直接删除可能会影响到集合。我们只能给每个下标处加一个状态,空为一个状态,已经删除为一个状态,存在元素为一个状态。(同理,查找的时候也需要根据这种方法进行变动)

        上面这种遇到冲突情况就往后找一个的方法称为线性探测法。同样还有二次方探测、随机探测等方法。以上的方法其实并不常用。

四、开散列方法(哈希桶)

        同样我们的hash函数还是选择hash(Key)=key % P。我们的思路还可以再换一换,为什么有冲突就要换地方呢?我们直接就在原地想办法不可以吗?于是就有开散列方法(哈希桶)。

        我们使用一个链表来存放产生冲突的元素,此时已经不存在什么冲突换址的问题,无论有多少个冲突都只是在当前位置给单链表增加结点的问题。该方法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当然这也就带来了查找时需要遍历单链表的性能损耗。

五、HashMap的手动实现

思路:get方法,获取下标之后,根据下标取得头结点,然后遍历链表,进行查找。put方法同理,但是我们需要考虑扩容,扩容需要size/array.length大于某个阈值。remove方法需要考虑特殊形况,也就是头结点为空的情况。主要方法已经基本完成:

package set_map.map.hashMap;

import java.util.*;

public class MyHashMap {
    private static final double THRESHOLD=0.75;
    private Node[] array;//链表头结点插在此处
    private int size;

    public MyHashMap() {
        this.array = new Node[7];
        this.size = 0;
    }

    public Integer get(String key){         //查找

        int n=key.hashCode();
        int index=n%array.length;

        Node head = array[index];

        //遍历整个链表,查找元素
        for(Node cur=head;cur!=null;cur=cur.next){
            if (key.equals(cur.Key)){
                return cur.value;
            }
        }
        return null;        //遍历结束没有查询到Key说明没有找到
    }
    public Integer put(String key ,int value){
        int n=key.hashCode();
        int index=n%array.length;

        Node head=array[index];
        for(Node cur=head;cur!=null;cur=cur.next){
            if (key.equals(cur.Key)){
                //此时找到了对应的元素,我们进行更新的操作
                int oldValue = cur.value;
                cur.value=value;
                return oldValue;
            }
        }
        Node node=new Node(key,value);
        //此处使用头插尾插都行,为了代码简单,我们使用头插法
        node.next=array[index];
        array[index]=node;
        size++;
        // TODO: 2022/3/26 扩容
        if((1.0*size/array.length)>THRESHOLD){
            ensureCapacity();
        }
        return null;            //插入成功返回null
    }

    private void ensureCapacity() {
        Node[] newArray=new Node[array.length*2];
        for (int i = 0; i < array.length; i++) {        //遍历数组中的每个元素
            Node next;//记录下一个节点
            for (Node cur=array[i];cur!=null ; cur=next) {//遍历链表的每个节点
                int n=cur.Key.hashCode();
                int index=n%newArray.length;

                next=cur.next;          //记录下一个节点
                //头插操作
                cur.next=newArray[index];
                newArray[index]=cur;
            }
        }
        array=newArray;
    }

    public Integer remove(String key){
        int n=key.hashCode();
        int index=n%array.length;
        Node cur=array[index];
        Node prev=null;
        if(array[index]!=null&&array[index].Key.equals(key)){
            Node head=array[index];
            array[index]=cur.next;
            return head.value;
        }
        while(cur!=null){
            if(cur.Key.equals(key)){
                prev.next=cur.next;
                return cur.value;
            }
            prev=cur;
            cur=cur.next;
        }
        return null;
    }
    public boolean remove(String key ,int value){
        int n=key.hashCode();
        int index=n%array.length;
        Node cur=array[index];
        Node prev=null;
        if(array[index]!=null&&array[index].Key.equals(key)&&array[index].value==value){
            array[index]=array[index].next;
            return true;
        }
        while(cur!=null){
            if(cur.Key.equals(key)&&cur.value==value){
                prev.next=cur.next;
                return true;
            }
            prev=cur;
            cur=cur.next;
        }
        return false;
    }
    public void clear(){
        Arrays.fill(array, null);
    }
    public int size(){
        return size;
    }
    public boolean isEmpty(){
        return size==0;
    }
    public Set <String> keySet(){
        Set<String> set=new HashSet<>();
        Set<Map.Entry<String,Integer>> nodes = this.entrySet();
        for (Map.Entry<String, Integer> node : nodes) {
            set.add(node.getKey());
        }
    return set;
    }
    public Collection<Integer> values(){
        List<Integer> list=new ArrayList<>();
        Set<Map.Entry<String,Integer>> nodes = this.entrySet();
        for (Map.Entry<String, Integer> node : nodes) {
            list.add(node.getValue());
        }
        return list;
    }
    public Set<Map.Entry<String,Integer>> entrySet(){
        Set<Map.Entry<String,Integer>> set=new HashSet<>();
        for (Node node : array) {
            Node cur;
            for (cur = node; cur != null; cur = cur.next) {
                set.add(cur);
            }
        }
        return set;
    }

    public static void main(String[] args) {
        MyHashMap map=new MyHashMap();
        System.out.println(map.put("aa", 1));
        System.out.println(map.put("ab", 2));
        System.out.println(map.put("ac", 3));
        System.out.println(map.put("ad", 4));
        System.out.println(map.put("ae", 3));
        System.out.println(map.put("af", 2));
        System.out.println(map.put("ag", 1));
        System.out.println("==============================");
        System.out.println(map.put("aa", 100));
        System.out.println("==============================");
        System.out.println(map.get("am"));
        System.out.println(map.get("aa"));
        System.out.println("==============================");
        System.out.println(map.remove("aa", 1));
        System.out.println(map.remove("aa"));
        System.out.println(map.remove("aa"));
        System.out.println(map.remove("af", 2));
        System.out.println("==============================");
        System.out.println(map.keySet());
        System.out.println(map.values());
        Set<Map.Entry<String, Integer>> set = map.entrySet();
        for (Map.Entry<String, Integer> entry : set) {
            System.out.println(entry.getKey()+"=>"+entry.getValue());
        }
    }
}

六、进一步分析

        可以看看知乎用户的解答:https://zhuanlan.zhihu.com/p/458305988

         以上图片是从Java的HashMap源码中的截图,该图的意思是说链表有第一个节点的概率大约为0.60,第二个节点的概率大约是0.3.....。也就是说这个概率分布符合泊松分布,一般来说,某个链表的长度超过8了,说明该泊松分布的假设失效了。这种情况往往是受到了恶意攻击。Java采取的做法就是使用红黑树来替换链表(退化成O(log n)而不是O(n))。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值