题目:1-1000放在含有1001个元素的数组中,只有唯一的一个元素值重复,其它均只出现一次。每个数组元素只能访问一次,设计一个算法,将它找出来;不用辅助存储空间,能否设计一个算法实现?
一、有序情况
有1001个数,按照从小到大的顺序排列,其中只有两个数是重复的。当然,因为有序,装两个数也必定是挨着的。
题目的要求就是用最少的内存开销、最少的时间开销找出这个重复的数字
在 莫贝特 的 博客中,他的算法是将(1001个数字的和)- (1000个数字的和)= 重复数字,这种算法思路简单,但是时间复杂度是最高的,共进行了2000次循环,至于留言中的其他算法也基本上属于线性查找,线性查找的时间复杂度是 O(N),对于这道题目,运气最好的时候第一次就能找到,运气最差的时候,要找1000次,平均要找500次。
static void Main(string[] args) { int[] list = new int[1001]; for (int i = 1; i < 1001; i++) { list[i - 1] = i; } Random random = new Random(); list[1000] = random.Next(1, 1000); int sum1 = 0; int sum2 = 0; foreach (int i in list) { sum1 = sum1 + i; } for (int i = 1; i < 1001; i++) { sum2 = sum2 + i; } Console.WriteLine("重复的数字是:"+(sum1 - sum2).ToString()); Console.Read(); }
然而,如果我们考虑到这是一个有序数列,而且只有两个重复数,二分折半查找法就一跃而出了,对数据结构和算法有了解的人都知道,二分查找法是有序数列最好的查找算法。
具体思路是:
- left=0
- right=10001
- 如果 left 和 right 中间那个位置 pos 的数值等于位置编号 pos,则:
- left=pos,否则:
- right=pos
- 如果(right-left>1),则重复步骤 3)-6),直到 right-left=1, pos 就是要查找的数
1000个数只需查找10次,一万个数13次,十万个数17次,一百万个数20次,时间复杂度 O(logN),速度很快。
#!/usr/bin/python # -*- coding: GBK -*- import random # 生成数列,一万个数 arr_length=100001 list=range(1,arr_length) #插入一个随机数 rnd=random.randrange(1,arr_length) list.insert(rnd-1,rnd) print '\n重复数为:'+ str(rnd) print'\n开始查找' # 正式开始查找算法 left=0 right=arr_length count=0 while ((right-left)>1): pos=(left+right)/2 count=count+1 print '第 %d 次定位:%d' % (count,pos) if (list[pos]==pos): right=pos else: left=pos print '\n查找结果:',list[left]
注:先附上原作者的Python,找个时间再转为java
二、无序情况
上面的算法是:(1001个数字的和)- (1000个数字的和)= (1001个数字的和)- n*(n+1)/2= 重复数字,求和需要1000次循环。
这个算法之所以能成立,是因为序列是由1-1000的连续数字组成的,因此,不管他们怎么排列,(1000个数字的和)都是从1加到1000。但是,如果这1000个数字不是连续的,比如是从1-100000中随机选出来的互不相同的1000个数,那么这个算法就不成立了,因为我们无法知道(1000个数字的和)到底是哪1000个数字。
既然现在给出的条件是1-1000的乱序,那么就可以利用连续这一特殊性来改进算法。
最简单的方法应该是:再开辟一个1000空间的数组 list2 ,将这1001个数字从第一个开始,依次放到 list2 中他的编号位置上
比如:
list2[list[0]-1]=list[0]
list2[list[1]-1]=list[1]
依次这样做下去,直到某个要保存的数字在 list2 对应的位置上已经存在,就找到了这个数字。
但是这次我看清要求了,不能开辟新的空间。
虽然不能开辟新的空间,思路却已经有了,就是利用连续性和索引的对应关系,具体算法描述如下:
1、取出第一个位置的数字:p1=list[0]
2、将第一个位置标记:list[0]=0
3、取出 p1 位置的数字:p2=list[p1]
4、将 p1 位置标记:list[p1]=0
5、取出 p2 位置的数字:p3=list[p2]
6、将 p2 位置标记:list[p2]=0
.............
依次做下去,直到下一个要找的位置被标记过了,数字就找到了。
原因是:那个重复的数字,最终要访问他自己的位置第二次,当他访问时,发现位置已经被上一个自己访问过了,他就知道自己不是唯一的了。
这种算法叫什么名字我也不知道,能不能叫漫游算法?
在最好的情况下,他的循环次数是 1,比如[1,1,....1000]
在最坏的情况下,他的循环次数是 1000,比如[1,2,3......1000,1000]
平均情况下,他的循环次数是 N/2,虽说数量级上仍然是 O(N)级别的,但是平均来说,仍然比前面的求和法快了一倍
#!/usr/bin/python # -*- coding: GBK -*- import random # 生成数列 arr_length=10001 list=range(1,arr_length) #加入一个随机数 rnd=random.randrange(1,arr_length) list.append(rnd) print '\n重复数为:'+ str(rnd) #打乱顺序 random.shuffle(list) # 正式开始查找算法 count=0 idx=0 while (list[idx]!=0): count=count +1 temp=list[idx] list[idx]=0 idx=temp print "找到:%d,循环 %d 次" % (temp,count)
注:先附上原作者的Python,找个时间再转为java