首页 > 其他分享 >六. 哈希表

六. 哈希表

时间:2025-01-03 22:44:45浏览次数:1  
标签:index 哈希 buckets key pair self

哈希表
哈希表又称散列表,通过建立键 key 与值 value 之间的映射,实现高效的元素查询。当对哈希表输入键 key 时,即可查询到对应的值 value,其时间复杂度仅为 O(1)。

1. 哈希表

1.1. 哈希表常用操作

1.1.1. 基础操作

基础操作包括初始化查询添加键值对删除键值对

# 初始化哈希表
hmap: dict = {}

# 添加操作
# 在哈希表中添加键值对 (key, value)
hmap[12836] = "小哈"
hmap[15937] = "小啰"
hmap[16750] = "小算"
hmap[13276] = "小法"
hmap[10583] = "小鸭"

# 查询操作
# 向哈希表中输入键 key ,得到值 value
name: str = hmap[15937]

# 删除操作
# 在哈希表中删除键值对 (key, value)
hmap.pop(10583)
/* 初始化哈希表 */
unordered_map<int, string> map;

/* 添加操作 */
// 在哈希表中添加键值对 (key, value)
map[12836] = "小哈";
map[15937] = "小啰";
map[16750] = "小算";
map[13276] = "小法";
map[10583] = "小鸭";

/* 查询操作 */
// 向哈希表中输入键 key ,得到值 value
string name = map[15937];

/* 删除操作 */
// 在哈希表中删除键值对 (key, value)
map.erase(10583);

可以从代码看出来,实际上,在 python 中,其初始化实际上使用了字典。在C++ 使用的是标准库中的一种关联容器,它存储键值对。

哈希表与数组和链表的常用操作的对比如下所示:

数组 链表 哈希表
查找(指定) O(n) O(n) O(1)
添加(尾部) O(1) O(1) O(1)
删除(指定) O(n) O(n) O(1)

1.1.2. 遍历操作

在哈希表中,遍历可以分为:遍历键值对、遍历键、遍历值

# 遍历哈希表(某一字典)
# 获取键值对视图
# 遍历键值对 key->value
for key, value in hmap.items():
    print(key, "->", value)
# 单独遍历键 key
for key in hmap.keys():
    print(key)
# 单独遍历值 value
for value in hmap.values():
    print(value)
/* 遍历哈希表 */
// 遍历键值对 key->value
for (auto kv: map) {
    cout << kv.first << " -> " << kv.second << endl;
}
// 使用迭代器遍历 key->value
for (auto iter = map.begin(); iter != map.end(); iter++) {
    cout << iter->first << "->" << iter->second << endl;
}

1.2. 哈希表的简单实现

假设使用一个数组来实现哈希表。在哈希表中,将数组中的每个空位称为桶(bucket),每个桶可存储一个键值对。因此,查询操作就是找到 key 对应的桶,并在桶中获取 value 。

利用 key 来找到对应的桶则需要使用某个哈希函数 hash()实现,换句话说,输入一个 key ,我们可以通过哈希函数得到该 key 对应的键值对在数组中的存储位置 index

其计算步骤可概括为:index = hash(key) % capacity,其中 hash(key)得到 key 对应的哈希值,capacity 为桶的数量,也就是数组的长度。设 capacity 为 100,其工作原理为:
image.png

则,简单实现哈希表:

#将key和value封装成一个类Pair
class Pair:
    """键值对"""
    def __init__(self, key: int, val: str):
        self.key = key
        self.val = val

class ArrayHashMap:
    """基于数组实现的哈希表"""
    def __init__(self):
        """构造方法"""
        # 初始化数组,包含 100 个空桶,该写法等价于:self.buckets = [None for _ in range(100)]
        self.buckets: list[Pair | None] = [None] * 100

    def hash_func(self, key: int) -> int:
        """哈希函数"""
        index = key % 100
        return index

    def get(self, key: int) -> str:
        """查询操作"""
        index: int = self.hash_func(key)#通过哈希函数计算索引值
        pair: Pair = self.buckets[index]
        if pair is None:
            return None
        return pair.val

    def put(self, key: int, val: str):
        """添加操作"""
        pair = Pair(key, val)
        index: int = self.hash_func(key)
        self.buckets[index] = pair

    def remove(self, key: int):
        """删除操作"""
        index: int = self.hash_func(key)
        # 置为 None ,代表删除
        self.buckets[index] = None

    def entry_set(self) -> list[Pair]:
        """获取所有键值对"""
        result: list[Pair] = []
        for pair in self.buckets:
            if pair is not None:
                result.append(pair)
        return result

    def key_set(self) -> list[int]:
        """获取所有键"""
        result = []
        for pair in self.buckets:
            if pair is not None:
                result.append(pair.key)
        return result

    def value_set(self) -> list[str]:
        """获取所有值"""
        result = []
        for pair in self.buckets:
            if pair is not None:
                result.append(pair.val)
        return result

    def print(self):
        """打印哈希表"""
        for pair in self.buckets:
            if pair is not None:
                print(pair.key, "->", pair.val)
