大家好啊我是秋刀鱼,今天呢因为时间的关系就没有写蓝桥杯每日刷题的题解了,不过在刷LeetCode的每日一题时觉得题目很有意思,因此我想给大家分享这道题并阐述一下我的解题思路,希望能对你有帮助。如果喜欢的话请务必一键三联支持一下博主哦。
![]()
🌍LeetCode 432. 全O(1)的数据结构
AC截图
题目难度:⭐️⭐️⭐️⭐️
知识点:哈希表、双向链表
🌑1、题目要求
题目中要求完成一个数据结构类的编写,该类能够存储一个字符串key
与该key
相对应的计数,并能够对任意 key 计数值实现加一、减一的操作,同时能够返回最大计数值的 key 与最小计数值的key 。
指定了该类拥有下面几个函数:
void inc(String key)
:key 对应的计数值加一,如果 key 值没有存储过,那么插入计数值为 1 的 keyvoid 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<>();
}
数据结构如下图所示
图片来源:
我们已经完成数据结构的搭建,现在就让我们开始处理代码逻辑。
🌓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存储每一个 计数值最左侧结点的位置即可,感兴趣的小伙伴可以自己写一下。
写在最后
如果博客中说明部分、代码存在任何问题,欢迎大家指正,同时如果有任何疑问,也可以在评论区交流,大家互相学习嘛,博主不会吃人!
最后的最后希望能收到你的一个小小的赞,谢谢你能看完ღ( ´・ᴗ・` )比心
