432. 全 O(1) 的数据结构 : 数据结构运用题(手写双向链表 + 哈希表)

简介: 432. 全 O(1) 的数据结构 : 数据结构运用题(手写双向链表 + 哈希表)

网络异常,图片无法展示
|


题目描述



这是 LeetCode 上的 432. 全 O(1) 的数据结构 ,难度为 困难


Tag : 「双向链表」、「哈希表」


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

实现 AllOne 类:


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


示例:


输入
["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 <= 101<=key.length<=10
  • key 由小写英文字母组成
  • 测试用例保证:在每次调用 dec 时,数据结构中总存在 key
  • 最多调用 incdecgetMaxKeygetMinKey 方法 5 * 10^45104


双向链表 + 哈希表



题目要求我们支持 O(1)O(1) 的查询和修改,其中查询只需返回任意一个计数次数「最多」和「最少」的元素即可(如果有)。


虽然插入的字符串长度不超过 1010(该数据范围的含义为字符串的哈希计算消耗可看作常数),但单纯的使用「哈希表」仅能做到 O(1)O(1) 的计数,无法做到 O(1)O(1) 查询。


我们可以采取和 实现一个 LRUCache & 实现一个 LFUCache 类似的思路,通过自定义节点并手写双链表来实现。


定义一个节点类 Node,除了包含用于实现双向链表的 leftright 以外,还包含一个数值类型的变量 val, 用于记录该节点存储的是计数次数为多少的元素,以及一个 Set 类型的容器,用于支持 O(1)O(1) 插入和删除元素,记作 set


同时为了快速知道某个字符串属于哪个 Node,我们还需要开一个「哈希表」进行定位(以字符串为哈希表的键,字符串所在 Node 作为值),当定位到字符串对应的 Node 之后则可以利用「双向链表」的 O(1)O(1) 增加/修改/删除。


在双向链表中,起始只有两个哨兵节点 hhtt ,当进行若干 inc/dec 操作后的基本形态为:


网络异常,图片无法展示
|


对应几个操作:


inc/dec 操作:当对一个字符串 key 进行「增加计数」或「减少计数」时,先在哈希表中看 key 是否存在:


  • 若存在:根据其所属的Node的计数cnt为多少,并结合当前是「增加计数」还是「减少计数」来决定是找Node的「右节点」还是「左节点」,同时检查相邻节点的计数值cnt是否为目标值,对应要检查数值是cnt + 1cnt+1cnt - 1cnt1
  • 若相邻节点的 cnt 为目标值:即目标节点存在,将 key 从原 Nodeset 集合中移除,并添加到目标节点的集合中,更新哈希表;
  • 若相邻节点的 cnt 不是目标值:则需要创建相应的目标节点,并构建双向链表关系,把 key 存入新创建的目标节点,更新哈希表。
  • 若不存在(只能是inc操作):查找是否存在cnt = 1cnt=1的节点(也就是检查hh.right节点的计数值):
  • 如果存在 cnt = 1cnt=1 的目标节点:将 key 添加到目标节点的 set 集合中,更新哈希表;
  • 若不存在 cnt = 1cnt=1 的目标节点:创建相应的节点,并构建双向关系,并构建双向链表关系,把 key 存入新创建的目标节点,更新哈希表。


getMaxKey/getMinKey 操作:分别从 tt.lefthh.right 中尝试查找,如果存在非哨兵节点,则从节点的 set 集合中取任意元素进行返回,否则返回空串。


最后,为了确保 getMaxKey/getMinKey 操作能够严格 O(1)O(1),我们在进行 inc/dec 操作时我们需要对一些 set 容量为 00 的节点进行释放,即解除其所在双向链表的关系。


代码:


class AllOne {
    class Node {
        int cnt;
        Set<String> set = new HashSet<>();
        Node left, right;
        Node(int _cnt) {
            cnt = _cnt;
        }
    }
    Node hh, tt;
    Map<String, Node> map = new HashMap<>();
    public AllOne() {
        hh = new Node(-1000); tt = new Node(-1000);
        hh.right = tt; tt.left = hh;
    }
    void clear(Node node) {
        if (node.set.size() == 0) {
            node.left.right = node.right;
            node.right.left = node.left;
        }
    }
    public void inc(String key) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            node.set.remove(key);
            int cnt = node.cnt;
            Node next = null;
            if (node.right.cnt == cnt + 1) {
                next = node.right;
            } else {
                next = new Node(cnt + 1);
                next.right = node.right;
                next.left = node;
                node.right.left = next;
                node.right = next;
            }
            next.set.add(key);
            map.put(key, next);
            clear(node);
        } else {
            Node node = null;
            if (hh.right.cnt == 1) {
                node = hh.right;
            } else {
                node = new Node(1);
                node.right = hh.right;
                node.left = hh;
                hh.right.left = node;
                hh.right = node;
            }
            node.set.add(key);
            map.put(key, node);
        }
    }
    public void dec(String key) {
        Node node = map.get(key);
        node.set.remove(key);
        int cnt = node.cnt;
        if (cnt == 1) {
            map.remove(key);
        } else {
            Node prev = null;
            if (node.left.cnt == cnt - 1) {
                prev = node.left;
            } else {
                prev = new Node(cnt - 1);
                prev.right = node;
                prev.left = node.left;
                node.left.right = prev;
                node.left = prev;
            }
            prev.set.add(key);
            map.put(key, prev);
        }
        clear(node);
    }
    public String getMaxKey() {
        Node node = tt.left;
        for (String str : node.set) return str;
        return "";
    }
    public String getMinKey() {
        Node node = hh.right;
        for (String str : node.set) return str;
        return "";
    }
}
复制代码


  • 时间复杂度:O(1)O(1)
  • 空间复杂度:O(n)O(n)


最后



这是我们「刷穿 LeetCode」系列文章的第 No.432 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。


在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。


为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…


在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

相关文章
|
29天前
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
53 4
|
21天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
43 5
|
1月前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
76 4
|
1月前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
29天前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
47 0
|
1月前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
56 0
|
2月前
|
存储
[数据结构] -- 双向循环链表
[数据结构] -- 双向循环链表
25 0
|
2月前
|
存储
探索数据结构:便捷的双向链表
探索数据结构:便捷的双向链表
|
2月前
|
存储
探索数据结构:单链表的实践和应用
探索数据结构:单链表的实践和应用