/* 键值对 */
struct Pair {
  public:
    int key;
    string val;
    Pair(int key, string val) {
        this->key = key;
        this->val = val;
    }
};

/* 基于数组实现的哈希表 */
class ArrayHashMap {
  private:
    vector<Pair *> buckets;

  public:
    ArrayHashMap() {
        // 初始化数组,包含 100 个桶
        buckets = vector<Pair *>(100);
    }

    ~ArrayHashMap() {
        // 释放内存
        for (const auto &bucket : buckets) {
            delete bucket;
        }
        buckets.clear();
    }

    /* 哈希函数 */
    int hashFunc(int key) {
        int index = key % 100;
        return index;
    }

    /* 查询操作 */
    string get(int key) {
        int index = hashFunc(key);
        Pair *pair = buckets[index];
        if (pair == nullptr)
            return "";
        return pair->val;
    }

    /* 添加操作 */
    void put(int key, string val) {
        Pair *pair = new Pair(key, val);
        int index = hashFunc(key);
        buckets[index] = pair;
    }

    /* 删除操作 */
    void remove(int key) {
        int index = hashFunc(key);
        // 释放内存并置为 nullptr
        delete buckets[index];
        buckets[index] = nullptr;
    }

    /* 获取所有键值对 */
    vector<Pair *> pairSet() {
        vector<Pair *> pairSet;
        for (Pair *pair : buckets) {
            if (pair != nullptr) {
                pairSet.push_back(pair);
            }
        }
        return pairSet;
    }

    /* 获取所有键 */
    vector<int> keySet() {
        vector<int> keySet;
        for (Pair *pair : buckets) {
            if (pair != nullptr) {
                keySet.push_back(pair->key);
            }
        }
        return keySet;
    }

    /* 获取所有值 */
    vector<string> valueSet() {
        vector<string> valueSet;
        for (Pair *pair : buckets) {
            if (pair != nullptr) {
                valueSet.push_back(pair->val);
            }
        }
        return valueSet;
    }

    /* 打印哈希表 */
    void print() {
        for (Pair *kv : pairSet()) {
            cout << kv->key << " -> " << kv->val << endl;
        }
    }
};

1.3. 哈希冲突与扩容

还是看原来计算索引的这个公式 index = hash(key) % capacity,根据这个公式,所以在计算过程中一定存在“多个输入对应相同输出”的情况。就比如当输入的 key 后两位相同时,哈希函数的输出结果也相同:

12836 % 100 = 36
20336 % 100 = 36

image.png

这也就是哈希冲突,而为了解决这一冲突,则主要使用扩容哈希表的操作来解决:
image.png

类似于数组扩容,哈希表扩容需将所有键值对从原哈希表迁移至新哈希表,非常耗时;并且由于哈希表容量 capacity 改变,我们需要通过哈希函数来重新计算所有键值对的存储位置,这进一步增加了扩容过程的计算开销。为此,编程语言通常会预留足够大的哈希表容量,防止频繁扩容
负载因子(load factor)是哈希表的一个重要概念,其定义为哈希表的元素数量除以桶数量,用于衡量哈希冲突的严重程度,也常作为哈希表扩容的触发条件

2. 哈希冲突

由于在通常情况下,哈希函数的输入空间远大于输出空间,因此哈希冲突无法避免。

哈希冲突容易导致查询结果错误,影响可用性。而频繁的进行哈希表的扩容,则造成效率过低。因此可以采用如下策略:

  1. 改良数据结构,使得哈希表在哈希冲突出现时正常工作。常用的改良方法为“链式地址”与“开放寻址”
  2. 仅在哈希冲突严重时进行扩容。

2.1. 链式地址

该方法将原始的由数组组成的,每个桶仅存储一个键值对的哈希表转换为利用链表存储:将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。

image.png

2.1.1. 链式地址哈希表常用操作

基于链表实现的哈希表其操作发生了改变:

  • 查询操作:第一步与原始哈希表相同,利用 key 并经过哈希函数找到对应的桶(也就是对应的链表节点),然后再利用 key 在这些发生冲突的元素中进行查找从而找到对应元素
  • 添加操作:通过哈希函数计算访问链表头节点,将节点(键值对)添加到链表中
  • 删除元素:通过哈希函数计算访问链表头节点,接着遍历链表以查找目标节点并将其删除。

利用列表(这样的话,就由索引替代了节点)代替链表进行简单实现,当负载因子超过 2/3 时,哈希表进行扩容

