leetcode
leetcode 401 ~ 450
全 O(1) 的数据结构

全 O(1) 的数据结构

难度:

标签:

题目描述

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

实现 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

代码结果

运行时间: 96 ms, 内存: 0.0 MB


/*
 * 思路:
 * 使用Java Stream实现题目要求的方法比较复杂,
 * 因为Stream的操作通常是无状态的,而本题要求的操作需要状态保持。
 * 但是可以通过Java 8+的集合框架来实现,
 * 主要利用Map和List来实现对字符串的计数和排序。
 */
 
import java.util.*;
import java.util.stream.Collectors;
 
class AllOne {
    private Map<String, Integer> keyCount;
    private Map<Integer, Set<String>> countKey;
 
    public AllOne() {
        keyCount = new HashMap<>();
        countKey = new HashMap<>();
    }
 
    public void inc(String key) {
        int count = keyCount.getOrDefault(key, 0);
        keyCount.put(key, count + 1);
        if (count > 0) {
            countKey.get(count).remove(key);
            if (countKey.get(count).isEmpty()) countKey.remove(count);
        }
        countKey.computeIfAbsent(count + 1, k -> new HashSet<>()).add(key);
    }
 
    public void dec(String key) {
        int count = keyCount.get(key);
        if (count == 1) {
            keyCount.remove(key);
        } else {
            keyCount.put(key, count - 1);
        }
        countKey.get(count).remove(key);
        if (countKey.get(count).isEmpty()) countKey.remove(count);
        if (count > 1) countKey.computeIfAbsent(count - 1, k -> new HashSet<>()).add(key);
    }
 
    public String getMaxKey() {
        return countKey.isEmpty() ? "" : countKey.entrySet().stream().max(Map.Entry.comparingByKey()).get().getValue().iterator().next();
    }
 
    public String getMinKey() {
        return countKey.isEmpty() ? "" : countKey.entrySet().stream().min(Map.Entry.comparingByKey()).get().getValue().iterator().next();
    }
}
 

解释

方法:

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

时间复杂度:

O(1)

空间复杂度:

O(n)

代码细节讲解

🦆
在`inc`和`dec`操作中,如何确保每次插入或删除节点后,链表仍然维持有序状态?
在`inc`操作中,当一个键的计数增加时,会检查该键当前所在的节点的下一个节点是否存在且其计数值正好是当前计数值加一。如果是,该键就被移动到下一个节点;如果不是,就在当前节点之后创建一个新的节点,并将计数值设为当前计数值加一,然后移动键到这个新节点。这样可以保证链表按计数值递增顺序排列。在`dec`操作中,逻辑类似,检查前一个节点的计数值是否正好是当前计数值减一,按需要进行键的移动或新节点的创建。这种方法确保了每次操作后链表的有序性。
🦆
为什么`getMaxKey`和`getMinKey`操作选择返回链表头尾节点的任意一个键,而不是所有具有最大或最小计数值的键?
选择返回任意一个键是因为在大多数实际应用中,获取具有最大或最小计数值的一个键已足够使用,并且可以更快地进行操作。如果要获取所有具有相同最大或最小计数值的键,将需要额外的时间来收集这些键,这可能不是必需的。此外,返回单个键可以简化API的使用和理解。
🦆
在`dec`方法中,如果当前key的计数值为1,直接删除key后,是否还需要对链表中的节点进行其他更新处理?
如果当前key的计数值为1,除了从哈希表中删除key外,还需要从其所在的节点的键集合中移除key。如果移除key后该节点的键集合变为空,那么这个节点也应从链表中删除,以保持链表的整洁和准确性。这确保了数据结构的一致性和有效的内存使用。
🦆
如果在多线程环境下使用此数据结构,当前的实现是否安全?如果不是,需要如何修改才能保证线程安全?
当前的实现在多线程环境下不是线程安全的。多个线程同时修改链表和哈希表可能会导致数据不一致或竞争条件。为了使其线程安全,可以引入锁机制,例如使用互斥锁(mutex)来同步对关键数据结构的访问。在每个方法执行修改操作前获取锁,并在操作完成后释放锁,可以保证在任何时刻只有一个线程可以修改数据结构,从而保证线程安全。

相关问题