LeetCode算法日记 - Day 21: 消失的两个数字、替换所有的问号
目录
1. 消失的两个数字
1.1 题目解析
1.2 解法
1.3 代码实现
2. 替换所有的问号
2.1 题目解析
2.2 解法
2.3 代码实现
1. 消失的两个数字
面试题 17.19. 消失的两个数字 - 力扣(LeetCode)
给定一个数组,包含从 1 到 N 所有的整数,但其中缺了两个数字。你能在 O(N) 时间内只用 O(1) 的空间找到它们吗?
以任意顺序返回这两个数字均可。
示例 1:
输入:[1]
输出:[2,3]
示例 2:
输入:[2,3]
输出:[1,4]
提示:
nums.length <= 30000
1.1 题目解析
这是一个「两类经典题目的结合体」:
-
只出现一次的数字 III → 提供了分组筛选的思路:利用异或和最低位不同的二进制位,把数组划分成两组,再各自异或出答案。
-
丢失的数字 → 提供了范围取值的思路:通过 1..N 和数组对比,利用位运算抵消信息,最终定位缺失或特殊的数字。
换句话说:
-
我们先要找到两个“落单数”整体的异或值。
-
然后借助「分组异或」手法,把它们拆分出来。
如果没有做过这两道题请移步到本博主的这两篇博客日记,会有更详细的解答。
LeetCode算法日记 - Day 18: 只出现一次的数字、只出现一次的数字III-CSDN博客
LeetCode算法日记 - Day 19:判定字符是否唯一、丢失的数字-CSDN博客
常规解法
哈希表统计频次 → 直接返回出现一次的两个数字。
-
时间:O(n),空间:O(n),虽然能过,但不符合题目要求(必须 O(1) 空间)。
-
所以必须转向「位运算」这一类对数值分布敏感的解法。
思路转折
要在 O(1) 空间下解决:
-
必须让数组里成对的数字能自动抵消。
-
异或运算正好具备这个特性:x ^ x = 0,0 ^ y = y。
-
于是整体异或得到 a ^ b,再用最低位的不同点分组,就能单独求出 a 和 b。
这就是「只出现一次的数字 III」的核心思想,而能联想到用异或抵消,其实也是源自「丢失的数字」给过的启发。
1.2 解法
-
整体异或 → 得到两个只出现一次数字的异或值。
-
利用最低位的不同二进制位,区分出这两个数字。
-
分组异或 →单独得到 a 和 b。
i)tmp = 0,遍历数组:tmp ^= num,for循环 tmp^i 也就是完整范围部分,遍历 i
的长次数是 nums 长度+2,最终得到 tmp = a ^ b。
ii)mark = Integer.lowestOneBit(tmp) → 取出最低的一个二进制 1。 。
iii)再遍历数组:
-
(num & mark) == 0 的分到一组,整体异或得到 a。
-
(num & mark) != 0 的分到另一组,整体异或得到 b。
-
(i & mark) == 0 分到一组,整体异或得到 a。
-
(i & mark) != 0 分到一组,整体异或得到 b。
iiii)返回 [a, b]。
1.3 代码实现
class Solution {public int[] singleNumber(int[] nums) {// 第一次整体异或,得到 a ^ bint tmp = 0;for (int num : nums) {tmp ^= num;}// 找到最低位不同的标志位int mark = Integer.lowestOneBit(tmp);// 分组异或int a = 0, b = 0;for (int num : nums) {if ((num & mark) == 0) {a ^= num;} else {b ^= num;}}return new int[]{a, b};}
}
复杂度分析
-
时间复杂度:O(n),两次线性扫描。
-
空间复杂度:O(1),只用常量存储。
2. 替换所有的问号
1576. 替换所有的问号 - 力扣(LeetCode)
给你一个仅包含小写英文字母和 '?'
字符的字符串 s
,请你将所有的 '?'
转换为若干小写字母,使最终的字符串不包含任何 连续重复 的字符。
注意:你 不能 修改非 '?'
字符。
题目测试用例保证 除 '?'
字符 之外,不存在连续重复的字符。
在完成所有转换(可能无需转换)后返回最终的字符串。如果有多个解决方案,请返回其中任何一个。可以证明,在给定的约束条件下,答案总是存在的。
示例 1:
输入:s = "?zs" 输出:"azs" 解释:该示例共有 25 种解决方案,从 "azs" 到 "yzs" 都是符合题目要求的。只有 "z" 是无效的修改,因为字符串 "zzs" 中有连续重复的两个 'z' 。
示例 2:
输入:s = "ubv?w" 输出:"ubvaw" 解释:该示例共有 24 种解决方案,只有替换成 "v" 和 "w" 不符合题目要求。因为 "ubvvw" 和 "ubvww" 都包含连续重复的字符。
2.1 题目解析
题目本质
-
输入一个只包含小写字母和 '?' 的字符串。
-
需要将 '?' 替换为小写字母,使得最终字符串不包含连续重复字符。
-
保证原有非 '?' 字符本身没有连续重复。
可以抽象为:在字符串中找到“空位”,选择合法字符填充,使左右字符不重复的问题
常规解法
-
遍历字符串,遇到 '?' 随意填充一个字母。
-
检查替换后是否出现连续重复字符,如果有则重新尝试。
-
这种方法简单,但每次都要回溯检查,最坏情况下复杂度高达 O(26^k),k 为 '?' 数量。
-
当 '?' 较多时,效率低下。
思路转折
-
发现规律:每个 '?' 只需考虑左右相邻字符。
-
因为题目保证原始字符串没有连续重复,所以只需保证填入的字符 ≠ 左右邻居即可。
-
不必回溯、多次尝试,直接顺序填充即可。
-
这样复杂度可降到 O(n)。
2.2 解法
算法思路:
-
顺序遍历字符串。
-
遇到 '?',从 'a' 到 'z' 遍历,选择不等于左边或右边字符的字母填充。
-
每次填充后立即停止当前 '?' 的选择,保证不会和左右重复。
i)将字符串转换为字符数组,便于修改。
ii)遍历字符数组:
-
如果当前字符不是 '?' → 跳过。
-
如果是 '?' → 从 'a' 到 'z' 找一个合法字符 c:
-
左边合法:i==0 || ch[i-1] != c
-
右边合法:i==ch.length-1 || ch[i+1] != c
-
若左右都合法 → 填充 c,并 break。
-
iii)遍历结束后,将字符数组转换回字符串并返回。
2.3 代码实现
class Solution {public String modifyString(String s) {char[] ch = s.toCharArray();for (int i = 0; i < ch.length; i++) {if (ch[i] == '?') {for (char c = 'a'; c <= 'z'; c++) {boolean leftOk = (i == 0 || ch[i - 1] != c);boolean rightOk = (i == ch.length - 1 || ch[i + 1] != c);if (leftOk && rightOk) {ch[i] = c;break;}}}}return new String(ch);}
}
复杂度分析
-
时间复杂度:O(n),每个字符最多尝试 26 种字母。
-
空间复杂度:O(n),用于存储字符数组。