class HashMapChaining:
	"""链式地址哈希表"""

	def __init__(self):
	    """构造方法"""
	    self.size = 0  # 键值对数量
	    self.capacity = 4  # 哈希表容量
	    self.load_thres = 2.0 / 3.0  # 触发扩容的负载因子阈值
	    self.extend_ratio = 2  # 扩容倍数
	    self.buckets = [[] for _ in range(self.capacity)]  # 桶数组
	
	def hash_func(self, key: int) -> int:
	    """哈希函数"""
	    return key % self.capacity
	
	def load_factor(self) -> float:
	    """负载因子"""
	    return self.size / self.capacity
	
	def get(self, key: int) -> str | None:
	    """查询操作"""
	    index = self.hash_func(key)
	    bucket = self.buckets[index]
	    # 遍历桶,若找到 key ,则返回对应 val
	    for pair in bucket:
	        if pair.key == key:
	            return pair.val
	    # 若未找到 key ,则返回 None
	    return None
	
	def put(self, key: int, val: str):
	    """添加操作"""
	    # 当负载因子超过阈值时,执行扩容
	    if self.load_factor() > self.load_thres:
	        self.extend()
	    index = self.hash_func(key)
	    bucket = self.buckets[index]# 注意,这里肯定是总能找到对应的索引的,因为对键key是取余操作
	    # 遍历桶,若遇到指定 key ,则更新对应 val 并返回,
	    for pair in bucket:
	        if pair.key == key:
	            pair.val = val
	            return
	    # 若无该 key ,则将键值对添加至尾部
	    pair = Pair(key, val)
	    bucket.append(pair)
	    self.size += 1
	
	def remove(self, key: int):
	    """删除操作"""
	    index = self.hash_func(key)
	    bucket = self.buckets[index]
	    # 遍历桶,从中删除键值对
	    for pair in bucket:
	        if pair.key == key:
	            bucket.remove(pair)
	            self.size -= 1
	            break
	
	def extend(self):
	    """扩容哈希表"""
	    # 暂存原哈希表
	    buckets = self.buckets
	    # 初始化扩容后的新哈希表
	    self.capacity *= self.extend_ratio
	    self.buckets = [[] for _ in range(self.capacity)]
	    self.size = 0
	    # 将键值对从原哈希表搬运至新哈希表
	    for bucket in buckets:
	        for pair in bucket:
	            self.put(pair.key, pair.val)
	
	def print(self):
	    """打印哈希表"""
	    for bucket in self.buckets:
	        res = []
	        for pair in bucket:
	            res.append(str(pair.key) + " -> " + pair.val)
	        print(res)
/* 链式地址哈希表 */
class HashMapChaining {
  private:
    int size;                       // 键值对数量
    int capacity;                   // 哈希表容量
    double loadThres;               // 触发扩容的负载因子阈值
    int extendRatio;                // 扩容倍数
    vector<vector<Pair *>> buckets; // 桶数组

  public:
    /* 构造方法 */
    HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {
        buckets.resize(capacity);
    }

    /* 析构方法 */
    ~HashMapChaining() {
        for (auto &bucket : buckets) {
            for (Pair *pair : bucket) {
                // 释放内存
                delete pair;
            }
        }
    }

    /* 哈希函数 */
    int hashFunc(int key) {
        return key % capacity;
    }

    /* 负载因子 */
    double loadFactor() {
        return (double)size / (double)capacity;
    }

    /* 查询操作 */
    string get(int key) {
        int index = hashFunc(key);
        // 遍历桶,若找到 key ,则返回对应 val
        for (Pair *pair : buckets[index]) {
            if (pair->key == key) {
                return pair->val;
            }
        }
        // 若未找到 key ,则返回空字符串
        return "";
    }

    /* 添加操作 */
    void put(int key, string val) {
        // 当负载因子超过阈值时,执行扩容
        if (loadFactor() > loadThres) {
            extend();
        }
        int index = hashFunc(key);
        // 遍历桶,若遇到指定 key ,则更新对应 val 并返回
        for (Pair *pair : buckets[index]) {
            if (pair->key == key) {
                pair->val = val;
                return;
            }
        }
        // 若无该 key ,则将键值对添加至尾部
        buckets[index].push_back(new Pair(key, val));
        size++;
    }

    /* 删除操作 */
    void remove(int key) {
        int index = hashFunc(key);
        auto &bucket = buckets[index];
        // 遍历桶,从中删除键值对
        for (int i = 0; i < bucket.size(); i++) {
            if (bucket[i]->key == key) {
                Pair *tmp = bucket[i];
                bucket.erase(bucket.begin() + i); // 从中删除键值对
                delete tmp;                       // 释放内存
                size--;
                return;
            }
        }
    }

    /* 扩容哈希表 */
    void extend() {
        // 暂存原哈希表
        vector<vector<Pair *>> bucketsTmp = buckets;
        // 初始化扩容后的新哈希表
        capacity *= extendRatio;
        buckets.clear();
        buckets.resize(capacity);
        size = 0;
        // 将键值对从原哈希表搬运至新哈希表
        for (auto &bucket : bucketsTmp) {
            for (Pair *pair : bucket) {
                put(pair->key, pair->val);
                // 释放内存
                delete pair;
            }
        }
    }

    /* 打印哈希表 */
    void print() {
        for (auto &bucket : buckets) {
            cout << "[";
            for (Pair *pair : bucket) {
                cout << pair->key << " -> " << pair->val << ", ";
            }
            cout << "]\n";
        }
    }
};

链式地址存在以下局限性。

  • 占用空间增大:链表包含节点指针,它相比数组更加耗费内存空间。
  • 查询效率降低:因为需要线性遍历链表来查找对应元素。查询效率 O(n),此时可以将链表转换为“AVL 树”或“红黑树”,从而将查询操作的时间复杂度优化至 O(log⁡n) 。

