【刷穿LeetCode系列】 LeetCode 432. 全 O(1) 的数据结构 解析

大家好啊我是秋刀鱼,今天呢因为时间的关系就没有写蓝桥杯每日刷题的题解了,不过在刷LeetCode的每日一题时觉得题目很有意思,因此我想给大家分享这道题并阐述一下我的解题思路,希望能对你有帮助。如果喜欢的话请务必一键三联支持一下博主哦。

小狼狗系列表情包

🌍LeetCode 432. 全O(1)的数据结构

image-20220316191223294

AC截图

image-20220316193501065

题目传送门

题目难度:⭐️⭐️⭐️⭐️

知识点:哈希表、双向链表

🌑1、题目要求

题目中要求完成一个数据结构类的编写,该类能够存储一个字符串key与该key相对应的计数,并能够对任意 key 计数值实现加一、减一的操作,同时能够返回最大计数值的 key 与最小计数值的key 。

指定了该类拥有下面几个函数:

  • void inc(String key):key 对应的计数值加一,如果 key 值没有存储过,那么插入计数值为 1 的 key
  • void dec(String key):key 对应的计数值减一,如果计数值减为0,将 key 移除数据结构
  • String getMaxKey():获取 计数值最大的 key ,如果没有元素则返回 “”
  • Stirng getMinKye(): 获取 计数值最小的 key , 若果没有元素则返回 “”

🌒2、自建数据结构

我第一眼看到题目要求时整个人是呆住的,如果真的能实现这个数据结构那还需要TreeMap做啥,时间复杂度 O ( 1 ) O(1) O(1) 啊!相信很多人和我一样开始时都是懵逼的,但是其实题目中有一个重要的要求大家没有看清,**每一次 key 的计数值只会+1或-1!**也就是意味着如果能够维护计数值的顺序,并使用HashMap来记录 key 与索引的关系,那么在每一次 计数值被修改时,只需要与其相近的两个元素比较即可,因为最大的变化值为1

看到这里可能很多朋友已经有思路了,下面来看看我的构建思路:

  • 创建一个双向链表,链表中存储 key计数值

    • 链表中保留一个头指针head,尾指针tail,头指针指向第一个元素结点的前一个结点,尾指针指向最后一个元素

      //----------------------数据结构-----------------
      // 链表结点的数据结构
      class Node{
          // 计数值
          int value ;
          // key
          String str;
          // 前置指针
          Node pre;
          // 后置指针
          Node next;
          
          Node(String str){
              this.str = str;
              value = 1;
              pre = next = null;
          }
      }
      
    • 链表的结点按照计数值从大到小的顺序存储,计数值最大的结点靠近head,计数值最小的结点靠近tail。getMaxKey、getMinKey只需要返回如下的结果:

      public String getMaxKey() {
          // 头指针下一个元素为空,说明数据结构中没有任何结点
          if (head.next == null) {
              return "";
          }
      
          return head.next.str;
      }
      // 尾指针前置指针为空,说明数据结构中没有任何结点
      public String getMinKey() {
          if (tail.pre == null) {
              return "";
          }
          
          return tail.str;
      }
      
  • 其次需要保留 key - Node 之间的关系,关系使用HashMap来存储,定义Map<String,Node>map

// 存储映射关系
Map<String, Node> map;

// 双向链表头部、尾部结点
Node head;
Node tail;
// 外部类构造函数
public AllOne() {
    head = new Node(null);
    tail = head;
    map = new HashMap<>();
}

数据结构如下图所示

图片来源

image.png

我们已经完成数据结构的搭建,现在就让我们开始处理代码逻辑。

🌓3、代码逻辑

编写inc方法

  • 该方法在没有 key 数据时会创建一个新的 key 存储计数值,同时双向链表中也需要添加一个新的结点。之前我们有说过,双向链表存储顺序按照计数值递减顺序存储,此时新加入的结点计数值为1,因此只需要使用尾插法插入一个新的结点,这里需要注意,tail指针始终指向最后一个结点,因此tail指针需要向后移动。
  • 在存在 key 数据时,将 key 的计数值 +1 ,此时 +1后为了维持递减的次序,需要比较的结点是+1之前比 key 结点更大的结点,定义为pre,如果说pre是头指针,或pre的计数值 >= key 的计数值,说明+1后不会对原链表产生影响,直接返回。否则的话,需要更新链表中的数据。

更新的操作我没有采用链表指针地址的更新,而是做一个交换,也就是下面这个函数,用于两个链表结点数据交换。

// 更新函数
private void sawp(Node temp,Node pre){
    // 保留更新前的 key
    String strTemp = temp.str;
    String strPre = pre.str;
    //交换 hashMap 中存储的节点位置
    map.put(strTemp, pre);
    map.put(strPre, temp);
    // 保留值
    int v = temp.value;
    // 交换结点中存储的值
    temp.str = strPre;
    pre.str = strTemp;
    temp.value = pre.value;
    pre.value=v;
}

