Skip to content

哈希表

字数
2467 字
阅读时间
11 分钟

哈希表(Hash table),也称作散列表,是一种通过哈希函数将键(key)映射到存储位置(索引)来访问数据的数据结构。它提供了快速的数据查找、插入和删除操作。

哈希函数

哈希函数的作用是将键值映射到哈希表上的索引。通过计算键的哈希值,然后将哈希值映射到哈希表(通常是数组)的某个位置,从而实现快速访问。

哈希函数示意图

如果哈希函数计算得到的哈希值(hashCode)超出了哈希表的大小(tableSize),通常会通过取模运算 (hashCode % tableSize) 将其映射到哈希表内的有效索引范围。

然而,当存储的数据量较大时,即使哈希函数设计得再好,也可能出现不同的键被映射到哈希表同一个索引位置的情况。这就是哈希碰撞

哈希碰撞

哈希碰撞示意图

如上图所示,小李和小王都被映射到了索引下标 1 的位置,这种现象称为哈希碰撞

解决哈希碰撞的常见方法有两种:拉链法线性探测法

拉链法 (Separate Chaining)

拉链法示意图

拉链法通过在哈希表的每个索引位置存储一个链表(或其它数据结构),将所有映射到该索引的元素都存储在这个链表中。

选择合适的哈希表大小(tableSize)非常重要。一个合适的 tableSize 既能避免因数组空值而浪费大量内存,也能避免因链表过长而导致查找效率降低。

线性探测法 (Linear Probing)

线性探测法示意图

线性探测法在发生碰撞时,会向后查找哈希表中的下一个空闲位置来存储冲突的元素。因此,使用线性探测法时,必须保证哈希表的大小 (tableSize) 大于实际存储的数据量 (dataSize),以确保总能找到空位。

常见哈希结构对比 (C++)

在 C++ 中,setmap 分别提供了基于红黑树和哈希表的多种数据结构。

集合 (Set)

集合底层原理是否有序数值是否可以重复能否更改数值查询效率增删效率
std::set红黑树有序O(log n)O(log n)
std::multiset红黑树有序O(log n)O(log n)
std::unordered_set哈希表无序O(1)O(1)

补充:红黑树是一种平衡二叉搜索树,因此其键(key)是有序的。但键本身不可修改,因为修改键值会导致整棵树的结构错乱,只能通过删除旧键再插入新键的方式进行“修改”。

映射 (Map)

映射底层原理是否有序键是否可以重复键能否修改查询效率增删效率
std::map红黑树键有序O(log n)O(log n)
std::multimap红黑树键有序O(log n)O(log n)
std::unordered_map哈希表键无序O(1)O(1)

哈希表相关称呼

应用示例

用 Set 判断快乐数

202. 快乐数 - 力扣(LeetCode)

「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是无限循环但始终变不到 1。如果可以变为 1,那么这个数就是快乐数。

如果 n 是快乐数就返回 True;不是,则返回 False

示例:

输入:19
输出:true
解释:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1

解题思路:

  • 在求解快乐数的计算过程中,若出现非快乐数计算结果重复,意味着已经陷入循环,断然不是快乐数。
  • 同时,非快乐数的计算最终都会进入 4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4 的循环中。
java
import java.util.HashSet;
import java.util.Set;

class Solution {
    /**
     * 计算一个数的每个位上的数字的平方和
     * @param n 输入的整数
     * @return 平方和
     */
    public int getSum(int n) {
        int sum = 0;
        int temp = 0;
        while (n > 0) {
            temp = n % 10; // 取出个位数
            sum += temp * temp; // 平方并累加
            n /= 10; // 去掉个位数
        }
        return sum;
    }

    /**
     * 判断一个数是否为快乐数
     * @param n 输入的整数
     * @return 如果是快乐数返回 true,否则返回 false
     */
    public boolean isHappy(int n) {
        Set<Integer> set = new HashSet<>(); // 用于检测循环
        while (n != 1 && !set.contains(n)) { // 如果不是 1 且没有出现循环
            set.add(n); // 将当前数加入集合
            n = getSum(n); // 计算下一个数
        }
        return n == 1; // 如果最终变为 1,则是快乐数
    }
}

两数之和

1. 两数之和 - 力扣(LeetCode)

注意target 为任意值。

这是一个简单的哈希表应用,利用哈希表键的唯一性来快速查找目标值。

java
import java.util.HashMap;
import java.util.Map;

class Solution {
    /**
     * 在数组中找到两个数,使其和等于目标值
     * @param nums 整数数组
     * @param target 目标和
     * @return 两个数的索引,如果不存在则返回空数组
     */
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> hashTable = new HashMap<>(); // 存储数值和其索引
        for (int i = 0; i < nums.length; ++i) {
            // 检查 target - 当前数 是否存在于哈希表中
            if (hashTable.containsKey(target - nums[i])) {
                return new int[]{i, hashTable.get(target - nums[i])};
            }
            // 将当前数和其索引放入哈希表
            hashTable.put(nums[i], i);
        }
        return new int[0]; // 未找到
    }
}

