引言 参照的是左程云的课程:https://space.bilibili.com/8888480/lists/3509640?type=series
本笔记是class035的内容,总结了数据结构设计类题目的高频考点,包含7道经典数据结构设计题目的详细解析。这些题目主要基于哈希表的O(1)操作时间复杂度特性。
前置知识 在学习数据结构设计高频题之前,需要掌握以下基础知识:
动态数组和扩容分析(007讲)
链表入门内容(009~012讲)
堆结构(025讲)
哈希表、有序表、比较器的使用(026讲)
重要说明 本节以数据结构设计高频题为主,并不涉及太难的数据结构设计题目,很多题的原理都是基于哈希表的O(1)时间复杂度。数据结构设计的更难题目,需要学习更多数据结构之后才能解决,如前缀树、并查集、线段树等。
035【必备】数据结构设计高频题 题目一:setAll功能的哈希表 问题描述 哈希表常见的三个操作是put、get和containsKey,而且这三个操作的时间复杂度为O(1)。现在想加一个setAll功能,就是把所有记录value都设成统一的值。请设计并实现这种有setAll功能的哈希表,并且put、get、containsKey和setAll四个操作的时间复杂度都为O(1)。
测试链接 :https://www.nowcoder.com/practice/7c4559f138e74ceb9ba57d76fd169967
核心思想 采用”懒更新”(Lazy Update)策略,setAll操作只记录一个全局值和当前时间戳,并不实际修改数据。当get一个键时,通过比较该键自身的时间戳和全局setAll的时间戳,来实时决定返回它自己的值还是全局的值。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class SetAllHashMap : def __init__ (self ): self .map = {} self .set_all_value = 0 self .set_all_time = -1 self .cnt = 0 def put (self, k, v ): if k in self .map : value = self .map [k] value[0 ] = v value[1 ] = self .cnt self .cnt += 1 else : self .map [k] = [v, self .cnt] self .cnt += 1 def set_all (self, v ): self .set_all_value = v self .set_all_time = self .cnt self .cnt += 1 def get (self, k ): if k not in self .map : return -1 value = self .map [k] if value[1 ] > self .set_all_time: return value[0 ] else : return self .set_all_value
高效读写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 def main (): solution = SetAllHashMap() lines = sys.stdin.readlines() i = 0 while i < len (lines): line = lines[i].strip() if not line: i += 1 continue n = int (line) solution.__init__() for j in range (n): i += 1 parts = list (map (int , lines[i].strip().split())) op = parts[0 ] if op == 1 : a, b = parts[1 ], parts[2 ] solution.put(a, b) elif op == 2 : a = parts[1 ] print (solution.get(a)) else : a = parts[1 ] solution.set_all(a) i += 1 if __name__ == "__main__" : main()
算法分析
时间复杂度 :所有操作都是O(1)
空间复杂度 :O(N),N为键的数量
核心技巧 :时间戳比较 + 懒更新策略
题目二:实现LRU结构 问题描述 实现 LRUCache 类:
LRUCache(int capacity) 以正整数作为容量capacity初始化LRU缓存
int get(int key) 如果关键字key存在于缓存中,则返回关键字的值,否则返回-1
void put(int key, int value) 如果关键字key已经存在,则变更其数据值value;如果不存在,则向缓存中插入该组key-value。如果插入操作导致关键字数量超过capacity,则应该逐出最久未使用的关键字
函数get和put必须以O(1)的平均时间复杂度运行。
测试链接 :https://leetcode.cn/problems/lru-cache/
核心思想 结合了哈希表和双向链表:哈希表提供了对任意键的O(1)快速访问,而双向链表则负责维护数据的访问顺序,使其能在O(1)时间内将最新访问的节点移到队尾,并在容量满时淘汰队首的最久未使用节点。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 class LRUCache : class DoubleNode : def __init__ (self, key=0 , val=0 ): self .key = key self .val = val self .last = None self .next = None class DoubleList : def __init__ (self ): self .head = self .DoubleNode() self .tail = self .DoubleNode() self .head.next = self .tail self .tail.last = self .head def add_node_to_tail (self, node ): node.next = self .tail node.last = self .tail.last self .tail.last.next = node self .tail.last = node def move_node_to_tail (self, node ): node.last.next = node.next node.next .last = node.last self .add_node_to_tail(node) def remove_head (self ): if self .head.next == self .tail: return None node_to_remove = self .head.next node_to_remove.last.next = node_to_remove.next node_to_remove.next .last = node_to_remove.last return node_to_remove def __init__ (self, capacity: int ): self .key_node_map = {} self .node_list = self .DoubleList() self .capacity = capacity def get (self, key: int ) -> int : if key in self .key_node_map: node = self .key_node_map[key] self .node_list.move_node_to_tail(node) return node.val return -1 def put (self, key: int , value: int ) -> None : if key in self .key_node_map: node = self .key_node_map[key] node.val = value self .node_list.move_node_to_tail(node) else : if len (self .key_node_map) == self .capacity: removed_node = self .node_list.remove_head() del self .key_node_map[removed_node.key] new_node = self .DoubleNode(key, value) self .key_node_map[key] = new_node self .node_list.add_node_to_tail(new_node)
算法分析
时间复杂度 :get和put操作都是O(1)
空间复杂度 :O(capacity)
核心技巧 :哈希表 + 双向链表
题目三:插入、删除和获取随机元素O(1)时间的结构 问题描述 设计一个支持在平均时间复杂度O(1)下,执行以下操作的数据结构:
insert(val):当元素val不存在时,向集合中插入该项
remove(val):元素val存在时,从集合中移除该项
getRandom():随机返回现有集合中的一项
测试链接 :https://leetcode.cn/problems/insert-delete-getrandom-o1/
核心思想 结合了动态数组与哈希表,数组负责存储元素以实现O(1)的随机获取,哈希表则存储”值到数组索引”的映射以实现O(1)的查找。其remove操作的精髓在于:将待删除元素与数组的最后一个元素交换,然后直接删除数组末尾。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import randomclass RandomizedSet : def __init__ (self ): self .map = {} self .arr = [] def insert (self, val: int ) -> bool : if val in self .map : return False self .arr.append(val) self .map [val] = len (self .arr) - 1 return True def remove (self, val: int ) -> bool : if val not in self .map : return False val_index = self .map [val] end_value = self .arr[-1 ] self .arr[val_index] = end_value self .map [end_value] = val_index del self .map [val] self .arr.pop() return True def getRandom (self ) -> int : return random.choice(self .arr)
算法分析
时间复杂度 :所有操作都是O(1)
空间复杂度 :O(N)
核心技巧 :数组末尾交换删除
题目四:插入、删除和获取随机元素O(1)时间且允许重复数字的结构 问题描述 设计一个支持在平均时间复杂度O(1)下,执行以下操作的数据结构,且允许有重复数字:
insert(val):向集合中插入元素val
remove(val):从集合中移除元素val的一个实例
getRandom():随机返回现有集合中的一项
测试链接 :https://leetcode.cn/problems/insert-delete-getrandom-o1-duplicates-allowed/
核心思想 与不允许重复的版本类似,但哈希表需要存储一个值所有出现位置的索引集合。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import randomfrom collections import defaultdictclass RandomizedCollection : def __init__ (self ): self .map = defaultdict(set ) self .arr = [] def insert (self, val: int ) -> bool : is_new = val not in self .map self .arr.append(val) self .map [val].add(len (self .arr) - 1 ) return is_new def remove (self, val: int ) -> bool : if val not in self .map : return False val_index = self .map [val].pop() end_value = self .arr[-1 ] end_index = len (self .arr) - 1 if val_index != end_index: self .arr[val_index] = end_value self .map [end_value].remove(end_index) self .map [end_value].add(val_index) self .arr.pop() if not self .map [val]: del self .map [val] return True def getRandom (self ) -> int : return random.choice(self .arr)
算法分析
时间复杂度 :所有操作都是O(1)
空间复杂度 :O(N)
核心技巧 :索引集合管理
题目五:快速获得数据流的中位数的结构 问题描述 中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。
例如,[2,3,4]的中位数是3,[2,3]的中位数是(2 + 3) / 2 = 2.5
设计一个支持以下两个操作的数据结构:
void addNum(int num) - 从数据流中添加一个整数到数据结构中
double findMedian() - 返回目前所有元素的中位数
测试链接 :https://leetcode.cn/problems/find-median-from-data-stream/
核心思想 使用两个堆来维护数据流,一个大顶堆和一个小顶堆。大顶堆存储数据流中较小的一半数字,小顶堆存储数据流中较大的一半数字。这样,中位数总是可以通过两个堆的堆顶元素快速得到。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import heapqclass MedianFinder : def __init__ (self ): self .max_heap = [] self .min_heap = [] def addNum (self, num: int ) -> None : if not self .max_heap or -self .max_heap[0 ] >= num: heapq.heappush(self .max_heap, -num) else : heapq.heappush(self .min_heap, num) self ._balance() def findMedian (self ) -> float : if len (self .max_heap) == len (self .min_heap): return (-self .max_heap[0 ] + self .min_heap[0 ]) / 2.0 else : return -self .max_heap[0 ] if len (self .max_heap) > len (self .min_heap) else self .min_heap[0 ] def _balance (self ) -> None : if abs (len (self .max_heap) - len (self .min_heap)) == 2 : if len (self .max_heap) > len (self .min_heap): heapq.heappush(self .min_heap, -heapq.heappop(self .max_heap)) else : heapq.heappush(self .max_heap, -heapq.heappop(self .min_heap))
算法分析
时间复杂度 :addNum为O(logN),findMedian为O(1)
空间复杂度 :O(N)
核心技巧 :双堆平衡
题目六:最大频率栈 问题描述 设计一个类似堆栈的数据结构,将元素推入堆栈,并从堆栈中弹出出现频率最高的元素。
实现 FreqStack 类:
FreqStack() 构造一个空的堆栈
void push(int val) 将一个整数val压入栈顶
int pop() 删除并返回堆栈中出现频率最高的元素
测试链接 :https://leetcode.cn/problems/maximum-frequency-stack/
核心思想 使用一个哈希表记录每个值出现的频率,使用另一个哈希表将频率映射到一个栈,这个栈存储了所有出现该频率的数字。使用一个变量实时追踪当前的最大频率。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 from collections import defaultdictclass FreqStack : def __init__ (self ): self .top_times = 0 self .cnt_values = defaultdict(list ) self .value_times = defaultdict(int ) def push (self, val: int ) -> None : self .value_times[val] += 1 current_freq = self .value_times[val] self .cnt_values[current_freq].append(val) self .top_times = max (self .top_times, current_freq) def pop (self ) -> int : ans = self .cnt_values[self .top_times].pop() if not self .cnt_values[self .top_times]: self .top_times -= 1 self .value_times[ans] -= 1 return ans
算法分析
时间复杂度 :push和pop操作都是O(1)
空间复杂度 :O(N)
核心技巧 :频率分层存储
题目七:全O(1)的数据结构 问题描述 请你实现一个数据结构支持以下操作:
Inc(key) - 插入一个新的值为1的key,或者使一个存在的key增加一,保证key不为空字符串
Dec(key) - 如果这个key的值是1,那么把他从数据结构中移除掉。否则使一个存在的key值减一。如果这个key不存在,这个函数不做任何事情。key保证不为空字符串
GetMaxKey() - 返回key中值最大的任意一个。如果没有元素存在,返回一个空字符串””
GetMinKey() - 返回key中值最小的任意一个。如果没有元素存在,返回一个空字符串””
挑战:以O(1)的时间复杂度实现所有操作。
测试链接 :https://leetcode.cn/problems/all-oone-data-structure/
核心思想 使用一个双向链表来组织桶(Bucket),链表按桶的计数值(cnt)升序排列。每个桶包含一个集合,存储所有计数值等于该桶cnt的key。使用一个哈希表来存储key -> Bucket的映射,实现O(1)的key定位。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 class AllOne : class Bucket : def __init__ (self, cnt ): self .cnt = cnt self .keys = set () self .last = None self .next = None def __init__ (self ): self .head = self .Bucket(0 ) self .tail = self .Bucket(float ('inf' )) self .head.next = self .tail self .tail.last = self .head self .map = {} def _insert_after (self, prev_bucket, new_bucket ): new_bucket.next = prev_bucket.next new_bucket.last = prev_bucket prev_bucket.next .last = new_bucket prev_bucket.next = new_bucket def _remove_bucket (self, bucket ): bucket.last.next = bucket.next bucket.next .last = bucket.last def inc (self, key: str ) -> None : if key not in self .map : target_bucket = self .head.next if target_bucket.cnt != 1 : target_bucket = self .Bucket(1 ) self ._insert_after(self .head, target_bucket) target_bucket.keys.add(key) self .map [key] = target_bucket else : current_bucket = self .map [key] new_cnt = current_bucket.cnt + 1 target_bucket = current_bucket.next if target_bucket.cnt != new_cnt: target_bucket = self .Bucket(new_cnt) self ._insert_after(current_bucket, target_bucket) target_bucket.keys.add(key) self .map [key] = target_bucket current_bucket.keys.remove(key) if not current_bucket.keys: self ._remove_bucket(current_bucket) def dec (self, key: str ) -> None : if key not in self .map : return current_bucket = self .map [key] current_bucket.keys.remove(key) if current_bucket.cnt > 1 : new_cnt = current_bucket.cnt - 1 target_bucket = current_bucket.last if target_bucket.cnt != new_cnt: target_bucket = self .Bucket(new_cnt) self ._insert_after(current_bucket.last, target_bucket) target_bucket.keys.add(key) self .map [key] = target_bucket else : del self .map [key] if not current_bucket.keys: self ._remove_bucket(current_bucket) def getMaxKey (self ) -> str : if self .tail.last == self .head: return "" return next (iter (self .tail.last.keys)) def getMinKey (self ) -> str : if self .head.next == self .tail: return "" return next (iter (self .head.next .keys))
算法分析
时间复杂度 :所有操作都是O(1)
空间复杂度 :O(N)
核心技巧 :双向链表 + 桶分组
数据结构设计核心技巧总结 1. 哈希表的O(1)特性 大部分设计题都基于哈希表的O(1)查找、插入、删除特性:
1 2 3 4 5 hash_map = {} hash_map[key] = value val = hash_map[key] del hash_map[key]
2. 时间戳技巧 用于实现懒更新策略:
1 2 3 4 5 if operation_time > global_time: return local_value else : return global_value
3. 双向链表维护顺序 适用于需要频繁移动元素位置的场景:
1 2 3 4 5 6 7 8 9 10 def move_to_tail (node ): node.prev.next = node.next node.next .prev = node.prev node.next = tail node.prev = tail.prev tail.prev.next = node tail.prev = node
4. 数组末尾交换删除 实现O(1)删除的经典技巧:
1 2 3 4 arr[del_index] = arr[-1 ] map [arr[-1 ]] = del_index arr.pop()
5. 双堆维护极值 适用于动态维护中位数或其他统计量:
1 2 3 4 if abs (len (max_heap) - len (min_heap)) > 1 : balance_heaps()
复杂度分析总结
题目
时间复杂度
空间复杂度
核心数据结构
setAll哈希表
O(1)
O(N)
哈希表+时间戳
LRU缓存
O(1)
O(capacity)
哈希表+双向链表
随机数据结构
O(1)
O(N)
哈希表+动态数组
随机数据结构(重复)
O(1)
O(N)
哈希表+集合+数组
中位数查找
O(logN)/O(1)
O(N)
双堆
最大频率栈
O(1)
O(N)
哈希表+栈数组
全O(1)数据结构
O(1)
O(N)
哈希表+双向链表+桶
学习建议
理解O(1)的本质 :大多数设计题的核心是利用哈希表的O(1)特性
掌握组合数据结构 :
哈希表 + 双向链表(LRU)
哈希表 + 动态数组(随机访问)
双堆(维护极值)
时间戳(懒更新)
注意边界条件 :
理解权衡取舍 :
时间复杂度 vs 空间复杂度
实现复杂度 vs 运行效率
多练习组合技巧 :数据结构设计题往往需要组合多种基础数据结构
数据结构设计题考察的是对基础数据结构的深入理解和灵活运用能力。通过掌握这些经典模式和技巧,可以应对大多数设计类问题。