【发布时间】:2011-08-09 23:43:50
【问题描述】:
输入:给定一个包含 n 个元素的数组,其中包含从 0 到 n-1 的元素,其中任何一个数字出现任意次数。
目标:在 O(n) 中找到这些重复的数字,并且只使用恒定的内存空间。
例如,设 n 为 7,数组为 {1, 2, 3, 1, 3, 0, 6},答案应为 1 & 3。
我在这里检查了类似的问题,但答案使用了一些数据结构,如HashSet 等。
任何有效的算法相同?
【问题讨论】:
输入:给定一个包含 n 个元素的数组,其中包含从 0 到 n-1 的元素,其中任何一个数字出现任意次数。
目标:在 O(n) 中找到这些重复的数字,并且只使用恒定的内存空间。
例如,设 n 为 7,数组为 {1, 2, 3, 1, 3, 0, 6},答案应为 1 & 3。
我在这里检查了类似的问题,但答案使用了一些数据结构,如HashSet 等。
任何有效的算法相同?
【问题讨论】:
我们可以做到 O(n) 时间和 O(1) 空间复杂度 -
取第 i 个数组元素。
如果它是负数,让它 +ve
最后,将 -1 乘以从数组索引(第 i 个元素)获得的数字。
如果数字为正,则返回索引。
def findDuplicate(self, arr: List[int]) -> int:
n=len(arr)
for i in range(0,n):
arr[(abs(arr[i]))-1]=arr[(abs(arr[i]))-1]*(-1)
if arr[(abs(arr[i]))-1]>0:
return abs(arr[i])
【讨论】:
在这里查看解释https://youtu.be/qJ_Y7pKP0e4
代码在这里https://github.com/TechieExpress/DataStructures/blob/main/findDuplicates
代码sn-p:
/**
*
* @author techieExpress
*
* You are given a list of n-1 integers and these integers are in the range * of 1 to n.
* Input: Given an array of n elements which contains elements
* from 0 to n-1, with any of these numbers appearing any number of times.
*
* Goal: To find these repeating numbers in O(n) and using only constant * * memory space.
**/
public class findDuplicates {
public static void main(String args[])
{
int arr[] = { 2,1,1,2 };
for (int i = 0; i < arr.length; i++) {
arr[arr[i] % arr.length]
= arr[arr[i] % arr.length]
+ arr.length;
}
System.out.println("The repeating elements are : ");
for (int i = 0; i < arr.length; i++) {
//System.out.print(numRay[i]);
if (arr[i] >= arr.length * 2) {
System.out.println(i + " ");
arr[i]=arr[i]%arr.length;
}
}
}
}
【讨论】:
int arr[] = { 2,1,1,2 };矛盾
private static void printRepeating(int arr[], int size) {
int i = 0;
int j = 1;
while (i < (size - 1)) {
if (arr[i] == arr[j]) {
System.out.println(arr[i] + " repeated at index " + j);
j = size;
}
j++;
if (j >= (size - 1)) {
i++;
j = i + 1;
}
}
}
【讨论】:
这是我想出来的,不需要额外的符号位:
for i := 0 to n - 1
while A[A[i]] != A[i]
swap(A[i], A[A[i]])
end while
end for
for i := 0 to n - 1
if A[i] != i then
print A[i]
end if
end for
第一个循环对数组进行置换,以便如果元素 x 至少出现一次,则其中一个条目将位于位置 A[x]。
请注意,乍一看它可能看起来不像 O(n),但它确实是 - 尽管它有一个嵌套循环,但它仍然在 O(N) 时间运行。仅当存在i 使得A[i] != i 时才会发生交换,并且每个交换设置至少一个元素使得A[i] == i,以前不是这样。这意味着交换的总数(以及 while 循环体的执行总数)最多为 N-1。
第二个循环打印x 的值,其中A[x] 不等于x - 因为第一个循环保证如果x 在数组中至少存在一次,其中一个实例将是在A[x],这意味着它会打印数组中不存在的x 的值。
【讨论】:
a[a[i]],而 O(1) 空间约束暗示 swap() 操作是键。
5 不在0..N-1 范围内(在这种情况下N 是5)。
print 语句更改为print i 会将其变成stackoverflow.com/questions/5249985/… 的解决方案,并且(假设“bag”是一个可修改的数组)stackoverflow.com/questions/3492302/… 的Qk。
这是伪代码
for i <- 0 to n-1:
if (A[abs(A[i])]) >= 0 :
(A[abs(A[i])]) = -(A[abs(A[i])])
else
print i
end for
【讨论】:
-替换为~来解决零问题。
O(n) 隐藏空间 - n 符号位。如果数组被定义为每个元素只能保存0 和n-1 之间的值,那么它显然不起作用。
假设我们将此数组表示为单向图数据结构 - 每个数字都是一个顶点,它在数组中的索引指向另一个顶点,形成图的一条边。
为了更简单,我们有从 0 到 n-1 的索引和从 0..n-1 的数字范围。 例如
0 1 2 3 4
a[3, 2, 4, 3, 1]
0(3) --> 3(3) 是一个循环。
Answer: 只需根据索引遍历数组即可。如果 a[x] = a[y] 那么它是一个循环,因此是重复的。跳到下一个索引并再次继续,依此类推,直到数组结束。 复杂度:O(n) 时间和 O(1) 空间。
【讨论】:
array = [1, 0]: element s 0 和 1 循环,但不重复。您可以推断,如果您使用这种遍历方法并到达一个循环,那么循环 before 的最后一个元素是重复的,例如:array = [1, 2, 3, 4, 2]。这会产生一些新问题。如何在不使用额外内存和时间的情况下检测循环。
array = [1, 2, ...., n - 1, 0, 0](0 值的单个副本)。遍历每个元素的周期将花费 O(n) 时间,因此总共将是 O(n^2) 时间。
O(n)时间和O(1)空间),但这无助于提供解决方案。
a[x] = a[y] 其中 x 和 y 是两个索引(两个不同的速度)?
static void findrepeat()
{
int[] arr = new int[7] {0,2,1,0,0,4,4};
for (int i = 0; i < arr.Length; i++)
{
if (i != arr[i])
{
if (arr[i] == arr[arr[i]])
{
Console.WriteLine(arr[i] + "!!!");
}
int t = arr[i];
arr[i] = arr[arr[i]];
arr[t] = t;
}
}
for (int j = 0; j < arr.Length; j++)
{
Console.Write(arr[j] + " ");
}
Console.WriteLine();
for (int j = 0; j < arr.Length; j++)
{
if (j == arr[j])
{
arr[j] = 1;
}
else
{
arr[arr[j]]++;
arr[j] = 0;
}
}
for (int j = 0; j < arr.Length; j++)
{
Console.Write(arr[j] + " ");
}
Console.WriteLine();
}
【讨论】:
C 中的一个解决方案是:
#include <stdio.h>
int finddup(int *arr,int len)
{
int i;
printf("Duplicate Elements ::");
for(i = 0; i < len; i++)
{
if(arr[abs(arr[i])] > 0)
arr[abs(arr[i])] = -arr[abs(arr[i])];
else if(arr[abs(arr[i])] == 0)
{
arr[abs(arr[i])] = - len ;
}
else
printf("%d ", abs(arr[i]));
}
}
int main()
{
int arr1[]={0,1,1,2,2,0,2,0,0,5};
finddup(arr1,sizeof(arr1)/sizeof(arr1[0]));
return 0;
}
时间复杂度为 O(n),空间复杂度为 O(1)。
【讨论】:
如果数组不是太大,这个解决方案更简单, 它创建了另一个相同大小的数组以供滴答。
1 创建一个与输入数组大小相同的位图/数组
int check_list[SIZE_OF_INPUT];
for(n elements in checklist)
check_list[i]=0; //initialize to zero
2 扫描您的输入数组并增加其在上述数组中的计数
for(i=0;i<n;i++) // every element in input array
{
check_list[a[i]]++; //increment its count
}
3 现在扫描 check_list 数组并打印重复的一次或多次重复
for(i=0;i<n;i++)
{
if(check_list[i]>1) // appeared as duplicate
{
printf(" ",i);
}
}
当然它占用的空间是上面给出的解决方案的两倍,但时间效率是O(2n),基本上是O(n)。
【讨论】:
在以下 C 函数中可以很容易地看到算法。检索原始数组虽然不是必需的,但可以将每个条目模n。
void print_repeats(unsigned a[], unsigned n)
{
unsigned i, _2n = 2*n;
for(i = 0; i < n; ++i) if(a[a[i] % n] < _2n) a[a[i] % n] += n;
for(i = 0; i < n; ++i) if(a[i] >= _2n) printf("%u ", i);
putchar('\n');
}
【讨论】:
不是很漂亮,但至少很容易看到 O(N) 和 O(1) 属性。基本上我们扫描数组,并且对于每个数字,我们查看对应的位置是否已被标记为已经看过一次 (N) 或已经多次看过 (N+1)。如果它被标记为已经看过一次,我们打印它并标记它已经看过多次。如果没有标记,我们标记它已经见过一次,并将相应索引的原始值移动到当前位置(标记是破坏性操作)。
for (i=0; i<a.length; i++) {
value = a[i];
if (value >= N)
continue;
if (a[value] == N) {
a[value] = N+1;
print value;
} else if (a[value] < N) {
if (value > i)
a[i--] = a[value];
a[value] = N;
}
}
或者,更好(更快,尽管有双循环):
for (i=0; i<a.length; i++) {
value = a[i];
while (value < N) {
if (a[value] == N) {
a[value] = N+1;
print value;
value = N;
} else if (a[value] < N) {
newvalue = value > i ? a[value] : N;
a[value] = N;
value = newvalue;
}
}
}
【讨论】:
if (value > i) a[i--] = a[value]; 工作的确切原因需要一些思考:如果value <= i 那么我们已经处理了a[value] 的值并且可以覆盖它安全。我也不会说 O(N) 的性质是显而易见的!拼写出来:主循环运行N 次,加上a[i--] = a[value]; 行运行多次。该行仅在a[value] < N 时才能运行,并且每次运行时,紧接着将尚未N 的数组值设置为N,因此它最多可以运行N 次,总共最多2N 循环迭代。
caf's brilliant answer 打印在数组 k-1 中出现 k 次的每个数字。这是有用的行为,但可以说,这个问题要求每个副本只打印一次,他暗示这样做的可能性不会超出线性时间/恒定空间界限。这可以通过用以下伪代码替换他的第二个循环来完成:
for (i = 0; i < N; ++i) {
if (A[i] != i && A[A[i]] == A[i]) {
print A[i];
A[A[i]] = i;
}
}
这利用了在第一个循环运行后,如果任何值m 多次出现,则其中一个出现在正确的位置,即A[m]。如果我们小心的话,我们可以使用该“家”位置来存储有关是否已打印任何副本的信息。
在 caf 的版本中,当我们遍历数组时,A[i] != i 暗示 A[i] 是重复的。在我的版本中,我依赖于一个稍微不同的不变量:A[i] != i && A[A[i]] == A[i] 意味着A[i] 是一个重复的我们以前没有见过。 (如果你去掉“我们以前没见过的”部分,其余部分可以看出是由 caf 不变量的真相所暗示的,并且保证所有重复项在一个主位置都有一些副本。)这个属性成立于一开始(在 caf 的第一个循环完成之后),我在下面显示它在每个步骤之后都得到维护。
当我们遍历数组时,测试的A[i] != i 部分的成功意味着A[i] 可能是以前从未见过的重复。如果我们以前没有见过它,那么我们期望A[i] 的主位置指向它自己——这就是if 条件的后半部分所测试的。如果是这种情况,我们将其打印出来并更改主位置以指向第一个找到的副本,从而创建一个两步“循环”。
要看到这个操作不会改变我们的不变量,假设m = A[i] 对特定位置i 满足A[i] != i && A[A[i]] == A[i]。很明显,我们所做的更改 (A[A[i]] = i) 将通过导致if 条件的第二半失败,来防止m 的其他非家庭事件被输出为重复项,但是当@ 时它会起作用吗? 987654339@到家,m?是的,因为现在,即使在这个新的i 处,我们发现if 条件的前半部分A[i] != i 为真,后半部分测试它指向的位置是否是家庭位置,并且发现不是。在这种情况下,我们不再知道 m 或 A[m] 是否是重复值,但我们知道无论哪种方式,已经报告过,因为保证不会出现这两个循环在 caf 的第一个循环的结果中。 (请注意,如果m != A[m],那么m 和A[m] 中恰好有一个出现了不止一次,而另一个根本不出现。)
【讨论】:
对于相对较小的 N 我们可以使用 div/mod 操作
n.times do |i|
e = a[i]%n
a[e] += n
end
n.times do |i|
count = a[i]/n
puts i if count > 1
end
不是 C/C++,但无论如何
【讨论】: