1 题目一 找出数组中重复的数字
1.1 描述
在一个长度为n的数组里的所有数字都在0到n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组 {2, 3, 1, 0, 2, 5, 3},那么对应的输出是重复的数字2或者3。
1.2 题解
方法一
将输入的数组排序。从排序的数组中找出重0的数字,只需从头到尾扫描排序后的数组。时间复杂度O(nlogn)。
方法二
利用哈希表,从头到尾按顺序扫描数组的每个数组,每扫描到一个数字的时候,都可以用 O(1)的时间来判断哈希表里是否已经包含了这个数字。如果没有,则加入哈希表。若已存在,就找到重复数字。时间复杂度 O(n),提高时间效率的代价是一个空间复杂度为 O(n)的哈希表。
方法三
有没有时间复杂度是O(n),空间复杂度为O(1)的方法呢?
我们注意到数组长度为 n,且数字范围为 0~n-1,若没有重复的数字,则数组排序后数字i将出现在下标为i的位置,值和下标刚好一一对应。若重复,则有些下标对应的位置存在多个一样的数字。
让我们重新在排列这个数组,从头到尾依次扫描每个数字。当扫到下标为 i 的数字,首先判断这个数字(m)是否等于 i。如果是,则扫描下一个数字。若不是,则再拿它和下标为 m 的数字比较,相等则找到一个重复的数字 (该数字在下标为 i 和 m 的位置都出现了),若不等则交换两者位置。使得数字 m 对应下标 m。接着继续重复这个过程,直到找到重复数字为止。
例如有个数组:{2, 3, 1, 0, 2, 4}
—> {1,3,2,0,2,4}
—> {3,1,2,0,2,4}
—> {0,1,2,3,2,4} 发现重复数字。
1.3 代码
C++
/*
参数:
numbers: 一个整数数组
length: 数组的长度
duplication: (输出) 数组中的一个重复的数字
返回值:
true - 输入有效,并且数组中存在重复的数字
false - 输入无效,或者数组中没有重复的数字
*/
bool duplicate(int numbers[], int length, int* duplication)
{
// 数组不能为空,长度必须大于0
if (numbers == nullptr || length <= 0)
return false;
// 确保每个数字范围在 0~n-1
for (int i = 0; i < length; i++)
{
if (numbers[i] < 0 || numbers[i] > length - 1)
return false;
}
for (int i = 0; i < length; ++i)
{
while (numbers[i] != i)
{
// 若相等则发现重复数字,返回 ture
if (numbers[i] == numbers[numbers[i]])
{
*duplication = numbers[i];
printf("%d", *duplication);
return true;
}
// 不等则交换 numbers[i] 和 numbers[numbers[i]]
int temp = numbers[i];
numbers[i] = numbers[temp];
numbers[temp] = temp;
}
}
return false;
}
Python
class Solution:
def duplicate(self, numbers, duplication):
if numbers == None or len(numbers) <=0:
return False
for i in numbers:
if i > len(numbers) - 1 or i < 0:
return False
for i in range(len(numbers)):
while numbers[i] != i:
if numbers[i] == numbers[numbers[i]]:
duplication.append(numbers[i])
print(duplication[0])
return True
else:
index = numbers[i]
numbers[i], numbers[index] = numbers[index], numbers[i]
return False
s = Solution()
array = [1, 2, 3, 3, 4, 4]
duplication = []
s.duplicate(array, duplication)
代码中虽然有个两重循环,但每个数字最多只要交换两次就能找属于它自己的位置。因此中的时间复杂度为 O(n),且空间复杂度为 O(1)。
2 题目二 不修改数组找出重复的数字
2.1 描述
在一个长度为 n+1 的数组里的所有数字都在 1 到 n 的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,如果输入长度为8的数组{2, 3, 5, 4, 3, 2, 6, 7},那么对应的输出是重复的数字2或者3。
2.2 题解
方法一
由于不能修改输入数组,我们可以创建一个长度为 n+1 的辅助数组,逐一把原数组的数字复制到辅助数组。如果原数组中的数字是 m,则把它复制到数组中下标为 m 的位置。持续执行则会发现重复数字,但这个需要 O(n) 的辅助空间。
方法二
接下来我们避免使用 O(n) 的辅助空间。假设没有重复数字,那 1~n 的范围内最多只有 n 个数字,而题目却有 n+1 个数字,所以一定存在重复数字。所以某范围内数字的个数对解决这个问题很重要。
原数组的数字范围为 1~n,我们用中间大小 m 把原数组按大小范围分为两部分,前一半的大小范围为 1~m,后一半的范围为 m+1~n。如果范围 1~m 的数字个数超过 m 个,则当中一定存在重复数字,否则后一半存在重复数字。我们继续把包含重复数字的范围一分为二,直到找到一个重复的数字。
2.3 代码
C++
// 参数:
// numbers: 一个整数数组
// length: 数组的长度
// 返回值:
// 正数 - 输入有效,并且数组中存在重复的数字,返回值为重复的数字
// 负数 - 输入无效,或者数组中没有重复的数字
int getDuplication(const int* numbers, int length)
{
if (numbers == nullptr || length <= 0)
return -1;
// 数组长度 n + 1, 数字范围 1 ~ n
int start = 1;
int end = length - 1;
while (end >= start)
{
int middle = ((end - start) >> 1) + start; // 不易越界,(end + start)/2 容易越界
int count = countRange(numbers, length, start, middle);
if (end == start) // 当只剩一个数字
{
if (count > 1) // 且这个数字在数组中出现的次数大于1
return start; // 返回此重复的数字
else
break; // 否则跳出循环,数组内无重复数字
}
if (count > (middle - start + 1)) // 如果前半部分范围内数字出现的次数大于范围
end = middle; // 说明此部分有重复数字,接着在前半范围搜索
else
start = middle + 1; // 否则后半部分有重复数字,接着在后半范围搜索
}
return -1;
}
// 统计给定数组给定范围内数字的个数
int countRange(const int* numbers, int length, int start, int end)
{
if (numbers == nullptr)
return 0;
int count = 0;
for (int i = 0; i < length; i++)
if (numbers[i] >= start && numbers[i] <= end)
++count;
return count;
}
Python
class Solution():
def getDuplicaton(self, numbers):
if numbers == None or len(numbers) <= 0:
return 'valid numbers'
start = 1
end = len(numbers) - 1
while(end >= start):
middle = ((end - start) >> 1) + start
count = self.countRange(numbers, start, middle)
if end == start:
if count > 1:
return start
else:
break
if count > (middle - start + 1):
end = middle;
else:
start = middle + 1
return 'no duplication'
def countRange(self, numbers, start, end):
if numbers == None:
return 0
count = 0
for i in range(len(numbers)):
if numbers[i] >= start and numbers[i] <= end:
count += 1
return count
s = Solution()
a = s.getDuplicaton([])
print(a)
上述代码按照二分查找的思路,长度为 n 的数组,countRange 将被调用 O(logn) 次,每次需要 O(n) 的时间,总的时间复杂度为 O(nlogn),空间复杂度为 O(1)。和方法一相比,相当于以时间换空间。
此方法并不能保证找出所有重复的数字。例如,数组{2,2}在范围 1~2 中的数字出现的次数也是2,并不会认为有重复数字。所以要根据题目的特性来写代码。