2.2. 开放寻址

相比于链式地址,开放寻址不引入额外的数据结构,通过“多次探测”来处理哈希冲突。其主要方式为:线性探测、平方探测和多次哈希。

在开放寻址哈希表中无法直接删除元素
image.png
为了解决该问题,采用懒删除(lazy deletion)机制:它不直接从哈希表中移除元素,而是利用一个常量 TOMBSTONE 来标记这个桶。在该机制下,None 和 TOMBSTONE 都代表空桶,都可以放置键值对。但不同的是,线性探测到 TOMBSTONE 时应该继续遍历,因为其之下可能还存在键值对。

然而,懒删除可能会加速哈希表的性能退化。这是因为每次删除操作都会产生一个删除标记,随着 TOMBSTONE 的增加,搜索时间也会增加,因为线性探测可能需要跳过多个 TOMBSTONE 才能找到目标元素。

2.2.1. 线性探测

线性探测采用固定步长的线性搜索来进行探测,其大体可以概括为,当进行元素的插入和查询时,当发生哈希冲突时,则从冲突位置按照固定的步长向后进行线性遍历来找到空桶或找到对应的元素。具体如图:
image.png

线性探测容易产生“聚集现象”。具体来说,数组中连续被占用的位置越长,这些连续位置发生哈希冲突的可能性越大,从而进一步促使该位置的聚堆生长,形成恶性循环,最终导致增删查改操作效率劣化。

为了避免删除操作的弊端,考虑在线性探测中记录遇到的首个 TOMBSTONE 的索引,并将搜索到的目标元素与该 TOMBSTONE 交换位置。这样做的好处是当每次查询或添加元素时,元素会被移动至距离理想位置(探测起始点)更近的桶,从而优化查询效率。

  • 为什么可以交换?

    • 在交换过程中,目标元素的键值并没有改变,只是移动到了一个更靠近探测起点(也就是理想的桶位,通过哈希函数计算得到的)的位置。
    • 线性探测会从理想位置开始逐一检查,因此移动后的目标元素仍然可以通过线性探测找到。
  • TOMBSTONE 的位置并不重要

    • TOMBSTONE 只是一个占位符,用来标记某个位置曾被使用过,但目前空闲。
    • 当我们用目标元素替换 TOMBSTONE 后,哈希表的结构仍然保持一致,不会影响查找。
  • 兼顾查询与插入效率

    • 查询:
      • 元素更靠近理想位置,查询时需要探测的步数减少。
    • 插入:
      • TOMBSTONE 被复用后,新元素可以更快找到合适位置,降低插入的复杂度。

下面实现了一个包含懒删除的开放寻址(线性探测)哈希表。为了更加充分地使用哈希表的空间,我们将哈希表看作一个“环形数组”,当越过数组尾部时,回到头部继续遍历。

class HashMapOpenAddressing:
	"""开放寻址哈希表"""	

	def __init__(self):
	    """构造方法"""
	    self.size = 0  # 键值对数量
	    self.capacity = 4  # 哈希表容量
	    self.load_thres = 2.0 / 3.0  # 触发扩容的负载因子阈值
	    self.extend_ratio = 2  # 扩容倍数
	    self.buckets: list[Pair | None] = [None] * self.capacity  # 桶数组
	    self.TOMBSTONE = Pair(-1, "-1")  # 删除标记
	
	def hash_func(self, key: int) -> int:
	    """哈希函数"""
	    return key % self.capacity
	
	def load_factor(self) -> float:
	    """负载因子"""
	    return self.size / self.capacity
	
	def find_bucket(self, key: int) -> int:
	    """搜索 key 对应的桶索引"""
	    index = self.hash_func(key)
	    first_tombstone = -1
	    # 线性探测,当遇到空桶时跳出
	    while self.buckets[index] is not None:
	        # 若遇到 key ,返回对应的桶索引
	        if self.buckets[index].key == key:
	            # 若之前遇到了删除标记,则将键值对移动至该索引处
	            if first_tombstone != -1:
	                self.buckets[first_tombstone] = self.buckets[index]
	                self.buckets[index] = self.TOMBSTONE
	                return first_tombstone  # 返回移动后的桶索引
	            return index  # 返回桶索引
	        # 记录遇到的首个删除标记
	        if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE:
	            first_tombstone = index
	        # 计算桶索引,越过尾部则返回头部
	        index = (index + 1) % self.capacity
	    # 若 key 不存在,则返回添加点的索引
	    return index if first_tombstone == -1 else first_tombstone
	
	def get(self, key: int) -> str:
	    """查询操作"""
	    # 搜索 key 对应的桶索引
	    index = self.find_bucket(key)
	    # 若找到键值对,则返回对应 val
	    if self.buckets[index] not in [None, self.TOMBSTONE]:
	        return self.buckets[index].val
	    # 若键值对不存在,则返回 None
	    return None
	
	def put(self, key: int, val: str):
	    """添加操作"""
	    # 当负载因子超过阈值时,执行扩容
	    if self.load_factor() > self.load_thres:
	        self.extend()
	    # 搜索 key 对应的桶索引
	    index = self.find_bucket(key)
	    # 若找到键值对,则覆盖 val 并返回
	    if self.buckets[index] not in [None, self.TOMBSTONE]:
	        self.buckets[index].val = val
	        return
	    # 若键值对不存在,则添加该键值对
	    self.buckets[index] = Pair(key, val)
	    self.size += 1
	
	def remove(self, key: int):
	    """删除操作"""
	    # 搜索 key 对应的桶索引
	    index = self.find_bucket(key)
	    # 若找到键值对,则用删除标记覆盖它
	    if self.buckets[index] not in [None, self.TOMBSTONE]:
	        self.buckets[index] = self.TOMBSTONE
	        self.size -= 1
	
	def extend(self):
	    """扩容哈希表"""
	    # 暂存原哈希表
	    buckets_tmp = self.buckets
	    # 初始化扩容后的新哈希表
	    self.capacity *= self.extend_ratio
	    self.buckets = [None] * self.capacity
	    self.size = 0
	    # 将键值对从原哈希表搬运至新哈希表
	    for pair in buckets_tmp:
	        if pair not in [None, self.TOMBSTONE]:
	            self.put(pair.key, pair.val)
	
	def print(self):
	    """打印哈希表"""
	    for pair in self.buckets:
	        if pair is None:
	            print("None")
	        elif pair is self.TOMBSTONE:
	            print("TOMBSTONE")
	        else:
	            print(pair.key, "->", pair.val)
