算法笔记-lc-面试题 17.19. 消失的两个数字(困难)

@[TOC](算法笔记-lc-面试题 17.19. 消失的两个数字(困难))

题目

题干

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

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

示例

示例 1:

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

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

提示:

nums.length <= 30000

题解

如果本题不要求时间复杂度和空间复杂度的话,可以使用排序(标记数组)或哈希表,其中排序的时间复杂度可以达到O(n)(计数排序相当于标记数组)-O(nlogn)(快速排序),哈希表的时间复杂度与标记数组相同都为O(n),这几种未优化的算法空间复杂度均为O(n)

方法一:位运算

思路与算法

寻找消失的数字,最直观的方法是使用哈希表存储数组中出现过的数字。由于这道题有时间复杂度 O(n) 和空间复杂度 O(1) 的要求,因此不能使用哈希表求解,必须使用其他方法。利用位运算的性质,可以达到时间复杂度 O(n) 和空间复杂度 O(1)。

由于从 1 到 n 的整数中有两个整数消失,其余每个整数都在数组中出现一次,因此数组的长度是 n−2。在数组中的 n−2 个数后面添加从 1 到 n 的每个整数各一次,则得到 2n−2 个数字,其中两个在数组中消失的数字各出现一次,其余每个数字各出现两次。

假设数组 nums 中消失的两个数字分别是 x1​和 x2 。如果把上述 2n−2 个数字全部异或起来,得到结果 x,那么一定有:
x=x1⊕x2其中⊕ 表示异或运算。这是因为nums 中出现两次的元素都会因为异或运算的性质 a⊕b⊕b=a 抵消掉,那么最终的结果就只剩下 x1 和 x2的异或和。
显然x! =0,因为如果 x=0,那么说明 x1 =x2​,这样x1和 x2就不是在上述 2n−2 个数字中只出现一次的数字了。因此,我们可以使用位运算 x & -x 取出 x 的二进制表示中最低位那个 1,设其为第 l 位,那么x1和 x2 中的某一个数的二进制表示的第 l 位为 0,另一个数的二进制表示的第 l 位为 1。在这种情况下,x1⊕x2的二进制表示的第 l 位才能为 1。
这样一来,我们就可以把从 11 到 nn 的所有整数分成两类,其中一类包含所有二进制表示的第 l 位为 0 的数,另一类包含所有二进制表示的第 l 位为 1 的数。可以发现:
对于任意一个在数组nums 中出现一次的数字,这些数字在上述 2n−2 个数字中出现两次,两次出现会被包含在同一类中;对于任意一个在数组nums 中消失的数字,即 x1和 x2 ,这些数字在上述 2n - 22n−2 个数字中出现一次,会被包含在不同类中。
因此,如果我们将每一类的元素全部异或起来,那么其中一类会得到 x1,另一类会得到 x2。这样我们就找出了这两个只出现一次的元素。

代码

class Solution {
    public int[] missingTwo(int[] nums) {
        int xorsum = 0;
        int n = nums.length + 2;
        for (int num : nums) {
            xorsum ^= num;
        }
        for (int i = 1; i <= n; i++) {
            xorsum ^= i;
        }
        // 防止溢出
        int lsb = (xorsum == Integer.MIN_VALUE ? xorsum : xorsum & (-xorsum));
        int type1 = 0, type2 = 0;
        for (int num : nums) {
            if ((num & lsb) != 0) {
                type1 ^= num;
            } else {
                type2 ^= num;
            }
        }
        for (int i = 1; i <= n; i++) {
            if ((i & lsb) != 0) {
                type1 ^= i;
            } else {
                type2 ^= i;
            }
        }
        return new int[]{type1, type2};
    }
}

复杂度分析

时间复杂度:O(n),其中 n 是最大的整数。需要遍历的数字有 2n−2 个,共遍历两次。

空间复杂度:O(1)。

方法二:解方程

我们记缺的两数为x、y,首先我们计算出n数的和以及实际和,差值即为a = x + y; 然后我们计算出n数的平方和与实际和,差值即为b = x * x + y * y。联立得到一元二次方程,不难得出两根分别为(a (±) sqrt(2 * b - a * a)) / 2。

class Solution {
	public int[] missingTwo(int[] nums) {
        long n = nums.size() + 2;
        int a = -accumulate(nums.cbegin(), nums.cend(), -(1 + n) * n / 2);
        int b = -inner_product(nums.cbegin(), nums.cend(), nums.cbegin(), -(1 + n) * n / 2 * (2 * n + 1) / 3);
        int tmp = Math.pow(2 * b - a * a,2);
        return { (a + tmp) / 2, (a - tmp) / 2 };
    }
}

方法三:数学

根据题意,给定 nums 的长度为 m 且缺失了两个数,所有的 nums[i] 加上缺失数字组成连续排列长度为 n=m+2。

根据等差数量求和公式可知,补齐后的排列总和为2n×(1+n),补全后的理论总和与实际总和之间的差值cur= 2n×(1+n)−
∑ i=0
m−1
​ nums[i] 为缺失数值之和。

根据补全后数值各不相同可知,两者必不可能同时位于t=2/cur的同一侧或共点(偏大、偏小或数值重复),因此我们可以计算 [1,t] 范围内的理论总和与实际总和之间的差值来确定其一(将问题转换为求解缺失一值),再结合缺失两值之和 sum 算得答案。

代码:

class Solution {
    /**解题思路:
     * 宫水三叶题解---数学
     */
    public int[] missingTwo(int[] nums) {
        int n = nums.length + 2;  //数组原本的长度
        int cur = n * (1 + n) / 2;  //数组原本的总和,根据等差数列求和公式得出
        for (int x : nums) cur -= x;  //cur为缺失的两个数的和
        int sum = cur;
        int t = cur / 2;  //t为缺失的两个数的和的一半
        cur = t * (1 + t) / 2;  //求数组[1, t]的理论总和
        for (int x : nums) {  //求数组[1, t]的实际总和,理论与实际之差即缺失两数之一
            if (x <= t) cur -= x;
        }
        return new int[]{cur, sum - cur};
    }
}

时间复杂度:O(n)
空间复杂度:O(1)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值