LeetCode 1146. 快照数组【哈希表+二分查找】中等

ops/2025/2/11 21:45:00/

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

实现支持下列接口的「快照数组」- SnapshotArray:

  • SnapshotArray(int length) - 初始化一个与指定长度相等的 类数组 的数据结构。初始时,每个元素都等于 0
  • void set(index, val) - 会将指定索引 index 处的元素设置为 val
  • int snap() - 获取该数组的快照,并返回快照的编号 snap_id(快照号是调用 snap() 的总次数减去 1)。
  • int get(index, snap_id) - 根据指定的 snap_id 选择快照,并返回该快照指定索引 index 的值。

示例:

输入:["SnapshotArray","set","snap","set","get"][[3],[0,5],[],[0,6],[0,0]]
输出:[null,null,0,null,5]
解释:
SnapshotArray snapshotArr = new SnapshotArray(3); // 初始化一个长度为 3 的快照数组
snapshotArr.set(0,5);  // 令 array[0] = 5
snapshotArr.snap();  // 获取快照,返回 snap_id = 0
snapshotArr.set(0,6);
snapshotArr.get(0,0);  // 获取 snap_id = 0 的快照中 array[0] 的值,返回 5

提示:

  • 1 <= length <= 50000
  • 题目最多进行50000 次 setsnap,和 get的调用 。
  • 0 <= index < length
  • 0 <= snap_id < 我们调用 snap() 的总次数
  • 0 <= val <= 10^9

解法 哈希表+二分查找

调用 s n a p ( ) snap() snap() 时,复制一份当前数组,作为「历史版本」。返回这是第几个历史版本(从 0 0 0 开始)。

调用 g e t ( i n d e x , s n a p I d ) get(index,snapId) get(index,snapId) 时,返回第 s n a p I d snapId snapId 个历史版本的下标为 index \textit{index} index 的元素值。

暴力?每次调用 s n a p ( ) snap() snap() ,就复制一份数组,可以吗?不行,最坏情况下,复制 50000 50000 50000 次长为 50000 50000 50000 的数组,会「超出内存限制」。

假设每调用一次 s e t set set ,就生成一个快照(复制一份数组)。仅仅是一个元素发生变化,就去复制整个数组,这太浪费了。

能否不复制数组呢?换个视角,调用 s e t ( index , val ) set(\textit{index}, \textit{val}) set(index,val) 时,不去修改数组,而是往下标为 index \textit{index} index 的历史修改记录末尾添加一条数据:此时的快照编号和 v a l val val 。有点像解决哈希冲突的拉链法

举例说明:

  • 在快照编号等于 2 2 2 时,调用 s e t ( 0 , 6 ) set(0, 6) set(0,6)
  • 在快照编号等于 3 3 3 时,调用 s e t ( 0 , 1 ) set(0,1) set(0,1)
  • 在快照编号等于 3 3 3 时,调用 s e t ( 0 , 7 ) set(0,7) set(0,7)
  • 在快照编号等于 5 5 5 时,调用 s e t ( 0 , 2 ) set(0,2) set(0,2)

这四次调用结束后,下标 0 0 0 的历史修改记录 history [ 0 ] = [ ( 2 , 6 ) , ( 3 , 1 ) , ( 3 , 7 ) , ( 5 , 2 ) ] \textit{history}[0] = [(2,6),(3,1),(3,7),(5,2)] history[0]=[(2,6),(3,1),(3,7),(5,2)] ,每个数对中的第一个数为调用 s e t set set 时的快照编号,第二个数为调用 s e t set set 时传入的 v a l val val 。注意历史修改记录中的快照编号是有序的

那么:

  • 调用 g e t ( 0 , 4 ) get(0,4) get(0,4) 。由于历史修改记录中的快照编号是有序的,我们可以在 h i s t o r y [ 0 ] history[0] history[0] 中二分查找快照编号 ≤ 4 \le 4 4 的最后一条修改记录,即 ( 3 , 7 ) (3,7) (3,7) 。修改记录中的 v a l = 7 val=7 val=7 就是答案。
  • 调用 g e t ( 0 , 1 ) get(0,1) get(0,1) 。在 h i s t o r y [ 0 ] history[0] history[0] 中,快照编号 ≤ 1 \le 1 1 的记录不存在,说明在快照编号 ≤ 1 ≤1 1 时,我们并没有修改下标 0 0 0 保存的元素,返回初始值 0 0 0

对于 s n a p snap snap,只需把当前快照编号加一(快照编号初始值为 0 0 0 ),返回加一前的快照编号。

