第一个缺失的整数
给定一个长度为N的数组A[0,…N-1],从1开始,找到第一个不在数组中的正整数。
如 [3,9,8,1,6,32]输出为2。
思路:
求解该问题可以有三种策略,分别是暴力破解法、BitMap法和循环不变式方法。
方法1—暴力破解法:
看到这个题后,既然是找从1开始第一个不在数组A中的正整数,那最简单最直接的反应就是我能否可以从1开始,在A中查找这个数是否存在,若存在则找下一个正整数,知道找到第一个不在A中的正整数。可以很明显可知,该算法的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2),空间复杂度为
O
(
1
)
O(1)
O(1),显然这不是我们想要的结果。
那么,我们进一步想,如果A是有序的话,则从头开始查找,找到第一个A[i]!=i(为简单讲解,假设数组的下标是从1开始,下同)的位置,则i即为要找的数字。因为给定的数组A并不有序,则需要首先对A进行排序,然后在对A进行遍历即可。则很明显,该算法时间复杂度主要花在对A的排序上,即使利用时间复杂度最低的排序算法,其时间复杂度仍为
O
(
n
∗
l
o
g
n
)
O(n*logn)
O(n∗logn),空间复杂度为
O
(
1
)
O(1)
O(1).
由上可知,暴力破解法的时间复杂度太高。
方法2—BitMap法:
所谓的BitMap就是用一个bit位来标记某个元素所对应的value,而key即是该元素。由于题目中并没有对算法的空间复杂度进行要求,所以我们可以进行以下操作:
首先申请一个与
A
A
A长度一样的空数组
B
=
[
1
,
.
.
.
,
N
]
B=[1,...,N]
B=[1,...,N] (假设数组
B
B
B下标是从
1
1
1开始),并将所有位置初始化为
−
1
-1
−1。然后从头遍历
A
A
A,将
A
[
i
]
A[i]
A[i]映射到
B
B
B中,具体映射方式为如果
A
[
i
]
<
1
A[i]<1
A[i]<1 或者
A
[
i
]
>
N
A[i]>N
A[i]>N则舍弃,否则
B
[
A
[
i
]
]
=
A
[
i
]
B[A[i]]=A[i]
B[A[i]]=A[i]。映射完成后,从
1
1
1开始遍历数组
B
B
B,找到第一个
B
[
i
]
!
=
i
B[i]!=i
B[i]!=i的位置,
i
i
i即为所求。
然而这是为什么呢?为什么可以这样做?
这是因为题目是从1开始找到第一个不在数组中的正整数,因为一个萝卜一个坑,只要A数组中存在一个数大于N或者小于1,那么可以很容易得出这个缺失的正整数
z
z
z一定满足
z
>
=
1
并
且
z
<
=
N
z>= 1 并且 z<=N
z>=1并且z<=N
或者如果
A
A
A数组中的数都介于
1
1
1和
N
N
N之间且互不重复,则z必等于
N
+
1
N+1
N+1
所以,上述映射和查找方式是成立的。
BitMap法花费的总时间为
2
N
2N
2N,空间大小为
N
N
N,因此BitMap算法的时间复杂度为
O
(
n
)
O(n)
O(n),空间复杂度也为
O
(
n
)
O(n)
O(n)。
方法3—循环不变式法:
因为BitMap的时间复杂度已经是
O
(
n
)
O(n)
O(n)数量级,对于此类查找算法已经不可能有数量级上的优化,即使优化也只是降低系数。但是BitMap算法的空间复杂度也是
O
(
n
)
O(n)
O(n),那这个空间复杂度是否可以降低呢?
其实,归根到底,上述BitMap方法只是在做一件事,那就是将每一个数字都放到它应该在的位置上去。既然是这样,我们其实可以不用申请空间,而直接对原数组进行操作。即从前向后遍历数组A,将A中的每一个数放到对的位置,知道某一个位置的数据一直没有被找到,那么这个位置上的数据即为所求,上面这句话比较不好理解,那我们形式化表示一下:
假定前
i
−
1
i-1
i−1个数已经找到,并且依次存放在
A
[
1
,
2
,
.
.
.
.
.
,
i
−
1
]
A[1,2,.....,i-1]
A[1,2,.....,i−1]中,继续考察A[i],有三种情况:
(1) 若
A
[
i
]
A[i]
A[i]<i 且 A
[
i
]
>
=
1
[i]>=1
[i]>=1,则A[i]在A[1,2,…,i-1]中已经出现过,可以直接丢弃。
ps:若A[i]为负,则更应该丢弃它。
(2)若A[i]>i且A[i]<=N,则A[i]应该位于后面的位置上,则将A[A[i]]和A[i]交换。
ps:若A[i]>=N,由于缺失数据一定小于N,则丢弃A[i]。
(3)若A[i]=i,则A[i]位于正确的位置上,则i加1,循环不变式扩大,继续比较后面的元素。
对上面描述进行整理可得:
(1) 若A[i]<i 或者A[i]>N,则丢弃A[i]
(2) 若A[i]>i,则将A[A[i]]和A[i]交换。
(3) 若A[i]=i,则i加1,继续比较后面的元素。
至此,整个的算法描述已经清楚了。
那么现在有个问题,即如何对A[i]进行丢弃呢?
倒是可以直接多数组中这个位置进行删除,然后后面的所有元素往前移动,不过这样子也太费时间了。
我们可以参考堆排序或者树的剪枝操作,直接用最后一个元素A[A.size()-1]去替换A[i],然后A的长度减1。这样子就相当于对A[i]进行了丢弃,并且也不需要进行大量的其它操作,就是进行一个逻辑删除。
好了,至此整个算法的思路已经讲清楚了,下面附上循环不变式解法的python代码,供参考。
# -*- coding: utf-8 -*-
"""
Created on Sun Jan 20 15:04:13 2019
@author: shuaifeng
"""
def findFirstMissingInteger(tableA):
if tableA is None or len(tableA)==0:
return 1
endindex=len(tableA)
index=0
while index<endindex:
if tableA[index]>endindex or tableA[index]<index+1: #第一种情况,直接丢弃
tableA[index]=tableA[endindex-1]
endindex=endindex-1
elif tableA[index]>index+1: #将数值放到正确的位置上去
temp=tableA[tableA[index]-1]
tableA[tableA[index]-1]=tableA[index]
tableA[index]=temp
else: #当前位置的数据在正确位置,则查找下一个
index=index+1
return index+1
def main():
A=[5,1,1,9,4,10]
print(findFirstMissingInteger(A))
if __name__=="__main__":
main()