Phase2 Day13 MyHashMap

1 符号表

  • 我们使用符号表这个词来描述一张抽象的表格,我们会将信息(值)存储在其中,然后按照指定的键来搜索并获取这些信息
  • 符号表有时也被称为字典。键就是单词,值就是单词对应的定义,发音和词源
  • 符号表有时又叫做索引。键就是术语,值就是书中该术语出现的所有页码
  • 无序符号表—HashMap
  • 有序符号表—TreeMap

在这里插入图片描述
符号表的应用
在这里插入图片描述

2 哈希表

2.1 概述

  • 如果所有的键都是小整数,我们可以用一个数组来实现的符号表,将键作为数组的索引,而数组中对应的位置存储键关联的值。哈希表是这种简单方法的扩展,并且能够处理更加复杂类型的键。我们需要用哈希函数将键转换成数组的索引
  • 哈希表的核心算法可以分为两步
    1.用哈希函数将键转换为数组中的一个索引。理想情况下不同的键都能转换成不同的索引值。当然这只是理想情况下,所以我们需要处理两个或者多个键都散列到相同索引值的情况 (哈希碰撞)。
    2.处理碰撞冲突:
      a. 开放地址法
      b. 拉链法
      c. 再散列法

哈希函数

一个优秀的 hash 算法,满足以下特点:

  • 正向快速:给定明文和 hash 算法,在有限时间和有限资源内能计算出 hash 值
  • 逆向困难:给定(若干) hash值,在有限时间内很难(基本不可能)逆推出明文。
  • 输入敏感:原始输入信息修改一点信息,产生的 hash 值看起来应该都有很大不同
  • 冲突避免:很难找到两段内容不同的明文,使得它们的 hash值一致(发生冲突)。即对于任意两个不同的数据块,其hash值相同的可能性极小;对于一个给定的数据块,找到和它hash值相同的数据块极为困难。

我们可以简单地把哈希函数理解为在模拟随机映射,然后从随机映射的角度去理解哈希函数

经典的哈希函数

  • MD4: 128位,MD4已被证明不够安全
  • MD5:MD5 已被证明不具备"强抗碰撞性"
  • SHA1:SHA-1 已被证明不具"强抗碰撞性"
  • SHA2: SHA3: 相关算法已经被提出

哈希算法的应用

  • 指纹 (身份的标识)
      a. 证书, 文件
      b. 加密
  • 散列
      a. 集群, 分布式
      b. 数据结构

在这里插入图片描述

注意事项:其实把hash算法当成是一种加密算法,这是不准确的,我们知道加密总是相对于解密而言的,没有解密何谈加密呢,HASH的设计以无法解密为目的的。并且如果我们不附加一个随机的salt值,HASH口令是很容易被字典攻击入侵的

2.2 碰撞处理:拉链法

  • 发生碰撞冲突的元素都存储在同一条链表中
  • 在哈希函数能保证平均分布的前提下,那么哈希表的性能就取决于链表的平均长度
  • 如果我们想在常数时间复杂度内, 完成哈希表的增删查操作,那么我们就得控制链表得平均长度不超过某个值。这个值我们称之为加载因子,也就是链表平均长度可以达到的最大值
  • 因此,当元素个数达到一定的数目的时候,我们就需要对数组进行扩容

3 MyHashMap

简化处理

  • 键不能为null。如果键为null,我们会抛出 NullPointerException
  • 值不能为null。我们这么规定的原因是:当键不存在的时候,get()方法会返回null。这样做有个好处,我们可以调用get()方法,看其返回值是否为 null 来判断键是否存在哈希表中
  • 缓存hash值。如果散列值计算很耗时 (比如长字符串)。那么我么可以在结点中用一个 hash 变量来保存它计算的 hash 值。Java 中的 String 就是这样做的
  • 注意!
      HashMap 类不同步,在并发场景中可能会出现许多问题。比较经典的一个问题是:查找的时候可能会出现死循环
      在这里插入图片描述

3.1 构造方法及内部类

package com.cskaoyan.hashmap;


import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Set;

/*
API:
    void put(K key, V value)
    V get(K key)
    void delete(K key)
    void clear()
    boolean contains(K key)
    boolean isEmpty()
    int size()
    Set<K> keys()
 */
public class MyHashMap<K, V> {
    private static final int DEFAULT_ARRAY_SIZE = 16;
    private static final int MAX_ARRAY_SIZE = 1 << 30;
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 属性
    private Entry[] table;
    private int size;
    private float loadFactor;//加载因子
    private int threshold; // 阈值

    private static class Entry {//私有内部类
        Object key;
        Object val;
        int hash;
        Entry next;//链表

        public Entry(Object key, Object val, int hash, Entry next) {
            this.key = key;
            this.val = val;
            this.hash = hash;
            this.next = next;
        }

        @Override
        public String toString() {
            return key + "=" + val;
        }
    }

    // 构造方法
    默认构造方法
    public MyHashMap() {
        this(DEFAULT_ARRAY_SIZE, DEFAULT_LOAD_FACTOR);
    }
    
	给定容量时构造方法
    public MyHashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
	
	给定容量和加载因子的构造方法
    public MyHashMap(int initialCapacity, float loadFactor) {
        // initialCapacity 初始数组大小
        if (initialCapacity <= 0 || initialCapacity > MAX_ARRAY_SIZE) {
            throw new IllegalArgumentException("initialCapacity=" + initialCapacity);
        }
        if (loadFactor <= 0) {
            throw new IllegalArgumentException("loadFactor=" + loadFactor);
        }
        this.loadFactor = loadFactor;
        
        计算大于等于initialCapacity的最小的2的幂
        int capacity = calculateCapacity(initialCapacity);
        table = new Entry[capacity];
        threshold = (int) (capacity * loadFactor);
    }
	JDK中提供非常巧妙找到一个数最小二次幂上限的方法
    private int calculateCapacity(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return n + 1;
    }
    重写tostring
     @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("{");
        for (Entry e : table) {
            while (e != null) {
                sb.append(e).append(", ");
                e = e.next;
            }
        }
        if (size > 0) sb.delete(sb.length() - 2, sb.length());
        return sb.append("}").toString();
    }
}

