最长的子阵列,其元素形成连续序列

时间:2013-04-12 08:03:42

标签: algorithm

给定未排序的正整数数组,找到最长子数组的长度,其中排序后的元素是连续的。你能想到一个O(n)解决方案吗?

示例:

{10,5,3,1,4,2,8,7},答案是5.

{4,5,1,5,7,6,8,4,1},答案是5。

对于第一个例子,子阵列{5,3,1,4,2}在排序时可以形成连续序列1,2,3,4,5,这是最长的。

对于第二个例子,子阵列{5,7,6,8,4}是结果子阵列。

我可以想到一个方法,对于每个子阵列,检查(最大值 - 最小值+ 1)是否等于该子阵列的长度,如果为真,那么它是一个连续的子阵列。花费最长的。但它是O(n ^ 2)并且不能处理重复。

有人可以提供更好的方法吗?

7 个答案:

答案 0 :(得分:2)

解决O(n)中没有重复的原始问题的算法。也许,它可以帮助某人开发处理重复的O(n)解决方案。

输入:[a1,a2,a3,...]

将原始数组映射为第一个元素为值的对,第二个为数组的索引。

数组:[[a1,i1],[a2,i2],[a3,i3],...]

使用一些O(n)算法(例如Counting Sort)对这个对数组进行排序,以便按值整数排序。 我们得到另一个数组:

数组:[[a3,i3],[a2,i2],[a1,i1],...]

其中a3,a2,a1,...按排序顺序。

通过已排序的数组

运行循环

在线性时间内,我们可以检测连续的数字组a3,a2,a1。连续组定义是下一个值= prev值+ 1。 在该扫描期间,保持当前组大小( n ),索引的最小值( min )和当前索引总和( actualSum )。< / p>

在连续组内的每一步,我们可以估计指数的总和,因为它们创建了第一个元素 min ,步骤 1 的算术级数,以及到目前为止看到的组的大小<强>名词即可。 该总和估计可以使用算术级数公式在O(1)时间内完成:

估计sum =(a1 + an)* n / 2;

估计sum =(min + min +(n - 1))* n / 2;

估计sum = min * n + n *(n - 1)/ 2;

如果在某个循环步骤内连续组估计和等于实际总和,则看到目前为止连续组满足条件。将 n 保存为当前最大结果,或在当前最大值和 n 之间选择最大值。

如果在值元素上我们停止看到连续组,则重置所有值并执行相同操作。

代码示例:https://gist.github.com/mishadoff/5371821

答案 1 :(得分:1)

UPD2:以下解决方案是针对不需要子阵列连续的问题。我误解了问题陈述。不要删除这个,因为有人可能会根据我的想法来解决实际问题。


以下是我的想法:

创建字典实例(实现为哈希表,在正常情况下给出O(1))。键是整数,值是整数的散列集(也是O(1)) - var D = new Dictionary<int, HashSet<int>>

遍历数组A并为索引为n的每个整数i执行:

  1. 检查n-1中是否包含密钥n+1D
    • 如果两个密钥都不存在,请执行D.Add(n, new HashSet<int>)
    • 如果只存在其中一个密钥,例如n-1D.Add(n, D[n-1])
    • 如果两个密钥都存在,请执行D[n-1].UnionWith(D[n+1]); D[n+1] = D[n] = D[n-1];
  2. D[n].Add(n)
  3. 现在浏览D中的每个密钥,找到长度最大的哈希集(查找长度为O(1))。最长的答案就是答案。

    根据我的理解,最坏情况复杂度将为O(n * log(n)),这仅仅是因为UnionWith操作。我不知道如何计算平均复杂度,但它应该接近O(n)。如果我错了,请纠正我。

    UPD:要说代码,这里是C#中的测试实现,它在两个OP的示例中都给出了正确的结果:

    var A = new int[] {4, 5, 1, 5, 7, 6, 8, 4, 1};
    var D = new Dictionary<int, HashSet<int>>();
    
    foreach(int n in A)
    {
        if(D.ContainsKey(n-1) && D.ContainsKey(n+1))
        {
            D[n-1].UnionWith(D[n+1]);
            D[n+1] = D[n] = D[n-1];
        }
        else if(D.ContainsKey(n-1))
        {
            D[n] = D[n-1];
        }
        else if(D.ContainsKey(n+1))
        {
            D[n] = D[n+1];
        }
        else if(!D.ContainsKey(n))
        {
            D.Add(n, new HashSet<int>());
        }
    
        D[n].Add(n);
    }
    
    int result = int.MinValue;
    foreach(HashSet<int> H in D.Values)
    {
        if(H.Count > result)
        {
            result = H.Count;
        }
    }
    
    Console.WriteLine(result);
    

答案 2 :(得分:1)

在数学集定义中查看数组 S

  

S = U j = 0 k I j )< / p>

I j 是不相交的整数段。您可以设计一个特定的间隔树(基于红黑树或您喜欢的自平衡树:))将数组存储在此数学定义中。节点和树结构应如下所示:

struct node {
    int d, u;
    int count;
    struct node *n_left, *n_right;
}

这里,d是整数段的较小界限,u是上界。添加count以处理数组中可能存在的重复:当尝试在树中插入已存在的元素时,我们将增加它所在节点的count值找到了。

struct root {
    struct node *root;
}        

