有一个数组data[n-1]存储了1~n中的n-1个数,问data中缺少的数字是多少【每日一题】

今天参加面试的时候面试官问了这个问题,当时很快速的想出了三种解决方案,当时还自我感觉挺好,回来后跟师兄一说,师兄说这个你没想出最好的方案来。: (

问题描述

有一个数组data,大小是n-1,其中存储的是1~n中的数字,不重复,即1~n中只有一个数字不在该数组内,找出该数字。

基础方法

先把我想出的几种方法例举一下吧。

排序 时间复杂度O(nlogn),空间复杂度O(c)

算法描述

先将data中的元素排序,然后从头开始遍历,如果遍历中发现元素有间断,则为确实的数据。

该算法中,排序的时间复杂度为O(nlogn),然后再遍历一次,因此综合时间复杂度是O(nlogn),空间复杂度O(c)。

代码
#include <stdio.h>
#include <stdlib.h>

int cmp (const void *a, const void *b) { 
    return *(int *)a - *(int *)b; 
}

// 找到不存在的元素
// data: 目标数组
// n: 数组中元素的最大可能值
//    data数组的大小为n-1
int findLost(int data[], int n) {
    // 快排
    qsort(data, n - 1, sizeof(int), cmp);
    int i;
    for (i = 0; i < n; ++i) {
        if (data[i] != i + 1)   // 如果第i个数不等于i+1,则返回i+1
            return i + 1;
    }

    // 程序执行到这里说明前面1~n-1个数都不缺,缺第n个数
    return n;
}

int main(void) {
    int data[8] = {1, 2, 3, 5, 8, 6, 7, 4};
    printf("%d\n", findLost(data, 9));
    return 0;
}

额外空间记录 时间复杂度O(n),空间复杂度O(n)

算法描述

用一个大小为n的数组exists,标记目标数组中的元素是否存在,遍历目标数组,修改exists相应位置的值,最后遍历exists找到不存在的元素。

该算法中需要遍历一次目标数组,遍历两次exists数组(初始化+查看数组值),时间复杂度O(n),空间复杂度O(n)

代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

// 找到不存在的元素
// data: 目标数组
// n: 数组中元素的最大可能值
//    data数组的大小为n-1
int findLost(int data[], int n) {
    bool *exists = malloc(sizeof(bool) * n);
    int i;
    // 初始化
    for (i = 0; i < n; ++i)
        *(exists + i) = false;
    // 遍历data,修改标志位
    for (i = 0; i < n - 1; ++i)
        *(exists + data[i] - 1) = true;
    // 遍历exists,找到不存在的元素
    for (i = 0; i < n; ++i)
        if (!*(exists + i)) {
            free(exists);
            return i + 1;
        }
    // 对于有效数据,程序不会执行到这里
    free(exists);
    return 0; 
}

int main(void) {
    int data[8] = {1, 8, 3, 5, 9, 6, 7, 4};
    printf("%d\n", findLost(data, 9));
    return 0;
}

加减法 时间复杂度O(n),空间复杂度O(c)

算法描述

计算1+2+…+n,用该值减去data中的每个数,差就是缺少的数。

该算法简单易懂,且计算1+2+…+n可直接用公式n*(n+1)/2,然后减去data中的每个数需要遍历data,时间复杂度O(n),空间复杂度O(c)。

代码
#include <stdio.h>

// 找到不存在的元素
// data: 目标数组
// n: 数组中元素的最大可能值
//    data数组的大小为n-1
int findLost(int data[], int n) {
    int sum = n * (n + 1) / 2;
    int i;
    for (i = 0; i < n - 1; ++i)
        sum -= data[i];
    return sum;
}

int main(void) {
    int data[8] = {1, 8, 3, 5, 9, 6, 7, 4};
    printf("%d\n", findLost(data, 9));
    return 0;
}

基础方法总结

对于上述三种方法,第一种时间复杂度不好,第二种空间复杂度不好,第三种时间复杂度和空间复杂度都不错,但是有一个隐藏的缺陷。当时面试官问我:

“如果我的n很大呢?”

当时我没明白他的意思,我还以为他时间复杂度,我说,n很大也没事儿啊,一次遍历O(n)。现在想想真的是太天真了。面试官想说的是:

当n很大的时候,n*(n+1)/2是有可能溢出的,这种情况下加减法的这种解决方案是有问题的

那么,该如何解决或避免这个问题呢?或许有人说可以换用范围更大的数据类型,但是这不是面试官最想听的答案,答案请接着往下看。

高级解决方案

首先,这个方案使用的位运算中的异或(^),a^b当a和b不相等时为1,相等时为0。
其次,这个方案是下面这个题的变种(参考http://blog.csdn.net/zhengzhb/article/details/7289325

有2n+1个数,其中有2n个数出现过两次,找出其中只出现一次的数

来看我们的原题,data[n-1]中存了n-1个数,每个数都是1~n之间的数,且没有重复,那么我们如果用这个n-1个数再加上1~n就变成了2n-1个数,且其中只有一个数出现了一次,其他均出现了两次。这就变成了上面这个题了。

算法描述

将data中的所有元素进行异或运算,然后再将结果与1~n每个元素依次异或,最后得到的结果就是缺少的元素(只出现了一次的元素)。

我们来论证一下这个算法的正确性:

  1. 0 ^ 1 = 1, 1 ^ 0 = 1, 0 ^ 0 = 0, 1 ^ 1 = 0

  2. 对于任意整数n,n ^ 0 = n, n ^ n = 0
    (1)当n与0异或时,由于0的所有二进制位均为0,因此,n的二进制位中为1的与0相应位的二进制位0异或结果为1,n的二进制位中为0的与0相应位的二进制位0异或结果为0,因此异或后的结果与n本身完全相同;(2)当n与n异或时,由于其二进制位完全相同,而根据1中0 ^ 0 = 0, 1 ^ 1 = 0,n ^ n结果的所有位均为0,所以结果为0。

  3. 异或运算满足交换结合律 a ^ b ^ c = a ^ c ^ b.
    其实我们可以将所有的abc均看做二进制形式,其结果可以看做是如下运算:
    00000000 00000000 00000000 00000010 a = 2
    ^
    00000000 00000000 00000000 00000001 b = 1
    ^
    00000000 00000000 00000000 00000100 c = 4


    00000000 00000000 00000000 00000111 result = 7
    即所有运算数的每一位分别异或,因此不论运算顺序如何,结果都相同。

结论
综合1、2、3,然后再根据我们的数据的特点,有2n-1个数,其中有n-1个数出现了两次,只有一个数出现了1次,那么我们将所有的2n-1个数进行异或时,可以看成如下过程,对于出现了两次的元素,x ^ x = 0,然后是n-1个0和剩余的那个只出现了一次的y进行异或,n-1个0异或的结果还是0,最后再与y异或结果是y,y就是我们要找的缺失的元素,因此上述算法是正确的。

这个算法,需要将所有元素做异或运算,时间复杂度O(n),空间复杂度O(c),而且不会有溢出的问题,这是面试官最喜欢的答案了。

代码

#include <stdio.h>

// 找到不存在的元素
// data: 目标数组
// n: 数组中元素的最大可能值
//    data数组的大小为n-1
int findLost(int data[], int n) {
    int result = 0;
    int i;
    for (i = 0; i < n - 1; ++i) {
        result ^= data[i];
        result ^= (i + 1);
    }
    result ^= n;
    return result;
}

int main(void) {
    int data[8] = {1, 8, 3, 5, 9, 6, 7, 4};
    printf("%d\n", findLost(data, 9));
    return 0;
}

代码中稍微做了一点点优化,在前n-1次循环的时候执行两次异或,分别异或data[i]和i+1,这样,代码循环只执行了n-1次,比两个for循环少一次循环。

嗯,面试官最喜欢的就是最后这个答案了。悔不早知 : (


// 个人学习记录,若有错误请指正,大神勿喷
// sfg1991@163.com
// 2015-04-22

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值