JDK中提供非常巧妙找到一个数最小二次幂上限的方法

在这里插入图片描述

3.2 Put方法

    /**
     * 添加键值对, 如果key已经存在, 则修改key关联的值
     *
     * @param key   键
     * @param value 值
     * @return key原来关联的值, 如果key不存在, 则返回null
     */
    @SuppressWarnings("unchecked")
    public V put(K key, V value) {
        if (key == null || value == null) {
            throw new IllegalArgumentException("Key or value cannot be null");
        }
        // 计算key的hash值
        int hash = hash(key);
        // 计算key应该位于数组的哪条链表中
        int idx = indexFor(hash, table.length);
        // 遍历链表, 查看key是否已经存在
        for (Entry e = table[idx]; e != null; e = e.next) {
            if (hash == e.hash && (key == e.key || key.equals(e.key))) {
                // 找到key,并更新value
                V oldValue = (V) e.val;
                e.val = value;
                return oldValue;
            }
        }
        // 没找到,添加结点
        addEntry(key, value, hash, idx);
        return null;
    }

    private void addEntry(K key, V value, int hash, int idx) {
        // 判断table是否需要扩容
        if (size >= threshold) {
            if (table.length == MAX_ARRAY_SIZE) {//数组达到最大长度
                // 破坏加载因子的限制,达到最大上限
                threshold = Integer.MAX_VALUE;
            } else {//没达到最大长度,数组*2
                grow(table.length << 1);
                // 重新计算键值对应该插入的位置
                idx = indexFor(hash, table.length);
            }
        }
        // 添加键值对(头插法)
        Entry entry = new Entry(key, value, hash, table[idx]);
        //创建Entry对象指向Entry数组对应索引的头节点
        table[idx] = entry;
        //然后再把头节点赋值给创建的Entry
        size++;
    }

    private void grow(int newCapacity) {
        Entry[] newTable = new Entry[newCapacity];
        // 把所有的键值对添加到newTable
        for (Entry e : table) {//遍历old数组
            while (e != null) {//当old数组某一索引位置上仍有元素,继续替换
                Entry next = e.next;//先保存old数组头节点的next
                int idx = indexFor(e.hash, newCapacity);//计算在新数组上的索引
                // 头插法
                e.next = newTable[idx];//old数组上的头节点指向new数组对应索引的头节点
                newTable[idx] = e;//要插入的目标Entry赋值给new数组的头节点
                e = next;//老数组的next上来顶替被换的节点
            }
        }
        
        //修改table和threshold
        table = newTable;
        threshold = (int) (newCapacity * loadFactor);
    }
    
	//计算索引位置
    private int indexFor(int hash, int length) {
        return hash & (length - 1);
    }
	//哈希函数
    private int hash(K key) {
        int h = key.hashCode();
        return (h >> 16) ^ (h << 16);
    }

3.2 Gget方法

    /**
     * 获取key关联的值
     *
     * @param key 键
     * @return key关联的值
     */
    @SuppressWarnings("unchecked")
    public V get(K key) {
        if (key == null) throw new IllegalArgumentException("Key cannot be null");
        int hash = hash(key);
        int idx = indexFor(hash, table.length);
        // 遍历链表
        for (Entry e = table[idx]; e != null; e = e.next) {
            if (hash == e.hash && (key == e.key || key.equals(e.key))) {
                return (V) e.val;
            }
        }
        return null;
    }

3.4 delete方法

    /**
     * 删除键等于key键值对
     *
     * @param key 键
     * @return key关联的值
     */
    @SuppressWarnings("unchecked")
    public V delete(K key) {
        if (key == null) throw new IllegalArgumentException("Key cannot be null");
        int hash = hash(key);
        int idx = indexFor(hash, table.length);
        for (Entry p = null, e = table[idx]; e != null; e = e.next) {//p节点用来存储遍历节点的上一个节点,用于删除
            if (hash == e.hash && (key == e.key || key.equals(e.key))) {
                // 删除键值对
                V deleteValue = (V) e.val;
                if (p == null) table[idx] = e.next;
                else p.next = e.next;
                size--;
                return deleteValue;
            }
            p = e;
        }
        return null;
    }

3.5 获取键的集合

    /**
     * 获取键的集合
     *
     * @return 键的集合
     */
    @SuppressWarnings("unchecked")
    public Set<K> keys() {
        Set<K> set = new LinkedHashSet<>();
        for (Entry e : table) {
            while (e != null) {
                set.add((K) e.key);
                e = e.next;
            }
        }
        return set;
    }

3.6 其他小方法

    /**
     * 清空所有键值对
     */
    public void clear() {
        for (int i = 0; i < table.length; i++) {
            table[i] = null;
        }
        size = 0;
    }

    /**
     * 获取哈希表中键值对的个数
     *
     * @return 键值对的个数
     */
    public int size() {
        return size;
    }

    /**
     * 判断哈希表是否为空
     *
     * @return 如果哈希表为空返回true, 否则返回false
     */
    public boolean isEmpty() {
        return size == 0;
    }

       /**
     * 判断键是否存在
     *
     * @param key 键
     * @return 如果键存在返回true, 否则返回false
     */
    public boolean contains(K key) {
        return get(key) != null;
    }



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值