树只存储不相交的节点,因此,插入比传统的红黑树插入要复杂一些。插入间隔时,必须扫描已存在间隔的潜在溢出。在你的情况下,因为你只会插入单例,所以不应该增加太多的开销。

给定三个节点P,L和R,L是P的左子,R是P的右子。然后,你必须强制执行L.u&lt; P.d和P.u&lt; R.d(对于每个节点,d&lt; = u,当然)。

插入整数段[x,y]时,必须找到“重叠”段,也就是说,满足以下不等式之一的区间[u,d]:

  

y&gt; = d - 1
  OR
  x&lt; = u + 1

如果插入的间隔是单个x,那么您最多只能找到2个重叠间隔节点N1和N2,例如N1.d == x + 1N2.u == x - 1。然后你必须合并两个间隔并更新计数,这样你就可以得到{3},N3.d = N2.dN3.u = N1.u的N3。由于N3.count = N1.count + N2.count + 1N1.d之间的差值是两个细分不相交的最小增量,因此必须具有以下之一:

  • N1是N2的合适孩子
  • N2是N1的左子女

因此,在最坏的情况下插入仍然会在N2.u

从这里开始,我无法弄清楚如何处理初始序列中的顺序,但这里的结果可能很有趣:如果输入数组定义了完美整数段,那么树只有一个节点。

答案 3 :(得分:1)

这需要对数据进行两次传递。首先创建一个哈希映射,将int映射到bools。我更新了我的算法,不使用STL中的map,我很乐意在内部使用排序。该算法使用散列,可以轻松更新任何最大或最小组合,甚至可能是整数可以获得的所有可能值。

#include <iostream>

using namespace std;
const int MINIMUM = 0;
const int MAXIMUM = 100;
const unsigned int ARRAY_SIZE = MAXIMUM - MINIMUM;

int main() {

bool* hashOfIntegers = new bool[ARRAY_SIZE];
//const int someArrayOfIntegers[] = {10, 9, 8, 6, 5, 3, 1, 4, 2, 8, 7};
//const int someArrayOfIntegers[] = {10, 6, 5, 3, 1, 4, 2, 8, 7};
const int someArrayOfIntegers[] = {-2, -3, 8, 6, 12, 14,  4, 0, 16, 18, 20};
const int SIZE_OF_ARRAY = 11;

//Initialize hashOfIntegers values to false, probably unnecessary but good practice.
for(unsigned int i = 0; i < ARRAY_SIZE; i++) {
    hashOfIntegers[i] = false;
}

//Chage appropriate values to true.
for(int i = 0; i < SIZE_OF_ARRAY; i++) {
    //We subtract the MINIMUM value to normalize the MINIMUM value to a zero index for negative numbers.
    hashOfIntegers[someArrayOfIntegers[i] - MINIMUM] = true;
}

int sequence = 0;
int maxSequence = 0;
//Find the maximum sequence in the values
for(unsigned int i = 0; i < ARRAY_SIZE; i++) {

    if(hashOfIntegers[i]) sequence++;
    else sequence = 0;

    if(sequence > maxSequence) maxSequence = sequence;
}

cout << "MAX SEQUENCE: " << maxSequence << endl;
return 0;
}

基本思想是将哈希映射用作存储桶排序,这样您只需要对数据进行两次传递。该算法是O(2n),其又是O(n)

答案 4 :(得分:0)

不要抱有希望,这只是部分答案。

我非常有信心O(n)无法解决问题。不幸的是,我无法证明这一点。

如果有一种方法可以在不到O(n^2)的时间内解决问题,我怀疑解决方案是基于以下策略:

  1. 决定是否O(n)(或者O(n log n))是否存在存在连续子数组,因为您使用至少i元素进行描述。让我们称之为谓词E(i)
  2. 使用二分法查找i保留的最大E(i)
  3. 此算法的总运行时间为O(n log n)(或O(n log^2 n))。

    这是我能够提出将问题简化为另一个问题的唯一方法,这个问题至少比原始公式更简单。但是,我找不到在E(i)以内计算O(n^2)的方法,所以我可能完全不在...

答案 5 :(得分:0)

这是另一种思考问题的方法:假设你有一个仅由1和0组成的数组,你想要找到最长的连续1s运行。这可以通过行程编码1来在线性时间内完成(忽略0)。为了将您的原始问题转换为这个新的游程长度编码问题,您计算一个新数组b [i] =(a [i]&lt; a [i + 1])。这不必明确地完成,您可以隐式地实现它,以实现具有恒定内存要求和线性复杂度的算法。

答案 6 :(得分:-1)

以下是3种可接受的解决方案:

第一个是时间O(nlog(n))O(n)空格,第二个是时间O(n),空间O(n),第三个是O(n) in时间和O(1)在太空中。

  1. 构建binary search tree然后遍历in order 保留2个指针,一个用于最大子集的开始,一个用于结束。 迭代树时保持max_size值。 这是O(n*log(n))时间和空间的复杂性。

  2. 您始终可以在线性时间内使用counting sort对设置的数字进行排序 并遍历数组,这意味着O(n)时间和空间 复杂性。

  3. 假设没有溢出或大整数数据类型。假设数组是一个数学集(没有重复值)。你可以在O(1)内存中执行此操作:

    • 计算数组和数组乘积的总和
    • 假设您拥有原始集合的最小值和最大值,弄清楚您拥有的数字。完全是O(n)时间复杂度。