您的位置:首页 > 其它

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

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

问题描述

有一个数组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。

其次,这个方案是下面这个题的变种(参考/article/1425252.html

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

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

算法描述

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

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

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

对于任意整数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。

异或运算满足交换结合律 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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