全 O(1) 的数据结构

标签: 设计 哈希表 链表 双向链表

难度: Hard

请你设计一个用于存储字符串计数的数据结构,并能够返回计数最小和最大的字符串。

实现 AllOne 类:

  • AllOne() 初始化数据结构的对象。
  • inc(String key) 字符串 key 的计数增加 1 。如果数据结构中尚不存在 key ,那么插入计数为 1key
  • dec(String key) 字符串 key 的计数减少 1 。如果 key 的计数在减少后为 0 ,那么需要将这个 key 从数据结构中删除。测试用例保证:在减少计数前,key 存在于数据结构中。
  • getMaxKey() 返回任意一个计数最大的字符串。如果没有元素存在,返回一个空字符串 ""
  • getMinKey() 返回任意一个计数最小的字符串。如果没有元素存在,返回一个空字符串 ""

注意:每个函数都应当满足 O(1) 平均时间复杂度。

示例:

输入
["AllOne", "inc", "inc", "getMaxKey", "getMinKey", "inc", "getMaxKey", "getMinKey"]
[[], ["hello"], ["hello"], [], [], ["leet"], [], []]
输出
[null, null, null, "hello", "hello", null, "hello", "leet"]

解释
AllOne allOne = new AllOne();
allOne.inc("hello");
allOne.inc("hello");
allOne.getMaxKey(); // 返回 "hello"
allOne.getMinKey(); // 返回 "hello"
allOne.inc("leet");
allOne.getMaxKey(); // 返回 "hello"
allOne.getMinKey(); // 返回 "leet"

提示:

  • 1 <= key.length <= 10
  • key 由小写英文字母组成
  • 测试用例保证:在每次调用 dec 时,数据结构中总存在 key
  • 最多调用 incdecgetMaxKeygetMinKey 方法 5 * 104

Submission

运行时间: 96 ms

内存: 0.0 MB

class DListNode:

    def __init__(self, val):
        self.val = val
        self.keys = set()
        
        self.next = None
        self.prev = None
    
    def __repr__(self):
        keys = list(self.keys)
        
        return "'" + str(self.val) + " " + ",".join(keys) + "'"