class SnapshotArray:def __init__(self, length: int):self.cur_snap_id = 0self.history = defaultdict(list) # 每个index的历史修改记录都是listdef set(self, index: int, val: int) -> None:self.history[index].append((self.cur_snap_id, val))def snap(self) -> int:self.cur_snap_id += 1return self.cur_snap_id - 1def get(self, index: int, snap_id: int) -> int:# 找快照编号 <= snap_id 的最后一次修改记录# 等价于找快照编号 >= snap_id+1 的第一个修改记录,它的上一个就是答案j = bisect_left(self.history[index], (snap_id + 1, )) - 1return self.history[index][j][1] if j >= 0 else 0
class SnapshotArray {private final Map<Integer, List<int[]>> history = new HashMap<>();private int curSnapId; // 当前快照编号,初始值为0public SnapshotArray(int length) {}public void set(int index, int val) {history.computeIfAbsent(index, k -> new ArrayList<>()).add(new int[]{ curSnapId, val });}public int snap() {return curSnapId++;}public int get(int index, int snap_id) {if (!history.containsKey(index)) return 0;List<int[]> h = history.get(index);int j = search(h, snap_id);return j < 0 ? 0 : h.get(j)[1];}// 返回最大的下标i,满足 h[i][0]<=x// 如果不存在则返回-1private int search(List<int[]> h, int x) {// 开区间(left, right)int left = -1;int right = h.size();while (left + 1 < right) { // 区间不为空// 循环不变量// h[left][0] <= x// h[right][0] > xint mid = left + (right - left) / 2;if (h.get(mid)[0] <= x) {left = mid; // 区间缩小为(mid, right)} else {right = mid; // 区间缩小为(left, mid)}}// 根据循环不变量,此时 h[left][0]<=x 且 h[left+1][0] = h[right][0] > x// 所以left是最大的满足 h[left][0]<=x 的下标// 如果不存在,则left为其初始值-1return left;}
}
class SnapshotArray {
private:int cur_snap_id = 0;unordered_map<int, vector<pair<int, int>>> history; // 每个index的历史修改记录
public:SnapshotArray(int length) {}void set(int index, int val) {history[index].emplace_back(cur_snap_id, val);}int snap() {return cur_snap_id++;}int get(int index, int snap_id) {auto& h = history[index];// 找快照编号 <= snap_id 的最后一次修改记录// 等价于找快照编号 >= snap_id+1 的第一个修改记录,它的上一个就是答案int j = ranges::lower_bound(h, make_pair(snap_id + 1, 0)) - h.begin() - 1;return j >= 0 ? h[j].second : 0;}
};

复杂度分析:

  • 时间复杂度:初始化、 set s e t \texttt{set}set setset snap \texttt{snap} snap 均为 O ( 1 ) \mathcal{O}(1) O(1) get \texttt{get} get O ( log ⁡ q ) \mathcal{O}(\log q) O(logq) ,其中 q q q set \texttt{set} set 的调用次数。
  • 空间复杂度: O ( q ) \mathcal{O}(q) O(q)

http://www.ppmy.cn/ops/28788.html

相关文章

打印机-STM32版本 硬件部分

最终PCB EDA工程: 一、确定芯片型号 根据项目需求&#xff0c;梳理需要用到的功能&#xff0c; 电量检测&#xff1a;ADC 按键&#xff1a;IO input外部中断 LED&#xff1a;IO output 温度检测&#xff1a;ADC 电机控制&#xff1a;IO output 打印通讯&#xff1a;SPI …

安装VMware后的相关配置

一、创建完虚拟机后 看看虚拟机设置里面的DVD&#xff1b;有没有自动检测到 二、打开虚拟机后 一直点击继续3、完成后进行重新下载VM——tools 来进行跨机子的复制粘贴&#xff0c;和屏幕大小的自适应注意:如果安装不了tools是灰色的 点开虚拟机设置——两个光盘都选用物理驱…

数据结构:实验七:数据查找

一、 实验目的 &#xff08;1&#xff09;领会各种查找算法的过程和算法设计。 &#xff08;2&#xff09;掌握查找算法解决实际问题。 二、 实验要求 &#xff08;1&#xff09;编写一个程序exp8-1.cpp, 按提示输入10个任意的整形数据&#xff08;无序&#xff09;&…

Word图片被隐藏了怎么办?

背景 如下图&#xff0c;插入的图片被隐藏了 解决办法 选中图片&#xff0c;右上角有个图标&#xff0c;选择嵌入型 继续选中图片&#xff0c;在状态栏里的段落中&#xff0c;点击小箭头到设置里&#xff08;即段落设置&#xff09; 将行距改成单倍行距 完成

面试题:有一个非常长的url,存储在数据库中,如何对其进行快速查找?

问题来源 同事在面字节的时候遇到的。 我的理解 数据库表中&#xff0c;有一个表&#xff0c;是以url为主键来区分数据&#xff0c;但是url很长&#xff0c;如果根据url进行查找&#xff0c;比较耗时&#xff0c;所以需要根据特殊的手段进行优化。 问题答案 方案一 除了存…

C#发票查验开发示例、发票识别开发文档

或许有人会问&#xff0c;发票查验与发票识别接口是什么&#xff1f;简单来说&#xff0c;它只是集成在财务系统或APP等应用中的一个功能模块。只需上传发票图片&#xff0c;发票识别接口即可快速提取发票代码、号码、日期、金额、校验码等关键信息&#xff0c;发票查验接口实时…

使用 BurpSuite 基于 Token 机制实施暴力破解

前言 Token是一种用于身份验证和授权的令牌&#xff0c;通常由服务器生成并发送给客户端&#xff0c;客户端在后续的请求中携带该令牌来进行身份验证和授权操作。Token的使用可以增强应用程序的安全性&#xff0c;避免了直接传递敏感凭证&#xff08;如用户名和密码&#xff0…

ngrinder项目-本地调试遇到的坑

前提-maven mirrors配置 <mirrors><!--阿里公有仓库--><mirror><id>nexus-aliyun</id><mirrorOf>central</mirrorOf><name>Nexus aliyun</name><url>http://maven.aliyun.com/nexus/content/groups/public</ur…