四数相加 II

454. 四数相加 II - 力扣(LeetCode)

这道题目与 三数之和四数之和 有所不同。它要求找到四个独立数组 A, B, C, D 中,使得 A[i] + B[j] + C[k] + D[l] = 0 的元组数量。不需要考虑重复的四个元素相加等于 0 的情况。

解题思路:

  1. 首先定义一个 HashMapkey 存储 nums1nums2 中任意两数之和,value 存储该和出现的次数。
  2. 遍历 nums1nums2 数组,统计所有可能的两数之和及其出现次数,并存入 HashMap
  3. 定义一个 int 变量 count,用来统计 a + b + c + d = 0 出现的总次数。
  4. 遍历 nums3nums4 数组,计算 c + d。然后查找 HashMap 中是否存在 -(c + d) 这个键。
  5. 如果存在,则将 HashMap 中该键对应的 value(即出现次数)累加到 count 中。
  6. 最后返回统计值 count
java
import java.util.HashMap;
import java.util.Map;

class Solution {
    /**
     * 计算四个数组中和为 0 的元组数量
     * @param nums1 数组1
     * @param nums2 数组2
     * @param nums3 数组3
     * @param nums4 数组4
     * @return 和为 0 的元组数量
     */
    public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        Map<Integer, Integer> hashMap = new HashMap<>();
        int tempSum = 0;
        int count = 0;

        // 统计 nums1 和 nums2 中所有两数之和的频率
        for (int i : nums1) {
            for (int j : nums2) {
                tempSum = i + j;
                hashMap.put(tempSum, hashMap.getOrDefault(tempSum, 0) + 1);
            }
        }

        // 遍历 nums3 和 nums4,查找是否存在对应的和
        for (int k : nums3) {
            for (int l : nums4) {
                tempSum = k + l;
                // 如果 hashMap 中存在 -(tempSum),则累加其频率
                if (hashMap.containsKey(-tempSum)) {
                    count += hashMap.get(-tempSum);
                }
            }
        }
        return count;
    }
}

赎金信

这是一个典型的哈希表应用,用于统计字符频率。

使用原生哈希表 (Map)

java
import java.util.HashMap;
import java.util.Map;

class Solution {
    /**
     * 判断赎金信能否由杂志中的字母构成
     * @param ransomNote 赎金信字符串
     * @param magazine 杂志字符串
     * @return 如果能构成返回 true,否则返回 false
     */
    public boolean canConstruct(String ransomNote, String magazine) {
        // 统计 ransomNote 中字符的频率
        Map<Character, Integer> ransomMap = new HashMap<>();
        for (char c : ransomNote.toCharArray()) {
            ransomMap.put(c, ransomMap.getOrDefault(c, 0) + 1);
        }

        // 统计 magazine 中字符的频率
        Map<Character, Integer> magazineMap = new HashMap<>();
        for (char c : magazine.toCharArray()) {
            magazineMap.put(c, magazineMap.getOrDefault(c, 0) + 1);
        }

        // 检查 ransomNote 中的每个字符是否都能在 magazine 中找到足够的数量
        for (char c : ransomMap.keySet()) {
            if (!magazineMap.containsKey(c) || magazineMap.get(c) < ransomMap.get(c)) {
                return false;
            }
        }
        return true;
    }
}

使用数组作为哈希表

考虑到题目中测试用例通常只涉及小写字母(共 26 个),可以使用数组来模拟哈希表,这样可以获得更好的性能,因为数组的访问速度比 HashMap 更快,且避免了哈希函数计算和处理碰撞的开销。

java
class Solution {
    /**
     * 判断赎金信能否由杂志中的字母构成(使用数组作为哈希表)
     * @param ransomNote 赎金信字符串
     * @param magazine 杂志字符串
     * @return 如果能构成返回 true,否则返回 false
     */
    // 时间复杂度: O(m + n),m 为 ransomNote 长度,n 为 magazine 长度
    // 空间复杂度:O(1),因为数组大小固定为 26
    public boolean canConstruct(String ransomNote, String magazine) {
        int[] charCounts = new int[26]; // 存储 magazine 中每个字符的频率

        // 统计 magazine 中每个字符的频率
        for (char c : magazine.toCharArray()) {
            charCounts[c - 'a']++;
        }

        // 遍历 ransomNote,检查所需字符是否足够
        for (char c : ransomNote.toCharArray()) {
            // 如果当前字符在 magazine 中数量不足,或者根本不存在
            if (charCounts[c - 'a'] <= 0) {
                return false;
            }
            charCounts[c - 'a']--; // 使用一个字符,频率减一
        }
        return true;
    }
}

贡献者

The avatar of contributor named as jiechen jiechen

页面历史

撰写