力扣面试题寻找消失的两个数字多解法总结

26 篇文章 0 订阅
16 篇文章 0 订阅

tags: LeetCode DSA

题目

面试题 17.19. 消失的两个数字

难度困难122

给定一个数组,包含从 1 到 N 所有的整数,但其中缺了两个数字。你能在 O(N) 时间内只用 O(1) 的空间找到它们吗?

以任意顺序返回这两个数字均可。

示例 1:

输入: [1]
输出: [2,3]

示例 2:

输入: [2,3]
输出: [1,4]

提示:

  • nums.length <= 30000

分析与代码

这里一开始我想的当然是用下面这种方法, 但是可惜, 时间复杂度飙升, 直接TLE:

class Solution:
    def missingTwo(self, nums: List[int]) -> List[int]:
        ans=[]
        n=len(nums)
        for i in range(1,3+n):
            if i not in nums:
                ans.append(i)
        return ans

题目要求的是 O ( N ) O(N) O(N)时间复杂度, O ( 1 ) O(1) O(1)空间复杂度, 那么就不能自己创建哈希结构了, 只能从其他思路进行分析.

位运算角度

由于这个题跟之前的一道题(136. 只出现一次的数字 - 力扣(LeetCode))很像, 这里当然也可以借鉴一下那道题的思路, 就是使用位运算的方法1 (参考官方题解), 进行所有数字的按位异或操作, 这里需要注意的是, 题目中要找出的是两个数字, 那么nums全部异或之后还需要再对range(1,n+3)进行异或, 这样针对2n-2个数字异或之后才能保证两两相同的被异或掉,只剩下待求的两个消失的数字, 这里记为 s 1 , s 2 s_1,s_2 s1,s2.

于是通过异或操作我们找到了两数字的异或值 s 1 ⊕ s 2 : = x s_1\oplus s_2:=x s1s2:=x, 但是这还不满足两个方程两个未知量求解的原则, 我们还需要找出一个两数的关系.

这里继续沿用位运算的思路, 因为两个不同数的异或值肯定不是 0 0 0, 否则两数相等.

除此之外, 我们还可以通过计算异或值的最低有效位(least significant bit, 为1的最低位,下称lsb. 这样取然后计算比较方便), 然后对值进行分类, 具体来说, 对于两数的异或值 x x x, 可以表示成 x = x k x k − 1 . . . x 2 x 1 x=x_kx_{k-1}...x_2x_1 x=xkxk1...x2x1, 设 x x xlsb为第 l l l位, 则对这两个消失的数字, 其中一个的第 l l l位为1,另一个的第 l l l位为0, 基于此

就可以把从 1 1 1 n n n 的所有整数分成两类, 其中一类包含所有二进制表示的第 l l l 位为 0 0 0 的数, 另一类包含所有二进制表示的第 l l l 位为 1 1 1 的数. 可以发现:

  • 对于任意一个在数组 nums 中出现一次的数字, 这些数字在上述 2 n − 2 2n−2 2n2 个数字中出现两次, 两次出现会被包含在同一类中;
  • 对于任意一个在数组 nums 中消失的数字, 即$ x_1$ 和 x 2 x_2 x2, 这些数字在上述 2 n − 2 2n−2 2n2 个数字中出现一次, 会被包含在不同类中.

因此, 如果我们将每一类的元素全部异或起来, 那么其中一类会得到 x 1 x_1 x1, 另一类会得到 x 2 x_2 x2. 这样我们就找出了这两个只出现一次的元素.

代码上可以通过先连接数组再遍历的方法, 或者两次循环, 后者速度会快一些,并且减少内存占用.

class Solution:
    def missingTwo(self, nums: List[int]) -> List[int]:  # xor
        xorsum = 0
        n = len(nums) + 2
        for num in nums:
            xorsum ^= num
        for i in range(1, n + 1):
            xorsum ^= i

        lsb = xorsum & (-xorsum)  # 取最低有效位
        type1 = 0
        for num in nums:
            if num & lsb:
                type1 ^= num
        for i in range(1, n + 1):
            if i & lsb:
                type1 ^= i

        return [type1, xorsum ^ type1]

这里跟官解不一样的就是, 第二组可以不进行分类, 直接通过得到的第一个数跟 x x x做异或就得到了第二个数.

求和角度

当然, 直接求和也能得到问题的答案, 不过这里又有两种技巧, 参考了2. 当然, 第一步都是先找出 s 1 + s 2 s_1+s_2 s1+s2, 然后去寻找第二个关系.

第一种技巧

通过公式:
∑ i = 1 n i 2 = n ( n + 1 ) ( 2 n + 1 ) 6 , \sum_{i=1}^ni^2=\dfrac{n(n+1)(2n+1)}6, i=1ni2=6n(n+1)(2n+1),
找出 s 1 2 + s 2 2 s_1^2+s_2^2 s12+s22, 然后联立得到 s 1 − s 2 s_1-s_2 s1s2的值, 解二元一次方程组即可得到答案.