class AllOne:

    def __init__(self):
        """
        Initialize your data structure here.
        """
        self._table = dict()
        
        self._head = DListNode(0)
        self._tail = DListNode(0)
        self._head.next = self._tail
        self._tail.prev = self._head
    
    def _first_node(self):
        return self._head.next
    
    def _last_node(self):
        return self._tail.prev
    
    def _insert_front(self, node):
        node.next = self._head.next
        node.prev = self._head
        
        self._head.next.prev = node
        self._head.next = node
    
    def _insert_last(self, node):
        node.next = self._tail
        node.prev = self._tail.prev
        
        self._tail.prev.next = node
        self._tail.prev = node
    
    def _delete_node(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def inc(self, key: str) -> None:
        """
        Inserts a new key <Key> with value 1. Or increments an existing key by 1.
        """
        if key not in self._table:
            # Key not in table
            first_node = self._first_node()
            if first_node.val != 1 or first_node == self._tail:
                new_node = DListNode(1)
                new_node.keys.add(key)
                self._insert_front(new_node)
                
                self._table[key] = new_node
            else:
                self._table[key] = first_node
                first_node.keys.add(key)
        else:
            # Key in table
            prev_node = self._table[key]
            prev_val = prev_node.val
            
            if prev_node.next == self._tail or prev_node.next.val != prev_val + 1:
                # Insert new node
                new_node = DListNode(prev_val + 1)
                new_node.keys.add(key)
                
                new_node.next = prev_node.next
                new_node.prev = prev_node
                
                prev_node.next.prev = new_node
                prev_node.next = new_node
                
                # Update table
                self._table[key] = new_node
                
                # Update prev node
                prev_node.keys.discard(key)
                if len(prev_node.keys) == 0:
                    self._delete_node(prev_node)
            else:
                self._table[key] = prev_node.next
                prev_node.next.keys.add(key)
                
                prev_node.keys.discard(key)
                if len(prev_node.keys) == 0:
                    self._delete_node(prev_node)
        
        # print("inc", key)
        # self.debug()

    def dec(self, key: str) -> None:
        """
        Decrements an existing key by 1. If Key's value is 1, remove it from the data structure.
        """
        if key not in self._table:
            return
        
        prev_node = self._table[key]
        prev_val = prev_node.val
        
        if prev_val == 1:
            del self._table[key]
            prev_node.keys.discard(key)
            if len(prev_node.keys) == 0:
                self._delete_node(prev_node)
        else:        
            if prev_node.prev == self._head or prev_node.prev.val != prev_val - 1:
                new_node = DListNode(prev_val - 1)
                new_node.keys.add(key)

                new_node.next = prev_node
                new_node.prev = prev_node.prev

                prev_node.prev.next = new_node
                prev_node.prev = new_node

                self._table[key] = new_node

                prev_node.keys.discard(key)
                if len(prev_node.keys) == 0:
                    self._delete_node(prev_node)
            else:
                self._table[key] = prev_node.prev
                prev_node.prev.keys.add(key)

                prev_node.keys.discard(key)
                if len(prev_node.keys) == 0:
                    self._delete_node(prev_node)
        
        # print("dec", key)
        # self.debug()

    def getMaxKey(self) -> str:
        """
        Returns one of the keys with maximal value.
        """
        node = self._tail.prev
        if node == self._head:
            return ""
        
        keys = node.keys
        return next(iter(keys))

    def getMinKey(self) -> str:
        """
        Returns one of the keys with Minimal value.
        """
        node = self._head.next
        if node == self._tail:
            return ""
        
        keys = node.keys
        return next(iter(keys))

    def debug(self):
        print(self._table)
        curr = self._head.next
        
        while curr != self._tail:
            print(curr)
            curr = curr.next
        print("======")


# Your AllOne object will be instantiated and called as such:
# obj = AllOne()
# obj.inc(key)
# obj.dec(key)
# param_3 = obj.getMaxKey()
# param_4 = obj.getMinKey()

Explain

这个题解使用双向链表和哈希表来实现AllOne类的功能。链表中的每个节点包含一个计数值和一个存储对应计数值的键的集合。哈希表用于快速查找每个键所在的链表节点。在inc和dec操作中,通过更新链表节点的键集合和在链表中插入或删除节点来维护计数值的有序性。getMaxKey和getMinKey操作可以通过返回链表头尾节点的任意一个键来实现。

时间复杂度: O(1)

空间复杂度: O(n)

class DListNode:

    def __init__(self, val):
        self.val = val  # 节点的计数值
        self.keys = set()  # 存储计数值为val的键的集合
        
        self.next = None  # 指向链表的下一个节点
        self.prev = None  # 指向链表的前一个节点
    
    def __repr__(self):
        keys = list(self.keys)
        
        return "'" + str(self.val) + " " + ",".join(keys) + "'"


class AllOne:

    def __init__(self):
        """
        Initialize your data structure here.
        """
        self._table = dict()  # 哈希表,用于快速查找键所在的链表节点
        
        self._head = DListNode(0)  # 双向链表的头节点,哨兵节点
        self._tail = DListNode(0)  # 双向链表的尾节点,哨兵节点
        self._head.next = self._tail
        self._tail.prev = self._head
    
    def _first_node(self):
        return self._head.next  # 返回链表的第一个节点(除哨兵节点外)
    
    def _last_node(self):
        return self._tail.prev  # 返回链表的最后一个节点(除哨兵节点外)
    
    def _insert_front(self, node):
        # 在链表头部插入节点
        node.next = self._head.next
        node.prev = self._head
        
        self._head.next.prev = node
        self._head.next = node
    
    def _insert_last(self, node):
        # 在链表尾部插入节点
        node.next = self._tail
        node.prev = self._tail.prev
        
        self._tail.prev.next = node
        self._tail.prev = node
    
    def _delete_node(self, node):
        # 从链表中删除节点
        node.prev.next = node.next
        node.next.prev = node.prev

    def inc(self, key: str) -> None:
        """
        Inserts a new key <Key> with value 1. Or increments an existing key by 1.
        """
        if key not in self._table:
            # 如果key不在哈希表中
            first_node = self._first_node()
            if first_node.val != 1 or first_node == self._tail:
                # 如果第一个节点的计数值不为1或链表为空,创建新节点并插入链表头部
                new_node = DListNode(1)
                new_node.keys.add(key)
                self._insert_front(new_node)
                
                self._table[key] = new_node
            else:
                # 否则,将key添加到第一个节点的键集合中
                self._table[key] = first_node
                first_node.keys.add(key)
        else:
            # 如果key在哈希表中
            prev_node = self._table[key]
            prev_val = prev_node.val
            
            if prev_node.next == self._tail or prev_node.next.val != prev_val + 1:
                # 如果当前节点是最后一个节点或下一个节点的计数值不等于当前计数值+1,创建新节点并插入当前节点之后
                new_node = DListNode(prev_val + 1)
                new_node.keys.add(key)
                
                new_node.next = prev_node.next
                new_node.prev = prev_node
                
                prev_node.next.prev = new_node
                prev_node.next = new_node
                
                # 更新哈希表
                self._table[key] = new_node
                
                # 更新当前节点
                prev_node.keys.discard(key)
                if len(prev_node.keys) == 0:
                    self._delete_node(prev_node)
            else:
                # 否则,将key添加到下一个节点的键集合中
                self._table[key] = prev_node.next
                prev_node.next.keys.add(key)
                
                prev_node.keys.discard(key)
                if len(prev_node.keys) == 0:
                    self._delete_node(prev_node)

    def dec(self, key: str) -> None:
        """
        Decrements an existing key by 1. If Key's value is 1, remove it from the data structure.
        """
        if key not in self._table:
            return
        
        prev_node = self._table[key]
        prev_val = prev_node.val
        
        if prev_val == 1:
            # 如果当前计数值为1,从哈希表和链表中删除key
            del self._table[key]
            prev_node.keys.discard(key)
            if len(prev_node.keys) == 0:
                self._delete_node(prev_node)
        else:        
            if prev_node.prev == self._head or prev_node.prev.val != prev_val - 1:
                # 如果当前节点是第一个节点或前一个节点的计数值不等于当前计数值-1,创建新节点并插入当前节点之前
                new_node = DListNode(prev_val - 1)
                new_node.keys.add(key)

                new_node.next = prev_node
                new_node.prev = prev_node.prev

                prev_node.prev.next = new_node
                prev_node.prev = new_node

                self._table[key] = new_node

                prev_node.keys.discard(key)
                if len(prev_node.keys) == 0:
                    self._delete_node(prev_node)
            else:
                # 否则,将key添加到前一个节点的键集合中
                self._table[key] = prev_node.prev
                prev_node.prev.keys.add(key)

                prev_node.keys.discard(key)
                if len(prev_node.keys) == 0:
                    self._delete_node(prev_node)

    def getMaxKey(self) -> str:
        """
        Returns one of the keys with maximal value.
        """
        node = self._tail.prev
        if node == self._head:
            return ""  # 如果链表为空,返回空字符串
        
        keys = node.keys
        return next(iter(keys))  # 返回最后一个节点的任意一个键

    def getMinKey(self) -> str:
        """
        Returns one of the keys with Minimal value.
        """
        node = self._head.next
        if node == self._tail:
            return ""  # 如果链表为空,返回空字符串
        
        keys = node.keys
        return next(iter(keys))  # 返回第一个节点的任意一个键

Explore

在`inc`操作中,当一个键的计数增加时,会检查该键当前所在的节点的下一个节点是否存在且其计数值正好是当前计数值加一。如果是,该键就被移动到下一个节点;如果不是,就在当前节点之后创建一个新的节点,并将计数值设为当前计数值加一,然后移动键到这个新节点。这样可以保证链表按计数值递增顺序排列。在`dec`操作中,逻辑类似,检查前一个节点的计数值是否正好是当前计数值减一,按需要进行键的移动或新节点的创建。这种方法确保了每次操作后链表的有序性。

选择返回任意一个键是因为在大多数实际应用中,获取具有最大或最小计数值的一个键已足够使用,并且可以更快地进行操作。如果要获取所有具有相同最大或最小计数值的键,将需要额外的时间来收集这些键,这可能不是必需的。此外,返回单个键可以简化API的使用和理解。

如果当前key的计数值为1,除了从哈希表中删除key外,还需要从其所在的节点的键集合中移除key。如果移除key后该节点的键集合变为空,那么这个节点也应从链表中删除,以保持链表的整洁和准确性。这确保了数据结构的一致性和有效的内存使用。

当前的实现在多线程环境下不是线程安全的。多个线程同时修改链表和哈希表可能会导致数据不一致或竞争条件。为了使其线程安全,可以引入锁机制,例如使用互斥锁(mutex)来同步对关键数据结构的访问。在每个方法执行修改操作前获取锁,并在操作完成后释放锁,可以保证在任何时刻只有一个线程可以修改数据结构,从而保证线程安全。