最大频率栈

标签: 设计 哈希表 有序集合

难度: Hard

设计一个类似堆栈的数据结构,将元素推入堆栈,并从堆栈中弹出出现频率最高的元素。

实现 FreqStack 类:

  • FreqStack() 构造一个空的堆栈。
  • void push(int val) 将一个整数 val 压入栈顶。
  • int pop() 删除并返回堆栈中出现频率最高的元素。
    • 如果出现频率最高的元素不只一个,则移除并返回最接近栈顶的元素。

示例 1:

输入:
["FreqStack","push","push","push","push","push","push","pop","pop","pop","pop"],
[[],[5],[7],[5],[7],[4],[5],[],[],[],[]]
输出:[null,null,null,null,null,null,null,5,7,5,4]
解释:
FreqStack = new FreqStack();
freqStack.push (5);//堆栈为 [5]
freqStack.push (7);//堆栈是 [5,7]
freqStack.push (5);//堆栈是 [5,7,5]
freqStack.push (7);//堆栈是 [5,7,5,7]
freqStack.push (4);//堆栈是 [5,7,5,7,4]
freqStack.push (5);//堆栈是 [5,7,5,7,4,5]
freqStack.pop ();//返回 5 ,因为 5 出现频率最高。堆栈变成 [5,7,5,7,4]。
freqStack.pop ();//返回 7 ,因为 5 和 7 出现频率最高,但7最接近顶部。堆栈变成 [5,7,5,4]。
freqStack.pop ();//返回 5 ,因为 5 出现频率最高。堆栈变成 [5,7,4]。
freqStack.pop ();//返回 4 ,因为 4, 5 和 7 出现频率最高,但 4 是最接近顶部的。堆栈变成 [5,7]。

提示:

  • 0 <= val <= 109
  • push 和 pop 的操作数不大于 2 * 104
  • 输入保证在调用 pop 之前堆栈中至少有一个元素。

Submission

运行时间: 184 ms

内存: 24.8 MB

class FreqStack:

    def __init__(self):
        self.elems = []
        self.freq = defaultdict(int)

    def push(self, val: int) -> None:
        fr = self.freq[val]
        self.freq[val] += 1
        if len(self.elems) <= fr:
            self.elems.append([])
        self.elems[fr].append(val)

    def pop(self) -> int:
        ans = self.elems[-1][-1]
        self.elems[-1].pop()
        if not self.elems[-1]: self.elems.pop()
        self.freq[ans] -= 1
        return ans


# Your FreqStack object will be instantiated and called as such:
# obj = FreqStack()
# obj.push(val)
# param_2 = obj.pop()

Explain

这个题解的思路是使用两个主要的数据结构:一个字典用来存储每个元素的出现频率,一个列表用来存储每个频率对应的元素集合。每次元素入栈时,更新该元素的频率,并将元素添加到对应频率的集合中。出栈时,从最高频率的集合中弹出元素,如果该集合为空,则移除该集合。

时间复杂度: O(1)

空间复杂度: O(n)

class FreqStack:

    def __init__(self):
        self.elems = []  # 存储每个频率对应的元素集合
        self.freq = defaultdict(int)  # 存储每个元素的出现频率

    def push(self, val: int) -> None:
        fr = self.freq[val]  # 获取当前元素的频率
        self.freq[val] += 1  # 更新元素频率
        if len(self.elems) <= fr:
            self.elems.append([])  # 如果没有对应频率的集合,则创建新的集合
        self.elems[fr].append(val)  # 将元素添加到对应频率的集合中

    def pop(self) -> int:
        ans = self.elems[-1][-1]  # 获取最高频率集合的最后一个元素
        self.elems[-1].pop()  # 从集合中移除该元素
        if not self.elems[-1]: self.elems.pop()  # 如果集合为空,则移除该集合
        self.freq[ans] -= 1  # 更新元素频率
        return ans  # 返回被移除的元素

# Your FreqStack object will be instantiated and called as such:
# obj = FreqStack()
# obj.push(val)
# param_2 = obj.pop()

Explore

在FreqStack实现中,元素栈`elems`是一个列表,其中每个索引位置存储一个列表,表示该频率下所有元素的集合。由于每次元素入栈时,都将其添加到其当前频率所对应的列表末尾,这保证了相同频率的元素按照入栈顺序存储。因此,当执行pop操作时,从`elems`中最后一个列表(即最高频率)的末尾弹出元素,这确保了返回的是最近入栈且出现频率最高的元素。

是的,使用字典`freq`来存储每个元素的出现频率是一个高效且准确的方法。字典提供常数时间复杂度的查找、插入和更新操作,这使得它非常适合频繁更新的场景。每次push操作都会增加元素的频率计数,而每次pop操作都会减少相应元素的频率计数。这种方法即便在频繁的push和pop操作后也能准确追踪每个元素的频率变化。

选择使用列表来存储每个频率对应的元素集合主要是因为需要频繁地进行末尾元素的插入和移除操作,并且需要按顺序存储同一频率的元素以保持其入栈顺序。列表在这些操作中表现良好,尤其是在末尾添加或移除元素时具有常数时间复杂度。而使用堆或者平衡树虽然可以优化某些操作,但在维护元素的入栈顺序方面可能会更加复杂且效率较低。

在pop操作中,如果发现最高频率的集合为空,这意味着该频率下的所有元素都已被移除。在这种情况下,我会从`elems`列表中移除这个空的集合,确保列表`elems`的最后一个元素始终是非空的,代表当前的最高频率的元素集合。这样处理保证了每次pop操作都能正确地返回最高频率且最近入栈的元素,并且维护了数据结构的一致性和正确性。