/* 开放寻址哈希表 */
class HashMapOpenAddressing {
  private:
    int size;                             // 键值对数量
    int capacity = 4;                     // 哈希表容量
    const double loadThres = 2.0 / 3.0;     // 触发扩容的负载因子阈值
    const int extendRatio = 2;            // 扩容倍数
    vector<Pair *> buckets;               // 桶数组
    Pair *TOMBSTONE = new Pair(-1, "-1"); // 删除标记

  public:
    /* 构造方法 */
    HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {
    }

    /* 析构方法 */
    ~HashMapOpenAddressing() {
        for (Pair *pair : buckets) {
            if (pair != nullptr && pair != TOMBSTONE) {
                delete pair;
            }
        }
        delete TOMBSTONE;
    }

    /* 哈希函数 */
    int hashFunc(int key) {
        return key % capacity;
    }

    /* 负载因子 */
    double loadFactor() {
        return (double)size / capacity;
    }

    /* 搜索 key 对应的桶索引 */
    int findBucket(int key) {
        int index = hashFunc(key);
        int firstTombstone = -1;
        // 线性探测,当遇到空桶时跳出
        while (buckets[index] != nullptr) {
            // 若遇到 key ,返回对应的桶索引
            if (buckets[index]->key == key) {
                // 若之前遇到了删除标记,则将键值对移动至该索引处
                if (firstTombstone != -1) {
                    buckets[firstTombstone] = buckets[index];
                    buckets[index] = TOMBSTONE;
                    return firstTombstone; // 返回移动后的桶索引
                }
                return index; // 返回桶索引
            }
            // 记录遇到的首个删除标记
            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {
                firstTombstone = index;
            }
            // 计算桶索引,越过尾部则返回头部
            index = (index + 1) % capacity;
        }
        // 若 key 不存在,则返回添加点的索引
        return firstTombstone == -1 ? index : firstTombstone;
    }

    /* 查询操作 */
    string get(int key) {
        // 搜索 key 对应的桶索引
        int index = findBucket(key);
        // 若找到键值对,则返回对应 val
        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {
            return buckets[index]->val;
        }
        // 若键值对不存在,则返回空字符串
        return "";
    }

    /* 添加操作 */
    void put(int key, string val) {
        // 当负载因子超过阈值时,执行扩容
        if (loadFactor() > loadThres) {
            extend();
        }
        // 搜索 key 对应的桶索引
        int index = findBucket(key);
        // 若找到键值对,则覆盖 val 并返回
        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {
            buckets[index]->val = val;
            return;
        }
        // 若键值对不存在,则添加该键值对
        buckets[index] = new Pair(key, val);
        size++;
    }

    /* 删除操作 */
    void remove(int key) {
        // 搜索 key 对应的桶索引
        int index = findBucket(key);
        // 若找到键值对,则用删除标记覆盖它
        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {
            delete buckets[index];
            buckets[index] = TOMBSTONE;
            size--;
        }
    }

    /* 扩容哈希表 */
    void extend() {
        // 暂存原哈希表
        vector<Pair *> bucketsTmp = buckets;
        // 初始化扩容后的新哈希表
        capacity *= extendRatio;
        buckets = vector<Pair *>(capacity, nullptr);
        size = 0;
        // 将键值对从原哈希表搬运至新哈希表
        for (Pair *pair : bucketsTmp) {
            if (pair != nullptr && pair != TOMBSTONE) {
                put(pair->key, pair->val);
                delete pair;
            }
        }
    }

