万书网 > 文学作品 > 漫画算法:小灰的算法之旅 > 5.12寻找缺失的整数

5.12寻找缺失的整数





5.12.1 “五行”缺一个整数



下面考你一道算法题:在一个无序数组里有99个不重复的正整数,范围从1到100……

题目

在一个无序数组里有99个不重复的正整数,范围是1~100,唯独缺少1个1~100中的整数。如何找出这个缺失的整数?

哦,让我想想……

有了!创建一个哈希表,以1到100这100个整数为Key,然后遍历数组。

解法1:

创建一个哈希表,以1到100这100个整数为Key。然后遍历整个数组,每读到一个整数,就定位到哈希表中对应的Key,然后删除这个Key。

由于数组中缺少1个整数,哈希表最终一定会有99个Key被删除,从而剩下1个唯一的Key。这个剩下的Key就是那个缺失的整数。

假设数组长度是n,那么该解法的时间复杂度是O(n),空间复杂度是O(n)。

OK,这个解法在时间上是最优的,但额外开辟了内存空间。那么,有没有办法降低空间复杂度呢?

哦,让我想想……

有了!首先给原数组排序,然后……

解法2:

先把数组元素从小到大进行排序,然后遍历已经有序的数组,如果发现某两个相邻元素并不连续,说明缺少的就是这两个元素之间的整数。

假设数组长度是n,如果用时间复杂度为O(nlogn)的排序算法进行排序,那么该解法的时间复杂度是O(nlogn),空间复杂度是O(1)。

OK,这个解法没有开辟额外的空间,但是时间复杂度又太大了。有没有办法对时间复杂度和空间复杂度都进行优化呢?

哦,让我想想……

有了!先算出1~100的累加和,然后再依次减去数组里的所有元素,最后的差值就是所缺少的整数。这么简单的办法我竟然才想到!

解法3:

这是一个很简单也很高效的方法,先算出1+2+3+…+100的和,然后依次减去数组里的元素,最后得到的差值,就是那个缺失的整数。

假设数组长度是n,那么该解法的时间复杂度是O(n),空间复杂度是O(1)。

OK,对于没有重复元素的数组,这个解法在时间和空间上已经最优了。但如果把问题扩展一下……



5.12.2 问题扩展

题目第1次扩展:

一个无序数组里有若干个正整数,范围是1~100,其中99个整数都出现了偶数次  ,只有1个整数出现了奇数次  ,如何找到这个出现奇数次的整数?

哦,让我想想……

按照刚才的方法先求和肯定不行,因为根本不知道每个整数出现的次数……同时又要保证时间和空间复杂度的最优,怎么办呢?

让我提示你一下吧,你知道异或运算  吗?

异或运算,我当然知道,在进行位运算时,相同位得0,不同位得1。可是怎么应用到这个题目上面呢?

啊,我想到了!只要把数组里所有元素依次进行异或运算,最后得到的就是那个缺失的整数!

解法:

遍历整个数组,依次做异或运算。由于异或运算在进行位运算时,相同为0,不同为1,因此所有出现偶数次的整数都会相互抵消变成0,只有唯一出现奇数次的整数会被留下。

让我们举一个例子:给出一个无序数组{3,1,3,2,4,1,4}。

异或运算像加法运算一样,满足交换律和结合律,所以这个数组元素的异或运算的结果如下图所示。

假设数组长度是n,那么该解法的时间复杂度是O(n),空间复杂度是O(1)。

这个方案已经非常好了。我们把问题最后扩展一下,如果数组里有2个整数出现了奇数次  ,其他整数出现偶数次,该如何找出这2个整数呢?

题目第2次扩展:

假设一个无序数组里有若干个正整数,范围是1~100,其中有98个整数出现了偶数次,只有2个  整数出现了奇数次,如何找到这2个出现奇数次的整数?

啊,这次要找2个整数,刚才的方法已经不够用了。因为把数组所有元素进行异或运算,最终只会得到2个整数的异或运算结果。

我来提示你一下吧,你知道分治法吗?

说起分治法,我似乎想到了什么……如果把数组分成两部分,保证每一部分都包含1个出现奇数次的整数,这样就与上一题的情况一样了。

终于想到了!首先把数组元素依次进行异或运算,得到的结果是2个出现了奇数次的整数的异或运算结果,在这个结果中至少有1个二进制位是1。

解法:

把2个出现了奇数次的整数命名为A和B。遍历整个数组,然后依次做异或运算,进行异或运算的最终结果,等同于A和B进行异或运算的结果。在这个结果中,至少会有一个二进制位是1(如果都是0,说明A和B相等,和题目不相符)。

举个例子,给出一个无序数组{4,1,2,2,5,1,4,3},所有元素进行异或运算的结果是00000110B。

选定该结果中值为1的某一位数字,如00000110B的倒数第2位是1,这说明A和B对应的二进制的倒数第2位是不同的。其中必定有一个整数的倒数第2位是0,另一个整数的倒数第2位是1。

根据这个结论,可以把原数组按照二进制的倒数第2位的不同,分成两部分,一部分的倒数第2位是0,另一部分的倒数第2位是1。由于A和B的倒数第2位不同,所以A被分配到其中一部分,B被分配到另一部分,绝不会出现A和B在同一部分,另一部分既没有A,也没有B的情况。

这样一来就简单多了,我们的问题又回归到了上一题的情况,按照原先的异或算法,从每一部分中找出唯一的奇数次整数即可。

假设数组长度是n,那么该解法的时间复杂度是O(n)。把数组分成两部分,并不需要借助额外的存储空间,完全可以在按二进制位分组的同时来做异或运算,所以空间复杂度仍然是O(1)。

没错,就是这个思路。请你按照这个思路来写一下代码。

好的,我来试试!

1.  public  static  int[]  findLostNum(int[]  array)  {

2.  //用于存储2个出现奇数次的整数


3.  int  result[]  =  new  int[2];

4.  //第1次进行整体异或运算

5.  int  xorResult  =  0;

6.  for(int  i=0;i
7.  xorResult^=array[i];

8.  }

9.  //如果进行异或运算的结果为0,则说明输入的数组不符合题目要求

10.  if(xorResult  ==  0){

11.  return  null;

12.  }

13.  //确定2个整数的不同位,以此来做分组

14.  int  separator  =  1;

15.  while  (0==(xorResult&separator)){

16.  separator<<=1;

17.  }

18.  //第2次分组进行异或运算

19.  for(int  i=0;i
20.  if(0==(array[i]&separator)){

21.  result[0]^=array[i];

22.  }else  {

23.  result[1]^=array[i];

24.  }

25.  }

26.

27.  return  result;

28.  }

29.

30.  public  static  void  main(String[]  args)  {

31.  int[]  array  =  {4,1,2,2,5,1,4,3};

32.  int[]  result  =  findLostNum(array);

33.  System.out.println(result[0]  +  ","  +  result[1]);

34.  }

很好,我们的技术面试就到这里。请你稍等一下,我去叫HR来和你谈谈。10min后……



就这样,小灰拿到了职业生涯中的第一个offer,但这并不意味着结束,小灰的程序员之路才刚刚开始。