哈希表

概述

  • 数据结构
    1. 连续型 => 数组(栈)
    2. 离散型 => 链表(队列 => 双端链表) | 二叉树
  • 拥有键值对元素的无序集合
  • 键的值是唯一的,键对应的值可以通过键来获取、更新或移除
  • Insert/Search/Delete => O(1)

基本原理

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

重要概念

  • 键值对
  • 哈希桶/哈希槽
  • 装填因子/负载因子 => Load Factor
  • 哈希函数/散列函数
  • 哈希冲突

Hash Function 哈希函数

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

Times 33 算法

  • 假设任何字符串都是基于33的一个大整数

case: hashcode("abcd") = (ascii(a) * (33 ^ 3) + ascii(b) * (33 ^ 2) + ascii(c) * (33 ^ 1) + ascii(d) * (33 ^ 0)) % hashSize

  • 给出一个字符串作为 key 和一个哈希表的大小,返回这个字符串的哈希值
    public int hashCode(String key, int hashSize) {
      long result = 0;
      char[] chars = key.toCharArray();
      for (int i = 0; i < chars.length; i++) {
        result = (result * 33 + ((int) chars[i])) % hashSize;
      }
      return (int) result;
    }
    

哈希冲突

  • 无论使用什么 Hash Function,都需要考虑冲突问题
  • 为啥会有冲突
    1. 有一些 key 会 map 到相同的 index 上
    2. 无限空间往有限空间映射

解决哈希冲突

  1. 设计好的 Hash Function
  2. 改变 Hash 索引
    1. Open Hashing
    2. Closed Hashing
  3. 扩容 => 负载因子 Load Factor: size/capacity => Java: LF > 0.75 -> resize()
闭散列 Closed Hashing

闭散列 => 开放定址法 => 负载因子大的时候还是会有冲突

  1. 线性探测 => hash = (hash(key) + i) % HASH_SIZE => i = 0, 1, 2, 3...
  2. 二次探测 => hash = (hash(key) + i ^ 2) % HASH_SIZE => i = 0, 1, 2, 3...
  3. 双重散列 => 使用两个 Hash Function,如果第一个 Hash Function 冲突了,就使用第二个 Hash Function
开散列 Open Hashing

开散列 => 拉链法 => 每个 bucket 对应一条链表,哈希值相同的元素直接连接在对应链表中 => 数组 + 链表

扩容
  • 重哈希 => rehashing
  • 哈希表容量的大小在一开始是不确定的
  • 如果哈希表存储元素太多,将哈希表容量扩大一倍,并将所有的 key 的哈希值重新计算映射到新的 bucket 上
  • 渐进式 rehash => 避免集中 rehash 带来的庞大的计算量和内存操作 => 使用两个 HashTable,在进行操作的时候再 rehash,查询的时候先在新的 HashTable 里面查,之后 fallback 到老的 HashTable 里面查 => 将一次的 rehash 均摊多多次的操作中
class Rehashing {
    public ListNode[] rehashing(ListNode[] hashTable) {
        if (hashTable == null || hashTable.length == 0) {
            return null;
        }
        int capacity = hashTable.length;
        int newCapacity = capacity * 2;
        ListNode[] newHashTable = new ListNode[newCapacity];

        for (ListNode head : hashTable) {
            while (head != null) {
                int key = head.val;
                int hashcode = key % newCapacity;
                if (newHashTable[hashcode] != null) {
                    ListNode node = newHashTable[hashcode];
                    while (node != null && node.next != null) {
                        node = node.next;
                    }
                    node.next = new ListNode(key);
                } else {
                    newHashTable[hashcode] = new ListNode(key);
                }
                head = head.next;
            }
        }
        return newHashTable;
    }
}

基本操作

以 Java 为例

  • 增 => put
  • 删 => remove/clear
  • 改 => put
  • 查 => get
  • containsKey/containsValue(O(n))
  • size/isEmpty
  • 遍历 => keySet/values/entrySet/forEach
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

public class Traversal {
    public Map<String, String> map = new HashMap<>();

    public void traversalForLambda() {
        // 推荐
        map.forEach((key, value) -> System.out.println("The key: " + key + ", the value: " + value));
    }

    public void traversalForEach() {
        // 推荐
        for (Entry<String, String> entry : map.entrySet()) {
            System.out.println("The key: " + entry.getKey() + ", the value: " + entry.getValue());
        }
    }

    public void traversalForKeySet() {
        for (String key : map.keySet()) {
            System.out.println("The key: " + key + ", the value: " + map.get(key));
        }
    }

    public void traversalForIterator() {
        Iterator<Entry<String, String>> iterator = map.entrySet().iterator();

        while (iterator.hasNext()) {
            Entry<String, String> entry = iterator.next();
            System.out.println("The key: " + entry.getKey() + ", the value: " + entry.getValue());
        }
    }
}

知识点

  • Java1.7 扩容时 => 头插法 => 有死循环
  • Java1.8 扩容时 => 尾插法 => 避免死循环
  • Character -> Integer 可以转化成数组存储 => char - 'a' => int -> int
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容