手写MyHashMap

64 阅读4分钟

哈希表基本原理

  • 数组通过下标访问数据的一种拓展
  • 核心:利用哈希函数,将键值映射到数组上(bucket)

Hash Function 哈希函数

  • 哈希函数是用来将一个字符串(或任何其他类型)转化为小于哈希表大小且大于等于零的整数
  • 一个好的哈希函数:
    • 可以尽可能少地产生冲突
    • 算的快

哈希函数案例

  • 一种广泛使用的哈希函数算法是使用数值33(Times 33算法)
hashcode("abcd") = (ascii(a) * 33 ^ 3 + ascii(b) * 33 ^ 2 + ascii(c) * 33 + ascii(d)) % HASH_SIZE
                 = (97 * 33 ^ 3 + 98 * 33 ^ 2 + 99 * 33 + 100) % HASH_SIZE
                 = 3595978 % HASH_SIZE
  • 其中 HASH_SIZE 表示哈希表的大小
  • 给出一个字符串作为key和一个哈希表的大小,返回这个字符串的哈希值

image.png

哈希冲突

  • 无论使用什么 hash function,都需要考虑冲突问题
  • 为啥会有冲突
    • 表面原因:有一些key会map到相同的index上
    • 本质:无限空间往有限空间映射

如何解决冲突

  • 设计好的hash函数
  • 改变哈希索引
    • Open hashing
    • Closed hashing
  • 扩容
    • 装填因子 Load factor: size/capacity
    • Java: LF > 0.75, resize()

闭散列:开放定址法

  • 线性探测
// 被占用了就往后找
hash = (hash(key) + i) % HASH_SIZE
i = 0, 1, 2, 3 ...
  • 插入/查找/删除怎么做?
    • 插入:先用hash(key)除以HASH_SIZE,如果冲突了就线性探测找空位
    • 查找:第一步同插入,如果发现不是就线性探测往后找
    • 删除:第一步同插入,如果发现不是就线性探测往后找,找到后删除并标记为删除
  • 二次探测/双重散列
// 二次探测
hash = (hash(key) + i^2) % HASH_SIZE
i = 0, 1, 2, 3 ...
双重散列:hash1冲突了就换hash2
  • 闭散列有什么问题?
    • 时间复杂度为O(N)
    • 每次删除都得标记

开散列:拉链法

  • 每个 bucket 对应一条链表,哈希值相同的元素直接连接在对应链表中
  • 链表的头节点存储在哈希表中
    • 空间更多,复杂度更高
    • 不能直接访问到元素
  • 拉链法的极端情况?
    • key 全都挂在一条链表上
  • Java 8:数组 + 链表 + 红黑树
  • Java 7:数组 + 链表

扩容:重哈希 rehashing

  • 哈希表容量的大小在一开始是不确定的,在需要的时候,可以对底层数组进行扩容
  • 一种简单的策略:如果哈希表存储元素太多,将哈希表容量扩大一倍,并将所有的key的哈希值重新计算映射到新的bucket上

动手实现HashMap

属性

// 1. Fields
private static final int DEFAULT_INIT_CAPACITY = 16;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;

class Node<K, V> {
    K key;
    V value;
    Node<K, V> next;

    public Node(K key, V value, Node<K, V> next) {
        this.key = key;
        this.value = value;
        this.next = next;
    }
}

// 底层数组初始化的容量 initCapacity
private int initCapacity;

// 装填因子LF loadFactor
private float loadFactor;

// HashMap 中元素个数 size
private int size;

// 采用数组 + 链表作为底层存储结构
private Node<K, V>[] table;

构造器

// 2. Constructors
public MyHashMap(){
    this(DEFAULT_INIT_CAPACITY, DEFAULT_LOAD_FACTOR);
}

public MyHashMap(int initCapacity, float loadFactor) {
    this.initCapacity = initCapacity;
    this.loadFactor = loadFactor;
}

哈希函数

private int hash(K key) {
    return key.hashCode() % this.table.length;
}

解决哈希冲突

  • 拉链法
  • 扩容

装填因子(扩容)

private void resize(int newCapacity) {
    System.out.println("Resizing: " + this.initCapacity);
    // 创建新的容量为newCapacity的node代替旧的node
    Node[] newTable = new Node[newCapacity];
    this.initCapacity = newCapacity;
    System.out.println("Resizing: " + this.initCapacity);
    // rehashing 重哈希,遍历老的哈希表中的哈希桶
    for (Node<K, V> node : table) {
        // 遍历链表
        while (node != null) {
            // 在新哈希表的index
            int index = hash(node.getKey());
            Node<K, V> newNode = new Node<>(node.getKey(), node.getValue(), null);
            if (newTable[index] != null) {
                // 找到合适插入的位置
                Node<K, V> head = newTable[index];
                while (head.getNext() != null) {
                    head = head.getNext();
                }
                // head.getNext() 为 null
                head.setNext(newNode);
            } else {
                // 直接插入到头节点
                newTable[index] = newNode;
            }
            node = node.getNext();
        }
        // 最好是把老的table中的元素设置成null,help GC

        // 将新的哈希表指向table
        this.table = newTable;
    }
}

方法实现

  • put/get/remove
/**
 * Put key-value entry into my hash map.
 * @param key specific key。
 * @param value value
 * @return null if add new entry, old value if key exists(update)
 */
public V put(K key, V value) {
    int index = hash(key);
    // 看这个下标节点有没有东西
    Node<K, V> node = table[index];

    // 哈希桶为空,直接插入新节点
    if (node == null) {
        this.table[index] = new Node<>(key, value, null);
        return null;
    }

    // 哈希桶不为空,两种情况:1. key存在,更新value  2. key不存在,新增key-value
    while (node != null) {
        if (node.key == key) {
            V oldValue = node.value;
            node.value = value;
            return oldValue;
        }

        if (node.getNext() == null) {
            node.setNext(new Node<>(key, value, null));
            return null;
        }

        node = node.next;
    }

    table[index] = new Node<>(key, value, null);
    return null;
}

private int hash(K key) {
    return key.hashCode() % this.table.length;
}

/**
 * Get related value by specific key.
 * @param key specific key.
 * @return null if key not exists, otherwise, return value with the specific key.
 */
public V get(K key) {
    // 确定哈希桶:key -> index
    int index = hash(key);
    Node<K, V> node = table[index];
    while (node != null) {
        if (node.getKey() == key) {
            return node.getValue();
        }

        node = node.getNext();
    }
    return null;
}

/**
 * Remove related value by specific key.
 * @param key specific key
 * @return null if key not exists, otherwise, return the value to be removed with the specific key.
 */
public V remove(K key) {
    // 确定哈希桶:key -> value
    int index = hash(key);
    Node<K, V> node = table[index];

    // 针对于链表头节点的逻辑
    if (node == null) {
        return null;
    }

    if (node.getKey() == key) {
        // 1. 保存被删掉node的next
        Node<K, V> temp = node.getNext();
        // 2. node的next节点设置为null
        node.setNext(null);
        table[index] = temp;
        return node.getValue();
    }

    Node<K, V> preNode = node;
    Node<K, V> curNode = node.getNext();
    // 针对于非头节点的逻辑
    while (curNode != null) {
        if (curNode.getKey() == key) {
            preNode.setNext(curNode.getNext());
            curNode.setNext(null);
            return curNode.getValue();
        }

        preNode = curNode;
        curNode = curNode.getNext();
    }
    return null;
}