如何在O(n)时间内判断一个数组是否为排列?

40

输入: 一个包含整数值1到N(某些整数值可以出现多次!)的只读长度为N的数组和一个固定大小(10,100,1000等 - 不取决于 N)的内存区域。

如何在O(n)的时间复杂度下确定该数组是否表示排列?

--到目前为止我所获得的(一个答案证明这是不好的):

  1. 我使用有限的内存区域来存储数组的总和和乘积。
  2. 我将总和与N *(N + 1)/ 2进行比较,将乘积与N!进行比较。

我知道如果条件(2)成立,我可能会有一个排列。 我想知道是否有办法证明条件(2)足以告诉我是否有一个排列。 到目前为止,我还没有弄清楚...


3
不,这纯粹是为了好玩。 - INS
3
产品 N! 所需的存储空间,严格来说取决于 N。而且严格来说,在 O(N) 的时间内无法对 N 个数字进行乘法运算。 - polygenelubricants
1
我相信这将是一个解决方案:http://aperiodic.net/phil/archives/Geekery/find-duplicate-elements.html - INS
3
几乎重复:https://dev59.com/63VC5IYBdhLWcg3wz0l9这篇文章讨论如何检查一个数组中是否包含n个相同的元素,其中n是数组长度的一部分。答案通过比较排序后的数组中相邻元素来实现。如果有n个连续的相同元素,则该数组包含n个相同的元素。 - Eric Bainville
@Iulian:你提供的文章并没有解决这个问题:它假设该数组不包含值N。 - interjay
这里是另一个无法解决的原因:假设你已经处理了 m 个数中的 n 个,并停止了你的算法。现在你可以(虽然需要一些时间)通过处理任何一个由 n-m 个数字组成的流并查看何时出现排列来确定你已经看到了哪 m 个数字。因此从信息的角度来看,你已经存储了所有的数字 m,因此必须使用线性内存。 - Thomas Ahle
16个回答

16

我稍微怀疑是否有一个解决方案。你的问题似乎与几年前在数学文献中提出的问题非常接近,其中这里给出了一个摘要(“重复检测问题”,S. Kamal Abdali,2003)使用了循环检测--其思想如下:

如果存在重复项,则存在一个介于1和N之间的数字j,使得以下内容会导致无限循环:

x := j;
do
{
   x := a[x];
}
while (x != j);

因为排列由一个或多个不同元素s0, s1, ... sk-1的子集S组成,其中对于所有1到k-1之间的j,sj=a[sj-1],且s0=a[sk-1],因此所有元素都参与循环--其中一个重复项将不是这样的子集的一部分。
例如,如果数组=[2, 1, 4, 6, 8, 7, 9, 3, 8],则在位置5上加粗的元素是一个重复项,因为所有其他元素形成循环:{ 2 -> 1, 4 -> 6 -> 7 -> 9 -> 8 -> 3}。而数组[2, 1, 4, 6, 5, 7, 9, 3, 8]和[2, 1, 4, 6, 3, 7, 9, 5, 8]是有效的排列(循环分别为{ 2 -> 1, 4 -> 6 -> 7 -> 9 -> 8 -> 3, 5 }和{ 2 -> 1, 4 -> 6 -> 7 -> 9 -> 8 -> 5 -> 3 })。

Abdali使用一种查找重复项的方法。基本上,以下算法(使用Floyd循环查找算法)可以在遇到其中一个重复项时工作:

function is_duplicate(a, N, j)
{
     /* assume we've already scanned the array to make sure all elements
        are integers between 1 and N */
     x1 := j;
     x2 := j;
     do
     {             
         x1 := a[x1];
         x2 := a[x2];
         x2 := a[x2];
     } while (x1 != x2);

     /* stops when it finds a cycle; x2 has gone around it twice, 
        x1 has gone around it once.
        If j is part of that cycle, both will be equal to j. */
     return (x1 != j);
}

困难在于我不确定你所述的问题是否与他的论文中的问题相匹配,而且我也不确定他描述的方法是否以O(N)运行或使用固定数量的空间。一个潜在的反例是以下数组:

[3, 4, 5, 6, 7, 8, 9, 10, ... N-10, N-9, N-8, N-7, N-2, N-5, N-5, N-3, N-5, N-1, N, 1, 2]

这基本上是一个由2位移的恒等置换,其中元素[N-6、N-4和N-2]被替换为[N-2、N-5、N-5]。这具有正确的总和(不是正确的乘积,但我拒绝将乘积作为可能的检测方法,因为使用任意精度算术计算N!的空间要求为O(N),这违反了“固定内存空间”要求的精神),如果您尝试查找循环,您将得到循环{3 -> 5 -> 7 -> 9 -> ... N-7 -> N-5 -> N-1}和{4 -> 6 -> 8 -> ... N-10 -> N-8 -> N-2 -> N -> 2}。问题在于可能有多达N个循环(恒等置换有N个循环),每个循环最多需要O(N)来查找重复项,而且您必须以某种方式跟踪已经跟踪和未跟踪的循环。我怀疑能否在固定的空间量中完成此操作。但也许可以。
这是一个比较棘手的问题,值得在mathoverflow.net上提问(尽管大多数情况下,当stackoverflow引用mathoverflow.net时,这是因为问题太简单)。