问题来了,哪一个结点与 key 结点进行交换呢?答案是 :**除头结点外计数值小于等于 key 结点计数值的最左侧结点。**为了找到该结点,这里我使用了一个while循环遍历直到找到需要交换的结点,并完成交换:

    public void inc(String key) {
        Node temp = map.get(key);
        // 创建新的结点插入
        if (temp == null) {
            temp = new Node(key);
            temp.pre = tail;
            tail.next = temp;
            tail = tail.next;
            map.put(key, temp);
        } else {
            temp.value += 1;
            // 上一个结点
            Node pre = temp.pre;
            if (pre.str == null || pre.value >= temp.value) {
                return;
            }
            // 寻找交换结点
            while(pre.pre.str!=null&&pre.pre.value<temp.value){
                pre=pre.pre;
            }
            sawp(temp, pre);
        }
    }

编写dec方法

dec方法与inc方法类似,只不过比较的对象从左侧结点变为其右侧节点,定义其右侧节点为next

  • 如果 key 对应的计数值减小为0,需要将该结点移除链表、移除哈希表,需要注意的是,如果需要移除的结点是tail结点,那么此时tail = tail.pre将tail指针向前移动。
  • 如果 key 对应的计数值不为0,此时需要判断是否需要交换,如果 右侧节点值大于 key 计数值,此时交换两节点的数据。
public void dec(String key) {
    Node temp = map.get(key);
    Node next = temp.next;
    temp.value -= 1;
    if (temp.value == 0) {
        if(temp==tail)
            tail = tail.pre;
        if (temp.next == null) {
            temp.pre.next = null;
        } else {
            temp.pre.next = temp.next;
            temp.next.pre = temp.pre;
        }
        map.remove(key);
        return;
    }
    if (next == null || next.value <= temp.value) {
        return;
    }
    sawp(temp, next);
}

🌗4、AC代码

class AllOne {
    
    class Node{
        int value ;
        Node pre;
        Node next;
        String str;
        Node(String str){
            this.str = str;
            value = 1;
            pre = next = null;
        }
    }
    Map<String, Node> map;
    Node head;
    Node tail;
    public AllOne() {
        head = new Node(null);
        tail = head;
        map = new HashMap<>();
    }

    private void sawp(Node temp,Node pre){
        String strTemp = temp.str;
        String strPre = pre.str;
        map.put(strTemp, pre);
        map.put(strPre, temp);
        int v = temp.value;
        temp.str = strPre;
        pre.str = strTemp;
        temp.value = pre.value;
        pre.value=v;
    }
    public void inc(String key) {
        Node temp = map.get(key);
        if (temp == null) {
            temp = new Node(key);
            temp.pre = tail;
            tail.next = temp;
            tail = tail.next;
            map.put(key, temp);
        } else {
            temp.value += 1;
            Node pre = temp.pre;
            if (pre.str == null || pre.value >= temp.value) {
                return;
            }
            while(pre.pre.str!=null&&pre.pre.value<temp.value){
                pre=pre.pre;
            }
            sawp(temp, pre);
        }
    }

    public void dec(String key) {
        Node temp = map.get(key);
        Node next = temp.next;
        temp.value -= 1;
        if (temp.value == 0) {
            if(temp==tail)
                tail = tail.pre;
            if (temp.next == null) {
                temp.pre.next = null;
            } else {
                temp.pre.next = temp.next;
                temp.next.pre = temp.pre;
            }
            map.remove(key);
            return;
        }
        if (next == null || next.value <= temp.value) {
            return;
        }
        sawp(temp, next);
    }

    public String getMaxKey() {
        if (head.next == null) {
            return "";
        }

        return head.next.str;
    }
    public String getMinKey() {
        if (tail.pre == null) {
            return "";
        }
        return tail.str;
    }
}

🌕5、优化

其实细心的小伙伴就要发问了:

为啥使用了while循环还自称是 O ( 1 ) O(1) O(1)的时间复杂度啊,这解法一定有问题!!

其实数据分散的情况下我们可以将本代码中的while循环看作成常数时间复杂度,因为while循环中只有在值相同的情况下才会一直循环,而如果值较分散,那么这部分查找时间可以忽略不计。

但是如果不使用while循环可以吗?也是可以的,算法界有一句老话说的好,时间换空间

我们可以额外定义一个HashMap存储每一个 计数值最左侧结点的位置即可,感兴趣的小伙伴可以自己写一下。

写在最后

如果博客中说明部分、代码存在任何问题,欢迎大家指正,同时如果有任何疑问,也可以在评论区交流,大家互相学习嘛,博主不会吃人!

最后的最后希望能收到你的一个小小的赞,谢谢你能看完ღ( ´・ᴗ・` )比心

小狼狗系列表情包
  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋刀鱼与猫_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值