    /* 打印哈希表 */
    void print() {
        for (Pair *pair : buckets) {
            if (pair == nullptr) {
                cout << "nullptr" << endl;
            } else if (pair == TOMBSTONE) {
                cout << "TOMBSTONE" << endl;
            } else {
                cout << pair->key << " -> " << pair->val << endl;
            }
        }
    }
};

2.2.2. 平方探测

当发生冲突时,平方探测不是简单地跳过一个固定的步数,而是跳过“探测次数的平方”的步数,即 1,4,9,… 步。

优势:

  • 平方探测通过跳过探测次数平方的距离,试图缓解线性探测的聚集效应。
  • 平方探测会跳过更大的距离来寻找空位置,有助于数据分布得更加均匀。

缺点:

  • 仍然存在聚集现象,即某些位置比其他位置更容易被占用。
  • 由于平方的增长,平方探测可能不会探测整个哈希表,这意味着即使哈希表中有空桶,平方探测也可能无法访问到它。

2.2.3. 多次哈希

使用多个哈希函数对键值 key 进行哈希

改变:

  1. 插入操作:若第一次哈希出现冲突,则不断重复用新的哈希函数进行哈希,直至找到空桶插入元素。
  2. 查找操作:使用相同的哈希顺序进行查找。

优势:不会出现聚集现象

3. 哈希算法

开放寻址还是链式地址,它们只能保证哈希表可以在发生冲突时正常工作,而无法减少哈希冲突的发生

最理想的哈希表的分布方式如图(以链式地址举例)
image.png

而为了达到这一目标,降低哈希冲突发生概率,根据公式 index = hash(key) % capacity 可知,优化哈希函数才是最根本的操作。

3.1. 哈希算法的目标

哈希表数据结构需要又快又稳,则对于哈希算法:

特点:

  1. 确定性:相同(同一)输入,始终产生相同输出
  2. 效率高:计算哈希值时的速度快
  3. 均匀分布:在计算键值时,能够让其均匀分布

哈希算法应用领域:

  1. 密码存储
  2. 数据完整性检查

安全特性:

  1. 单向性:无法通过哈希值反推出关于输入数据的任何信息
  2. 抗碰撞性:极难找到两个不同的输入,使得它们的哈希值相同。(区别于均匀分布)
  3. 雪崩效应:输入的微小变化应当导致输出的显著且不可预测的变化。

3.2. 简单哈希算法设计

  • 加法哈希:对输入的每个字符的 ASCII 码进行相加,将得到的总和作为哈希值。
  • 乘法哈希:利用乘法的不相关性,每轮乘以一个常数,再将各个字符的 ASCII 码累积到哈希值中。
  • 异或哈希:将输入数据的每个元素通过异或操作累积到一个哈希值中。
  • 旋转哈希:将每个字符的 ASCII 码累积到一个哈希值中,每次累积之前都会对哈希值进行旋转操作。
def add_hash(key: str) -> int:
    """加法哈希"""
    hash = 0
    modulus = 1000000007
    for c in key:
        hash += ord(c)
    return hash % modulus

def mul_hash(key: str) -> int:
    """乘法哈希"""
    hash = 0
    modulus = 1000000007
    for c in key:
        hash = 31 * hash + ord(c)
    return hash % modulus

def xor_hash(key: str) -> int:
    """异或哈希"""
    hash = 0
    modulus = 1000000007
    for c in key:
        hash ^= ord(c)
    return hash % modulus

def rot_hash(key: str) -> int:
    """旋转哈希"""
    hash = 0
    modulus = 1000000007
    for c in key:
        hash = (hash << 4) ^ (hash >> 28) ^ ord(c)
    #1. `(hash << 4)`:将 `hash` 值左移 4 位,实际上是将其乘以 16。
	#2. `(hash >> 28)`:将 `hash` 值右移 28 位,实际上是将其除以 268,435,456。
	#3. `ord(c)`:将 `c` 字符转换为其 Unicode 码点,它是一个整数。
	#4. `(hash << 4) ^ (hash >> 28) ^ ord(c)`:使用按位异或运算符(^)将前三个操作的结果组合在一起。
    return hash % modulus
/* 加法哈希 */
int addHash(string key) {
    long long hash = 0;
    const int MODULUS = 1000000007;
    for (unsigned char c : key) {
        hash = (hash + (int)c) % MODULUS;
    }
    return (int)hash;
}

/* 乘法哈希 */
int mulHash(string key) {
    long long hash = 0;
    const int MODULUS = 1000000007;
    for (unsigned char c : key) {
        hash = (31 * hash + (int)c) % MODULUS;
    }
    return (int)hash;
}

/* 异或哈希 */
int xorHash(string key) {
    int hash = 0;
    const int MODULUS = 1000000007;
    for (unsigned char c : key) {
        hash ^= (int)c;
    }
    return hash & MODULUS;
}

/* 旋转哈希 */
int rotHash(string key) {
    long long hash = 0;
    const int MODULUS = 1000000007;
    for (unsigned char c : key) {
        hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;
    }
    return (int)hash;
}