编辑:我在mathoverflow上询问,那里有一些有趣的讨论。


这篇论文中的算法需要一个大小为n+1的数组,以便它始终包含至少一个重复项。这与OP提出的问题不同。也许可以改编该算法,但不能直接使用。 - Jules
如果函数 is_duplicate(a, N, j) 应该在 j 是重复项时返回 true,那么它的返回条件不应该是 return (x1 == j) 吗? - dark_prince

10

这是不可能在O(1)空间内完成的,至少需要使用单扫描算法。

证明

假设您已经处理了N/2个元素。 假设序列是一个排列,则给定算法的状态,您应该能够找出剩余的N/2个元素的集合。 如果无法找出剩余的元素,则算法可以通过重复一些旧元素来欺骗它。

有N中N/2种可能的剩余集。 每个剩余集必须由算法的不同内部状态表示,否则您无法找出剩余的元素。 但是,要存储X个状态需要对数空间,因此需要BigTheta(log(N choose N/2))空间来存储N选择N/2个状态。 随着N增长,该值也增长,因此算法的内部状态不能适合于O(1)空间。

更正式的证明

您想创建一个程序P,该程序在处理了N/2个元素后,给定最终的N/2个元素和线性时间常量空间算法的内部状态,确定整个序列是否为1..N的排列。 对此次要求二次程序没有时间或空间限制。

假设P存在,我们可以创建一个程序Q,仅使用线性时间常量空间算法的内部状态,它确定序列的必要最终N/2个元素(如果它是排列)。 Q通过将每个可能的最终N/2个元素传递给P,并返回P返回真的集合来工作。

但是,由于Q具有N选择N/2种可能的输出,因此它必须具有至少N选择N/2个可能的输入。 这意味着原始算法的内部状态必须存储至少N选择N/2个状态,需要BigTheta(log N choose N/2)的空间,这大于恒定的大小。

因此,如果具有恒定大小的内部状态,则原始算法也无法正常工作。

[我认为这个想法可以推广,但思考并不能证明]

后果

BigTheta(log(N choose N/2))等价于BigTheta(N)。因此,仅使用布尔数组并在遇到值时进行标记(可能)是空间和时间上的最优解决方案,因为它需要线性时间。


你为什么能够找出剩下的N/2个元素集合呢?你只需要说你在{1..N}^N集合中的排列集合(最后)中有成员资格即可。 - Rex Kerr
2
你已经证明了这个问题是“不可再分的”,但并没有证明它不能在O(N)时间内解决。你怎么知道不存在一种策略,在列表的N/2位置,你可能仍然需要重新访问列表的早期部分来处理剩下的部分?只要你不那么频繁地这样做,它仍然可以是O(N) - Rex Kerr
@supercat 这很简单。将每个值乘以2,得到n个位标志,如A [0]&1,并运行朴素算法。之后撤销乘法。 - Craig Gidney
@Strilanc 你仍然在假设你的结论。为什么单扫描算法“必须能够确定剩余的元素”?在我给出的求和示例中,这显然不是这种情况,而且你也没有证明为什么对于确定排列而言它是这种情况。 - Nick Johnson
@NickJohnson 不,我并不是在假设。我已经多次陈述了证明。为了使算法能够在单次扫描中识别排列,它必须直接或间接地知道哪些项目仍然存在。在一半的位置上有太多的可能性无法适应亚线性空间。求和示例不同,因为求和所需的空间比排列少(对数级别,假设值是多项式边界,而不是线性的)。 - Craig Gidney
显示剩余18条评论

5

我怀疑你能够证明那件事 ;)


  (1, 2, 4, 4, 4, 5, 7, 9, 9)

我认为更一般地说,这个问题不能通过按顺序处理数字来解决。假设您按顺序处理元素,并且您已经处理了数组的一半。现在,您的程序状态必须反映出到目前为止遇到的数字。这至少需要O(n)位来存储。


谢谢!这个解决方案不符合规则。 - INS
2
这更像是一条评论而不是答案,因为它实际上并没有回答问题。 - Joren
1
我同意,但这也排除了下面一半的“答案”以及提问者采用的方法。因此,我认为它解决了问题的一部分:你不必继续寻找按顺序处理元素来解决它的方法。 - Jules

3
由于复杂度是作为N的函数给出而不是M,所以这样做行不通,这意味着N远大于M。
这是我的尝试,但是为了使布隆过滤器有用,你需要一个很大的M,此时你可能会选择对类似整数的简单位切换进行操作。

