哈希表

1.哈希表

数据结构中的压轴戏,面试中出场频率极高的内容,同时也是工作中出场频率极高的内容,更是现代分布式系统的基础。

给定若干个整数[0,99],再给定一个数字N,判定N是否在刚才的集合中出现:

  • 基于顺序表:可以用一个数组保存若干个整数,拿着N在数组中依次遍历,进行比较。时间复杂度为:O(N)
  • 基于链表:可以用一个链表保存若干个整数,拿着N在数组中依次遍历,进行比较。时间复杂度为O(N)
  • 基于二叉搜索树:可以用二叉搜索树来保存这些整数,按照二叉搜索树的方式进行查找比较。时间复杂度最坏O(N),比较理想的情况:O(logN)
    哈希表:可以按照O(1)时间复杂度,完成插入,查找,删除操作,数组访问下标是很高效的,哈希表本质依赖了数组下标的随机访问能力。

2.hash冲突

如果发现两个key不同的元素,计算得到的hash值相同,此时就称为“hash冲突”。

3.解决hash冲突的办法

主要有两种方式:(hash就是散列,这是意译,哈希是音译,哈希函数,哈希表也可以称为散列函数,散列表)

3.1闭散列

核心思路是:在冲突位置开始往后找到一个合适位置来存放这个冲突的值。

3.2开散列

数组的每个元素不再是存key,而是一个存key的顺序表或者链表(常见)

3.3hash冲突与hash表

一旦涉及到hash冲突,此时hash表的基本操作的时间复杂度就不是严格的O(1)了,随着冲突越严重,效率就会越低。正因为如此,在hash表长度的选择的时候,就有一种说法,一般都要选一个比较大的值(如果集合中有100个元素,就最好搞一个1000个元素的数组),如果数组元素个数选的比较大了,确实能降低冲突概率,但是浪费的空间也会变多。另外,如果把数组元素个数选成一个素数,那么冲突概率就会低一些。
hash冲突是理论上客观存在的,避免不了的~~但是可以通过开散列或闭散列的方式来处理冲突,虽然出现冲突,但是只是影响到效率,而不会影响到增删查结果的正确性。

在这里插入图片描述
存放key更好一些,当前咱们的hash函数只是一个简单粗暴的%,但是实际上的hash函数可能是非常复杂的运算(要经历一些列 + - * / % << >> …),hash函数的目标就是为了把key映射成下标(希望映射过程能够尽量避免冲突)

对于key为整数的时候,算hash一般比较好算,直接进行一些数学变换就行了。
如果key为String的时候,算hash就会比较复杂一些。(md5,sha1两种常用的字符串hash算法)

3.4负载因子

通过这个指标可以衡量元素冲突的概率,当前hash表中的实际元素个数/数组的capacity=> 负载因子,可以根据负载因子的值来决定是否要对hash表进行扩容,所谓的扩容就是申请一个更大的数组作为新的hash表,把原来的元素拷贝过去。(非常耗时的)

如果是闭散列:负载因子一定是小于1的。
如果是开散列:负载因子可以大于1,因为元素挂的是链表。

4.hash表的代码讲解

//通过开散列的方式来处理hash冲突
public class MyHashMap {
    static class Node {//用开散列的方式需要创建一个节点类
        public int key;
        public int value;
        public Node next;

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
    private static final double LOAD_FACTOR = 0.75;//设置一个负载因子
    //array就是hash表的本体,数组的每个元素又是一个链表的头结点,所以array的类型用Node[]来表示
    private Node[] array = new Node[101];
    private int size = 0;//表示当前hash表中的元素个数
    private int hashFunc(int key) {//构造一个hash函数
        //实际用的hashFunc一般会比较复杂,在这里我们就用相对简单的方式。
        return key % array.length;
    }
    //如果key已经已经存在,就修改当前的value值
    //如果key不存在,就插入新的键值对。
    public void put(int key,int value) {//插入hash表的操作
        //1.需要通过hash函数把key映射成数组下标。
        int index = hashFunc(key);
        //2.根据下标找到对应的链表
        Node list = array[index];
        //3.当前key在链表中是否存在。
        for (Node cur = list; cur != null; cur = cur.next) {
            if (cur.key == key) {//key已经存在,直接修改value即可。
                cur.value = value;
            }
        }
        //4.如果刚才循环结束,没有找到相同的key节点,那么就将键值对插入到指定链表的头部。
        //这里尾插也可以,只是在这头插方便所以用头插。
        Node newNode = new Node(key,value);
        newNode.next = list;
        array[index] = newNode;
        size++;
        if (size / array.length > LOAD_FACTOR) {//hash表的扩容操作
            resize();
        }
    }

    private void resize() {//hash表的扩容方法
        Node[] newArray = new Node[array.length * 2];
        //把原来hash表中的所有元素搬运到新的数组上
        for (int i = 0; i < array.length; i++) {
            for (Node cur = array[i]; cur != null; cur = cur.next) {
                int index = cur.key % newArray.length;//原来数组中的key在新的newArray数组中对应的下标
                Node newNode = new Node(cur.key,cur.value);//注意,这里用的也是头插法。
                newNode.next = newArray[index];
                newArray[index] = newNode;
            }
        }
        //让新的数组代替原来数组
        array = newArray;
    }

    //根据key查找指定元素,如果找到那么就返回对应的value,如果没找到,那么就返回null
    public Integer get(int key) {
        //1.先计算出key对应的下标
        int index = hashFunc(key);
        //2.根据下标找到对应的链表
        Node list = array[index];
        //3.在链表中查找指定元素
        for (Node cur = list;cur != null;cur = cur.next) {
            if (cur.key == key) {
                return cur.value;
            }
        }
        return null;
    }

}

5.哈希思想的应用

(实际工作中非常常见的场景)
例如:此处我有很多很多的数据(比如用户数据,用户数据中包含用户的账户,用户购物车内容,用户的注册时间,用户的浏览记录…),可能有几亿个用户数据,由于每个用户数据量都很大(10m),此时我们不能把所有的用户信息都放到一个内存中(要放入磁盘中),接下来如果想高效的查找到给定用户账户所对应的相关信息,该如何做呢?
例如:可以从磁盘文件中查找,但是计算机访问磁盘的操作是非常慢的【太低效】,要想高效,最好还是得在内存中查找,如果一台机器放不下,就使用多台机器来保存。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值