由于使用大质数作为模数,可以最大化地保证哈希值的均匀分布。因为质数不与其他数字存在公约数,可以减少因取模操作而产生的周期性模式,从而避免哈希冲突。因此每种哈希算法的最后一步都是对大质数 1000000007 取模,以确保哈希值在合适的范围内均匀分布。

3.3. 常见哈希算法

哈希算法 推出时间 输出长度 哈希冲突 安全等级 应用 备注
MD5 1992 128 bit 较多 低,已被成功攻击 已被弃用,仍用于数据完整性检查
SHA-1 1995 160 bit 较多 低,已被成功攻击 已被弃用
SHA-2 2002 256/512 bit 很少 加密货币交易验证、数字签名等 SHA-256 是最安全的哈希算法之一,仍被常用到
SHA-3 2008 224/256/384/512 bit 很少 可用于替代 SHA-2 相较 SHA-2 的实现开销更低、计算效率更高

3.4. 数据结构的哈希值

哈希表的 key 可以是整数、小数或字符串等数据类型。编程语言通常会为这些数据类型提供内置的哈希算法,用于计算哈希表中的桶索引。

'''---------------------整数和布尔量哈希值为本身---------------------'''
num = 3
hash_num = hash(num)
# 整数 3 的哈希值为 3
bol = True
hash_bol = hash(bol)
# 布尔量 True 的哈希值为 1

'''--------------------------浮点数和字符串的哈希值计算较为复杂--------------------------'''
dec = 3.14159
hash_dec = hash(dec)
# 小数 3.14159 的哈希值为 326484311674566659
str = "Hello 算法"
hash_str = hash(str)
# 字符串“Hello 算法”的哈希值为 4617003410720528961

"""---------------元组的哈希值是对其中每一个元素进行哈希,然后将这些哈希值组合起来,得到单一的哈希值。----------------"""
tup = (12836, "小哈")
hash_tup = hash(tup)
# 元组 (12836, '小哈') 的哈希值为 1029005403108185979

"""----------------对象的哈希值基于其内存地址生成。通过重写对象的哈希方法,可实现基于内容生成哈希值。--------------------"""
obj = ListNode(0)
hash_obj = hash(obj)
# 节点对象 <ListNode object at 0x1058fd810> 的哈希值为 274267521
int num = 3;
size_t hashNum = hash<int>()(num);
// 整数 3 的哈希值为 3

bool bol = true;
size_t hashBol = hash<bool>()(bol);
// 布尔量 1 的哈希值为 1

double dec = 3.14159;
size_t hashDec = hash<double>()(dec);
// 小数 3.14159 的哈希值为 4614256650576692846

string str = "Hello 算法";
size_t hashStr = hash<string>()(str);
// 字符串“Hello 算法”的哈希值为 15466937326284535026

// 在 C++ 中,内置 std:hash() 仅提供基本数据类型的哈希值计算
// 数组、对象的哈希值计算需要自行实现

其他:

  1. 在许多编程语言中,只有不可变对象才可作为哈希表的 key 。
  2. 对象的哈希值通常是基于内存地址生成的,即使对象的内容发生了变化,但它的内存地址不变,哈希值仍然是不变的。
  3. Python 解释器在每次启动时,都会为字符串哈希函数加入一个随机的盐(salt)值。这种做法可以有效防止 HashDoS 攻击,提升哈希算法的安全性。

4. 小结

  1. 哈希表又称散列表,通过建立键 key 与值 value 之间的映射,实现高效的元素查询。
  2. 哈希表的目标是将一个较大的状态空间(非常多的输入)映射到一个较小的空间(数组长度),并提供 O(1) 的查询效率。
  3. 哈希表底层实现是数组、链表、二叉树,时间效率变高,但空间效率变低了。
  4. 哈希表操作包括查询、添加、删除键值对和遍历哈希表,前三者的的时间复杂度均为 O(1),遍历时间复杂度为 O(n)
  5. 哈希函数将 key 映射为数组索引,从而访问对应桶并获取 value 。
  6. 哈希表中通过 key 计算索引的公式为:index = hash(key) % capacity
  7. 不同的 key 经过计算后可能会得到相同的数组索引,进而导致查询出错,称为哈希冲突。哈希冲突比较严重时哈希表的时间复杂度会退化至 O(n)。
  8. 哈希表容量越大,哈希冲突的概率就越低。因此可以通过扩容哈希表来缓解哈希冲突。与数组扩容类似,哈希表扩容操作的开销很大。
  9. 负载因子定义为哈希表中元素数量除以桶数量,常用作触发哈希表扩容的条件。
  10. 链式地址通过将单个元素转化为链表,将所有冲突元素存储在同一个链表中。然而,链表过长会降低查询效率,可以通过进一步将链表转换为红黑树来提高效率。
  11. 哈希表开放寻址法无法直接进行删除操作
  12. 开放寻址通过多次探测来处理哈希冲突。线性探测和平方使用固定步长,缺点是不能删除元素,且容易产生聚集。多次哈希使用多个哈希函数进行探测,相较线性探测更不易产生聚集,但多个哈希函数增加了计算量。
  13. 哈希函数(算法)是解决哈希冲突的最好的办法,设计具有确定性、高效率和均匀分布的特点非常重要。同时在密码学中,哈希算法还应该具备单向性、抗碰撞性和雪崩效应。
  14. 常见的哈希算法包括 MD5、SHA-1、SHA-2 和 SHA-3 等。广泛使用的是 SHA-2 中的 SHA-256
  15. 哈希算法通常采用大质数作为模数,以最大化地保证哈希值均匀分布,减少哈希冲突。
  16. 通常情况下,只有不可变对象是可哈希的。链表除外。
  17. Q&A.