http://en.wikipedia.org/wiki/Bloom_filter

对于数组中的每个元素

运行k个哈希函数

检查布隆过滤器中是否包含该元素

如果存在,则有一定概率之前已经看到该元素

如果不存在,则将其添加进去

完成后,你也可以将结果与按顺序排列的1..N数组的结果进行比较,因为这样只需要再花费N。

现在,如果我没有提供足够的警告,那么它并不是100%准确的,甚至不接近,因为你指定了复杂度为N,这意味着N>>M, 因此从根本上讲,这种方法无法按照你的规定工作。

顺便说一下,单个项的误报率应该为

e = 2^(-m/(n*sqrt(2)))

调整它将会给你一个大致的想法,M需要多大才能被接受。


这不是O(n^2)吗?你说“对于每个元素...将其与结果进行比较...这只会让你多花费另外的N”。所以N个元素,每个元素额外花费N,N^2? - samoz
你跳过了“完成后”的部分。最终检查完全是可选的,并且会在循环之后发生。 - McBeth

1

我不知道如何以O(N)的时间复杂度完成它,甚至不确定是否可以用O(N)的时间复杂度完成。如果使用适当的排序和比较,我知道可以在O(N log N)的时间复杂度内完成。

话虽如此,有许多O(N)的技术可以用来证明一个序列不是另一个序列的排列。

  1. 检查长度。如果长度不相等,则显然不是排列。
  2. 创建异或指纹。如果所有元素的值异或在一起的结果不匹配,则不能是排列。但匹配结果可能是不确定的。
  3. 找到所有元素的总和。虽然结果可能会溢出,但在匹配这个“指纹”时不应该担心。但是,如果你做了一个涉及乘法的校验和,那么溢出就成了一个问题。

希望这能帮到你。


1

你可以通过计算sum(x_i)product(x_i)模一堆不同随机选定大小为O(n)的常数C,以随机化的方式在O(n)时间和常量空间内完成此操作。这基本上解决了product(x_i)过大的问题。

然而,仍然存在许多未解决的问题,例如如果sum(x_i)=N(N+1)/2product(x_i)=N!是保证排列的充分条件,以及非排列生成假阳性的几率是多少(我希望每次尝试的C的几率约为1/C,但也许不是)。


0
下面的Java解决方案部分回答了这个问题。我相信时间复杂度是O(n)。(这种信仰是基于解决方案不包含嵌套循环的事实。)关于内存——不确定。这个问题似乎首先出现在谷歌相关请求中,所以它可能对某些人有用。
public static boolean isPermutation(int[] array) {   
    boolean result = true;
    array = removeDuplicates(array);
    int startValue = 1;
    for (int i = 0; i < array.length; i++) {
        if (startValue + i  != array[i]){
            return false;
        }
    }
    return result;
}
public static int[] removeDuplicates(int[] input){
    Arrays.sort(input);
    List<Integer> result = new ArrayList<Integer>();
    int current = input[0];
    boolean found = false;

    for (int i = 0; i < input.length; i++) {
        if (current == input[i] && !found) {
            found = true;
        } else if (current != input[i]) {
            result.add(current);
            current = input[i];
            found = false;
        }
    }
    result.add(current);
    int[] array = new int[result.size()];
    for (int i = 0; i < array.length ; i ++){
        array[i] = result.get(i);
    }
    return array;
}
public static void main (String ... args){
    int[] input = new int[] { 4,2,3,4,1};
    System.out.println(isPermutation(input));
    //output true
    input = new int[] { 4,2,4,1};
    System.out.println(isPermutation(input));
    //output false
}

0

好的,这有些不同,但似乎可以工作!