class Solution:
	def missingTwo(self, nums: List[int]) -> List[int]:  # math
        su = ((n := len(nums)) + 2) * (n + 3) // 2 - sum(nums)  # s1+s2
        squ = (n + 2) * (n + 3) * (2 * n + 5) // 6 - \
            sum(map(lambda x: x * x, nums))  # s1*s1+s2*s2
        sm = sqrt(2 * squ - su * su)
        return [int((su + sm) // 2), int(abs(su - sm) // 2)]

第二种技巧

这种方法是基于以下的事实:

消失的两个数字不相等, 则必定有一个值 s ˉ = ( s 1 + s 2 ) / 2 \bar s=(s_1+s_2)/2 sˉ=(s1+s2)/2, 使得 s i ≤ s ˉ , s j ≥ s ˉ ( i , j = { 1 , 2 } ) s_i\leq\bar s,s_j\geq\bar s(i,j=\{1,2\}) sisˉ,sjsˉ(i,j={1,2}).

那么当我们遍历nums时, 对所有小等 s ˉ \bar s sˉ的数求和, 然后用sum(range(1,s_bar+1))(或者等差求和公式)减去这个和, 就能得到第一个数.

class Solution:
	def missingTwo(self, nums: List[int]) -> List[int]:  # math
        n = len(nums) + 2
        sumTwo = n * (n + 1) // 2 - sum(nums)
        lmt = sumTwo // 2
        total = 0
        for i in nums:
            if i <= lmt:
                total += i
        one = lmt * (lmt + 1) // 2 - total
        return [one, sumTwo - one]

Python集合

这个办法比较骚了, 直接一行, 但是空间复杂度拉满了, 面试时候最好别用:

class Solution:
    def missingTwo(self, nums: List[int]) -> List[int]:  # set
        return list(set(range(1, len(nums) + 3)) - set(nums))

原地Hash

这个办法也是比较巧妙的, 参考2, 把数组的值跟索引对应起来, 于是找缺失值就成了遍历找不存在的索引, 这里用-1表示, 其中的交换那一步是思路的重点, nums[i]既是值也是索引,要达到的目的是将对应的值放到对应的索引上.

class Solution:
    def missingTwo(self, nums: List[int]) -> List[int]:  # hash
        nums += [-1] * 3
        for i in range(n := len(nums)):
            while (ni := nums[i]) != i and ni != -1:
                nums[i], nums[ni] = nums[ni], nums[i]
        return [i for i in range(1, n) if nums[i] == -1]

小结

这道题其实跟另外一道有异曲同工之妙:剑指 Offer 56 - I. 数组中数字出现的次数 - 力扣(LeetCode), 本质上都是用位运算然后分组找两个数字的方法(官方), 或者用数学联立等式找关系的方法, 最后的原地哈希算是特别精巧的办法了.

参考


  1. 消失的两个数字 - 消失的两个数字 - 力扣(LeetCode); ↩︎

  2. 专治消失的数的三种解法 - 消失的两个数字 - 力扣(LeetCode); ↩︎ ↩︎

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
目描述 柠檬水找零:在柠檬水摊上,每一杯柠檬水的售价为 5 美元。 顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一个。 每位顾客只买一杯柠檬水,然后向你支付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。 注意,一开始你手头没有任何零钱。 如果你能给每位顾客正确找零,返回 true ,否则返回 false 示例 1: 输入:[5,5,5,10,20] 输出:true 解释: 前 3 位顾客那里分别支付了 5 美元。 第 4 位顾客那里支付了 10 美元,接下来是两个 5 美元。 第 5 位顾客那里支付了 20 美元,接下来是 15 美元, 无法提供且返回false。 示例 2: 输入:[5,5,10] 输出:true 示例 3: 输入:[10,10] 输出:false 示例 4: 输入:[5,5,10,10,20] 输出:false 解思路 使用两个变量 five 和 ten 分别表示手头上的 5 美元钞票和 10 美元钞票的数量。从前往后遍历数组,根据顾客支付的钞票进行分类讨论: 如果顾客支付 5 美元,收入 5 美元钞票一个。 如果顾客支付 10 美元,需要找回一张 5 美元钞票,收入 10 美元钞票一张和减去一张 5 美元钞票。 如果顾客支付 20 美元,优先找回一张 10 美元和一张 5 美元,如果没有再找回三张 5 美元,否则收入不够减,返回 false。 算法流程 遍历 bills,记当前手上拥有的 5 美元张数 five 和 10 美元张数 ten 的数量,初始值为 0。 判断 bills[i] 的大小+0、+5 还是+15,并更新 five 和 ten 的数量。 代码实现 class Solution(object): def lemonadeChange(self, bills): """ :type bills: List[int] :rtype: bool """ five, ten = 0, 0 for bill in bills: if bill == 5: five += 1 elif bill == 10: if not five: return False five -= 1 ten += 1 else: if ten and five: ten -= 1 five -= 1 elif five >= 3: five -= 3 else: return False return True

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

zorchp

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

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

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

打赏作者

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

抵扣说明:

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

余额充值