标签:index,哈希,buckets,key,pair,self
From: https://www.cnblogs.com/zyh778/p/18651091

相关文章

  • 算法解析-经典150(矩阵、哈希表)
    文章目录矩阵1.有效的数独1.答案2.思路2.螺旋矩阵1.答案2.思路3.旋转图像1.答案2.思路4.矩阵置零1.答案2.思路哈希表1.赎金信1.答案2.思路2.同构字符串1.答案2.思路3.单词规律1.答案2.思路4.有效的字母异位词1.答案2.思路5.字母异位词分组1.答案2.思路......
  • 哈希算法篇——散落的秘密与精准的归宿,混沌中的秩序之美(上)
    文章目录引言:混沌中的秩序之美第一章:哈希的本质——化繁为简的魔法第二章:经典哈希函数——一座算法的博物馆第三章:哈希表的奇迹——从无序到有序的转变3.1哈希函数的基本实现3.2基本的哈希表实现3.3哈希算法的实际应用小结引言:混沌中的秩序之美在信息科学的星......
  • Luogu P9646 SNCPC2019 Paper-cutting 题解 [ 紫 ] [ manacher ] [ 贪心 ] [ 哈希 ] [
    Paper-cutting:思维很好,但代码很构式的manacher题。蒟蒻2025年切的第一道题,是个紫,并且基本独立想出的,特此纪念。判断能否折叠我们先考虑一部分能折叠需要满足什么条件。显然,这一部分需要是一个长度为偶数的回文串。那么横向和纵向会不会影响呢?答案是不会,因为横向折了之后,折......
  • 中南大学(CSU)世界杯球赛(10分)—— 哈希表的使用
        孩子们,我回来了。最近OJ的作业比较多,一不小心写昏过去了。在写世界杯球赛这一题时经常因为程序较复杂,运行时间太久而导致时间超限。为此,我痛定思痛,在查阅资料后找到了一种高效、灵活的数据结构——哈希表。现在,我将以这一道题为例来分享哈希表的简单使用。(提示:本......
  • Leecode热题100——1.哈希
    1.两数之和给定一个整数数组nums和一个整数目标值target,请你在该数组中找出和为目标值target的那两个整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。你可以按任意顺序返回答案。示例1:输入:nums=[2,7,11,15],target......
  • python中的可不可变,可不可哈希,可不可修改
    在Python中,可不可变性、可不可哈希性、以及是否支持修改是理解数据类型行为的关键。以下是对这些特性的详细分析,包括定义、例子和它们之间的联系。1.可变性vs不可变性定义可变性:可变类型的数据可以直接修改,而不会改变其引用(内存地址)。不可变性:不可变类型的数据在创建后......
  • 常见加密方式:对称加密,非对称加密和哈希算法
    前言长度位数,字符规律数据加密是一种保护数据安全的技术,通过将数据明文转换为不易被未授权的人理解的形式密文,防止数据泄露、串改和滥用。对称加密加密和解密使用同一密钥,加解密速度快,适合加密大量数据。但密钥需要安全地存储和传输,否则容易窃取,破坏数据地保密性。DES明......
  • [算法/数据结构]系列 华为面试原题:和为n的子串(前缀和+哈希表)
    [算法/数据结构]系列华为面试原题:和为n的子串(前缀和+哈希表)文章目录[算法/数据结构]系列华为面试原题:和为n的子串(前缀和+哈希表)面试原题样例分析代码及思路面试原题输入一串只有0和1的数组,返回输入和为n的子串的个数。样例:输入:[011100],n=3输出:6样......
  • 二分(离散化/哈希)
    题目:链接:https://ac.nowcoder.com/acm/problem/207053题意:简单来说就是每次猜值,根据反馈判断答案所在的区间,找区间重叠次数最多的那部分的重叠次数思路:若猜中,区间[num,num]次数+1若猜大了,区间[-inf,num-1]次数+1若猜小了,区间[num+1,inf]次数+1在[-inf,inf]上使用......
  • 通过在 组策略管理控制台 中配置 AppLocker,可以非常有效地限制 PowerShell 脚本的执行
    在组策略管理控制台(GroupPolicyManagementConsole,GPMC)中配置AppLocker,可以有效地限制和控制哪些应用程序(包括PowerShell脚本)可以在计算机上执行。这是一种通过白名单策略确保只有已批准的应用程序能够运行的强大安全措施。配置AppLocker的步骤:1. 打开组策略管理控制......