我运行了这个测试程序(C#):

    static void Main(string[] args) {
        for (int j = 3; j < 100; j++) {
            int x = 0;
            for (int i = 1; i <= j; i++) {
                x ^= i;
            }
            Console.WriteLine("j: " + j + "\tx: " + x + "\tj%4: " + (j % 4));
        }
    }

简短解释:x是单个列表中所有XOR的结果,i是特定列表中的元素,j是列表的大小。由于我所做的只是XOR,因此元素的顺序并不重要。但是当应用此操作时,我正在查看正确排列的外观。
如果您查看j%4,则可以在该值上进行切换并获得类似以下内容的内容:
    bool IsPermutation = false;
    switch (j % 4) {
        case 0:
            IsPermutation = (x == j);
            break;
        case 1:
            IsPermutation = (x == 1);
            break;
        case 2:
            IsPermutation = (x == j + 1);
            break;
        case 3:
            IsPermutation = (x == 0);
            break;
    }

现在我承认这可能需要一些微调。它不是100%的,但这是一个很好的简单方法来开始。也许通过在XOR循环中运行一些小检查,这可以被完善。尝试从那里开始。


谢谢,我会仔细看一下这个。 - INS

0

请查看以下解决方案。它使用了O(1)的额外空间。 在检查过程中,它会更改数组,但在最后将其返回到初始状态。

思路如下:

  1. 检查任何一个元素是否超出范围[1, n] => O(n)。
  2. 按顺序遍历数字(现在所有数字都保证在范围[1, n]内),对于每个数字x(例如3):

    • 转到第x个单元格(例如a [3]),如果它为负数,则有人在你之前已经访问过它=>不是排列方式。否则(a [3]为正数),将其乘以-1。 => O(n)。
  3. 遍历数组并取反所有负数。

这样,我们确信所有元素都在范围[1, n]内,并且没有重复项 => 数组是一个排列。

int is_permutation_linear(int a[], int n) {
    int i, is_permutation = 1;

    // Step 1.
    for (i = 0; i < n; ++i) {
        if (a[i] < 1 || a[i] > n) {
            return 0;
        }
    }

    // Step 2.
    for (i = 0; i < n; ++i) {
        if (a[abs(a[i]) - 1] < 0) {
            is_permutation = 0;
            break;
        }
        a[i] *= -1;
    }

    // Step 3.
    for (i = 0; i < n; ++i) {
        if (a[i] < 0) {
            a[i] *= -1;
        }
    }

    return is_permutation;
}

这是完整的测试程序:

/*
 * is_permutation_linear.c
 *
 *  Created on: Dec 27, 2011
 *      Author: Anis
 */

#include <stdio.h>

int abs(int x) {
    return x >= 0 ? x : -x;
}

int is_permutation_linear(int a[], int n) {
    int i, is_permutation = 1;

    for (i = 0; i < n; ++i) {
        if (a[i] < 1 || a[i] > n) {
            return 0;
        }
    }

    for (i = 0; i < n; ++i) {
        if (a[abs(a[i]) - 1] < 0) {
            is_permutation = 0;
            break;
        }
        a[abs(a[i]) - 1] *= -1;
    }

    for (i = 0; i < n; ++i) {
        if (a[i] < 0) {
            a[i] *= -1;
        }
    }

    return is_permutation;
}

void print_array(int a[], int n) {
    int i;
    for (i = 0; i < n; i++) {
        printf("%2d ", a[i]);
    }
}

int main() {
    int arrays[9][8] = { { 1, 2, 3, 4, 5, 6, 7, 8 },
                         { 8, 6, 7, 2, 5, 4, 1, 3 },
                         { 0, 1, 2, 3, 4, 5, 6, 7 },
                         { 1, 1, 2, 3, 4, 5, 6, 7 },
                         { 8, 7, 6, 5, 4, 3, 2, 1 },
                         { 3, 5, 1, 6, 8, 4, 7, 2 },
                         { 8, 3, 2, 1, 4, 5, 6, 7 },
                         { 1, 1, 1, 1, 1, 1, 1, 1 },
                         { 1, 8, 4, 2, 1, 3, 5, 6 } };
    int i;

    for (i = 0; i < 9; i++) {
        printf("array: ");
        print_array(arrays[i], 8);
        printf("is %spermutation.\n",
               is_permutation_linear(arrays[i], 8) ? "" : "not ");
        printf("after: ");
        print_array(arrays[i], 8);
        printf("\n\n");

    }

    return 0;
}

并且它的输出为:

array:  1  2  3  4  5  6  7  8 is permutation.
after:  1  2  3  4  5  6  7  8 

array:  8  6  7  2  5  4  1  3 is permutation.
after:  8  6  7  2  5  4  1  3 

array:  0  1  2  3  4  5  6  7 is not permutation.
after:  0  1  2  3  4  5  6  7 

array:  1  1  2  3  4  5  6  7 is not permutation.
after:  1  1  2  3  4  5  6  7 

array:  8  7  6  5  4  3  2  1 is permutation.
after:  8  7  6  5  4  3  2  1 

array:  3  5  1  6  8  4  7  2 is permutation.
after:  3  5  1  6  8  4  7  2 

array:  8  3  2  1  4  5  6  7 is permutation.
after:  8  3  2  1  4  5  6  7 

array:  1  1  1  1  1  1  1  1 is not permutation.
after:  1  1  1  1  1  1  1  1 

array:  1  8  4  2  1  3  5  6 is not permutation.
after:  1  8  4  2  1  3  5  6 

OP特别提到数组是只读的,你不应该对其进行修改。 - dark_prince

0

如果数组中没有重复的值,那么它就是一个排列,可以很容易地在O(N)时间内检查。


在满足上述限制的情况下,我该如何以O(n)的时间复杂度实现呢?:) - INS
抱歉,我错过了空间限制。 - Chris Card

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接