设计一个支持动态顺序统计的哈希结构
题目描述
设计一个数据结构,它需要支持以下四种操作,并且每个操作的平均时间复杂度应尽可能接近 O(1):
insert(key):插入一个元素。delete(key):删除一个元素。search(key):检查一个元素是否存在。getRank(key):返回给定元素在排序顺序中的排名(即,比它小的元素数量 + 1)。getKeyByRank(k):返回排名为 k 的元素。
题目要求结合哈希表与平衡二叉搜索树的特性,以在接近常数时间内同时支持快速的查找、插入、删除以及基于排名的顺序查询。
解题过程
1. 问题分析
- 如果只需要前三个操作,一个标准的哈希表就能在平均 O(1) 时间内完成。
- 但哈希表内部是无序的,无法直接支持
getRank和getKeyByRank,因为这两个操作需要知道元素的排序顺序。 - 如果只用一棵平衡二叉搜索树(如 AVL 树、红黑树),我们可以通过树的中序遍历来得到排序顺序,进而实现排名查询。但计算排名通常需要遍历子树节点,时间复杂度为 O(log n),虽然不错,但题目希望我们追求更优的平均性能,并尽可能向 O(1) 靠近。
2. 设计思路
我们可以结合两种数据结构的优势:
- 哈希表:实现 O(1) 的
search、insert、delete(平均)。 - 平衡二叉搜索树:维护元素的有序结构,以支持排名查询。
但关键在于,我们需要在树中快速计算任意节点的排名。如果在每个节点中额外记录以其为根的子树的大小,那么就可以在 O(log n) 时间内计算出该节点的排名:
- 节点排名 = 左子树节点数 + 1(如果查找的键等于当前节点) + 路径上所有比当前节点小的祖先节点的左子树大小之和。
为了进一步优化,我们可以将哈希表与树状数组或名次树结合。但更经典且直接的设计是采用Treap 或 红黑树,并在节点中存储子树大小。
3. 数据结构设计
我们采用红黑树(或任何平衡 BST)的变种,其中每个节点包含:
key:节点的键值。size:以该节点为根的子树中节点的总数(包括自身)。- 左右子节点指针。
同时,我们维护一个哈希表,其键为 key,值为指向树中对应节点的指针。这样,我们可以通过哈希表在 O(1) 时间内定位到树中的节点。
4. 操作实现步骤
(1) 辅助函数:更新子树大小
在树的插入、删除、旋转操作后,需要更新相关节点的 size 属性。
规则:node.size = node.left.size + node.right.size + 1。
(约定空节点的 size 为 0)
(2) 插入操作
- 检查哈希表:如果
key已存在,则不需要重复插入。 - 在红黑树中插入新节点。插入过程按照标准二叉搜索树插入,之后通过旋转和变色维持红黑树平衡。每次经过的节点路径上,更新节点的
size值(因为子树增加了一个节点)。 - 将
key和指向新树节点的指针存入哈希表。
- 哈希表插入:O(1) 平均。
- 红黑树插入:O(log n) 最坏情况。
- 总平均时间复杂度接近 O(log n)(由于哈希表的存在,查找是否已存在是 O(1),但树插入本身是 O(log n))。
(3) 删除操作
- 通过哈希表检查
key是否存在,若不存在则直接返回。 - 在红黑树中找到该节点(借助哈希表指针直接访问,无需搜索)。
- 执行标准红黑树删除操作,并在删除过程中更新路径上相关节点的
size值。 - 从哈希表中删除该
key。
- 哈希表删除:O(1) 平均。
- 红黑树删除:O(log n) 最坏。
- 总平均时间复杂度 O(log n)。
(4) 搜索操作
直接通过哈希表查找 key 是否存在,O(1) 平均时间复杂度。
(5) 获取排名 getRank(key)
- 通过哈希表获取该
key对应的树节点指针。如果不存在,返回错误或 0。 - 在树中,从根节点开始向下搜索到该节点,沿途计算排名:
- 初始化排名 = 0。
- 设当前节点为
curr,从根开始。 - 如果
key等于curr.key,则排名增加curr.left.size + 1,结束。 - 如果
key<curr.key,则进入左子树继续。 - 如果
key>curr.key,则排名增加curr.left.size + 1,然后进入右子树继续。
- 最终得到的排名即为结果。
- 由于我们通过哈希表直接获得了节点指针,但为了计算排名,我们仍需从根遍历到该节点以累计左子树大小。这个过程是 O(log n)。
(6) 根据排名获取键 getKeyByRank(k)
- 如果 k < 1 或 k > 树的总节点数,返回错误。
- 从根节点开始:
- 设左子树大小
leftSize = curr.left.size。 - 如果 k == leftSize + 1,则当前节点键值就是答案。
- 如果 k <= leftSize,进入左子树继续查找。
- 否则,
k = k - (leftSize + 1),进入右子树继续查找。
- 设左子树大小
- 返回找到的节点的键。
- 时间复杂度 O(log n)。
5. 复杂度分析
- 搜索:O(1) 平均(哈希表)。
- 插入:O(log n) 最坏(红黑树调整)。
- 删除:O(log n) 最坏。
- 获取排名:O(log n)。
- 根据排名获取键:O(log n)。
6. 边界情况与注意事项
- 当树为空时,
getKeyByRank应返回错误。 - 重复插入相同键应被忽略(或视为更新操作,但题目未要求更新值,所以忽略即可)。
- 在删除节点时,注意更新哈希表与树的同步,避免野指针。
- 在旋转操作(红黑树再平衡)时,需正确重新计算受影响节点的
size值。
总结
本题通过结合哈希表与平衡二叉搜索树,构建了一个既能快速查找、插入、删除,又能支持基于排名的顺序查询的数据结构。哈希表负责提供 O(1) 的查找能力,而带子树大小记录的红黑树则维护了全序关系,并能在对数时间内响应排名查询。这是一种典型的空间换时间策略,通过额外的指针关联和子树大小维护,实现了动态顺序统计的高效操作。