在O(n)中查找列表中包含所有成员的最大间隔

时间:2013-07-11 10:52:53

标签: algorithm logic

我在接受采访时被问到这个问题。给定一个整数列表,我们怎样才能找到给定列表中所有成员的最大间隔?

E.g。给出列表1,3,5,7,4,6​​,10然后回答是[3,7]。因为它具有3到7之间的所有元素。

我试着回答,但我没有说服力。我采取的方法是先对列表进行排序,然后检查最大间隔。但我被要求在O(n) 中这样做。

10 个答案:

答案 0 :(得分:35)

我知道基于散列和动态编程的解决方案。设 f(x) 为哈希函数。诀窍是哈希表值。考虑列表中包含的 最长间隔,该间隔以x 开头或结尾。然后 h [ f(x)] = y ,其中 y 该间隔的另一端 即可。请注意,该间隔的长度为 abs( x - y )+ 1 。算法描述将清楚说明存储该值的原因。

移动列表。让 i 成为当前索引, x := list [ i ] - 当前数。现在

1。如果 h [ f(x)] 不为空,那么我们之前就遇到了数字x。无所事事,继续。

2. 检查 h [ f(x-1)] h [ f(x + 1) ) 即可。

2.1。如果它们都不为空,则表示我们已经遇到 x-1 x + 1 ,我们知道一些间隔 [ a..x-1 ] [ x +我们已在列表中遇到的1..b ] 。我们知道这是因为 a = h [ f(x-1)] b = h根据 h 的定义,[ f(x + 1)] 。现在当我们得到 x 时,这意味着我们现在已经满足了整个时间间隔 [ a,b ] ,所以我们按如下方式更新值: h [ f(a)]:= b h [ f(b )]:= a
同时将 h [ f(x)] 设置为某个值(假设 x ,不影响答案) ),以便下次我们在列表中遇到 x 时,我们会忽略它。 x 已经完成了他的工作。

2.2。如果只设置其中一个,请说 h [ f(x-1)] = a ,这意味着我们已经遇到了一些区间 [ a..x-1 ] ,现在它已经扩展为 x < / EM> 即可。更新将是 h [ f(a)]:= x h [ f(x)]:= a

2.3。如果没有设置,则表示我们既未遇到 x-1 ,也未遇到 x + 1 ,我们已经遇到的包含 x 的最大间隔是单个 [ x < / em>] 本身。所以设置 h [ f(x)]:= x

最后,为了得到答案,请传递整个列表并取 最大 abs( x - h [ f(x) x 的em>])+ 1

答案 1 :(得分:8)

如果不希望排序,可以使用哈希映射和Disjoint-set data structure的组合。

对于列表中的每个元素,创建一个节点并使用key = element的值将其插入到哈希映射中。然后查询哈希映射的值+ 1和值-1。如果找到任何内容,请将当前节点与相邻节点所属的集合组合。完成列表后,最大集合对应最大间隔。

时间复杂度为O(N *α(N))其中α(N)为逆Ackermann函数。

编辑:实际上,Disjoint-set对于这个简单的任务来说太强大了。 Grigor Gevorgyan的解决方案不使用它。所以它更简单,更有效。

答案 2 :(得分:5)

你可以用空间来换取线性时间。

  1. 扫描列表中的最小值和最大值S和L.
  2. 使用布尔数组或位向量A,大到足以容纳(L - S + 1)条目。
  3. 再次浏览列表,在看到它时将A的相应元素设置为true。
  4. 现在,A已排序。通过A找到最大的连续真值集。
  5. 列表中的第一步是线性的。最后一个是A的大小是线性的,如果你只有一些相距很远的值,它可能相对于你的列表很大。但是,既然你正在处理整数,那么A就是有限的。

答案 3 :(得分:3)

1个想法:好吧,我认为无论如何你必须对列表进行排序,但你不能使用合并或快速排序。但是如果你有记忆,你可以使用counting sort中的想法来表示整数。

所以你可以创建0和1的数组,从0到最大int值,如果你有值,则用1填充它,然后找到最大的连续数组

2想法:创建值词典,找到最小值和最大值 - 所有O(N)操作:

dict = {1: 1, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 10: 10}
min = 1
max = 10
然后,转到i in range(min, max)并找到最长的连续子集

>>> d = [1, 3, 5, 7, 4, 6, 10]
>>> s = set(d)
>>> mind = min(d)
>>> maxd = max(d)
>>> a, b, j = 0, 0, 0

>>> for i in range(mind, maxd):
        if i not in s:
            if (b - a) < (i - j - 1):
                a, b = j, i - 1
            j = i + 1

>>> a, b
(3, 7)

但对于像[1, 9000, 100000]

这样的稀疏列表来说这可能会很慢

编辑:基于Grigor Gevorgyan的超级答案,这里是Python中O(N)字典解决方案的代码(我只是喜欢它简单!!!)

l = [1, 3, 5, 7, 4, 6, 10]
d = {x:None for x in l}
print d
for (k, v) in d.iteritems():
    if v is not None: continue
    a, b = d.get(k - 1), d.get(k + 1)
    if a is not None and b is not None: d[k], d[a], d[b] = k, b, a
    elif a is not None: d[a], d[k] = k, a
    elif b is not None: d[b], d[k] = k, b
    else: d[k] = k
    print d

m = max(d, key=lambda x: d[x] - x)
print m, d[m]

输出:

{1: None, 3: None, 4: None, 5: None, 6: None, 7: None, 10: None}
{1: 1, 3: None, 4: None, 5: None, 6: None, 7: None, 10: None}
{1: 1, 3: 3, 4: None, 5: None, 6: None, 7: None, 10: None}
{1: 1, 3: 4, 4: 3, 5: None, 6: None, 7: None, 10: None}
{1: 1, 3: 5, 4: 3, 5: 3, 6: None, 7: None, 10: None}
{1: 1, 3: 6, 4: 3, 5: 3, 6: 3, 7: None, 10: None}
{1: 1, 3: 7, 4: 3, 5: 3, 6: 3, 7: 3, 10: None}
{1: 1, 3: 7, 4: 3, 5: 3, 6: 3, 7: 3, 10: 10}
3 7

答案 4 :(得分:2)

我使用HashSet制作了一个非常简单的解决方案。由于containsremove是O(1)操作,您可以简单地从随机设置项创建一个新间隔,然后“展开”它的间隔,直到您发现其完整大小,从集合中删除项目当你走的时候。删除是关键,因为这是阻止您“重复”任何间隔的原因。

这可能有助于以这种方式思考 - 列表具有K个区间,其大小加起来为N.然后,您的任务是发现这些区间是什么,而不重复任何间隔或项目。这就是HashSet非常适合这项工作的原因 - 您可以在扩展间隔时有效地从集合中删除项目。然后,您需要做的就是跟踪最大间隔。

  1. 将列表放入HashSet
  2. 虽然该集合非空:
    1. 从集合中随机删除项目
    2. 从该项目定义新的间隔
    3. 按如下方式展开间隔:
      1. 定义i = interval.start-1
      2. 虽然该集合包含i,但请从集合中删除i并减少iinterval.start
      3. 向另一个方向重复步骤2(从interval.end向上扩展)
    4. 如果扩展间隔大于先前最大间隔,则将新间隔记录为最大间隔
  3. 返回最大间隔
  4. 以下是Java中的解决方案:

    public class BiggestInterval {
    
        static class Interval {
            int start;
            int end;
    
            public Interval(int base) {
                this(base,base);
            }
    
            public Interval(int start, int end) {
                this.start = start;
                this.end = end;
            }
    
            public int size() {
                return 1 + end - start;
            }
    
            @Override
            public String toString() {
                return "[" + start + "," + end + "]";
            }
        }
    
        /**
         * @param args
         */
        public static void main(String[] args) {
            System.out.println(biggestInterval(Arrays.asList(1,3,5,7,4,6,10)));
        }
    
        public static Interval biggestInterval(List<Integer> list) {
            HashSet<Integer> set = new HashSet<Integer>(list);
            Interval largest = null;
    
            while(set.size() > 0) {
                Integer item = set.iterator().next();
                set.remove(item);
    
                Interval interval = new Interval(item);
                while(set.remove(interval.start-1)) {
                    interval.start--;
                }
                while(set.remove(interval.end+1)) {
                    interval.end++;
                }
    
                if (largest == null || interval.size() > largest.size()) {
                    largest = interval;
                }
            }
    
            return largest;
        }
    }
    

答案 5 :(得分:1)

考虑到使用平均O(1)哈希表构建的字典,这将是线性的。

L = [1,3,5,7,4,6,10]

a_to_b = {}
b_to_a = {}

for i in L:
    if i+1 in a_to_b and i-1 in b_to_a:
        new_a = b_to_a[i-1]
        new_b = a_to_b[i+1]
        a_to_b[new_a] = new_b
        b_to_a[new_b] = new_a
        continue
    if i+1 in a_to_b:
        a_to_b[i] = a_to_b[i+1]
        b_to_a[a_to_b[i]] = i
    if i-1 in b_to_a:
        b_to_a[i] = b_to_a[i-1]
        a_to_b[b_to_a[i]] = i
    if not (i+1 in a_to_b or i-1 in b_to_a):
        a_to_b[i] = i
        b_to_a[i] = i

max_a_b = max_a = max_b = 0
for a,b in a_to_b.iteritems():
    if b-a > max_a_b:
        max_a = a
        max_b = b
        max_a_b = b-a

print max_a, max_b  

答案 6 :(得分:1)

这是一个类似于Grigor的解决方案。两个主要区别是此解决方案存储顺序集的长度而不是其他索引,这消除了对最后一个散列集迭代的需要。

  1. 迭代数组

    • 通过查找和更新相邻的设置端点来构建哈希映射:

      - 数组值

      - 当密钥是顺序集的端点时,存储该集的长度。否则,保持它真实,所以你只考虑一次。

    • 如果当前设置的大小最长,请更新最长设置大小和最长设置开始。

  2. 为清晰起见,这是一个JavaScript实现,以及fiddle以查看它的实际效果:

    var array = [1,3,5,7,4,6,10];
    
    //Make a hash of the numbers - O(n) assuming O(1) insertion
    var longestSetStart;
    var longestSetSize = 0;
    
    var objArray = {};
    for(var i = 0; i < array.length; i++){
        var num = array[i];
    
        if(!objArray[num]){//Only consider numbers once
            objArray[num] = 1;//Initialize to 1 item in the set by default
    
            //Get the updated start and end of the current set
            var currentSetStart = num;//Starting index of the current set
            var currentSetEnd = num;//Ending index of the current set
    
            //Get the updated start of the set
            var leftSetSize = objArray[num - 1];
            if(leftSetSize){
                currentSetStart = num - leftSetSize;
            }
    
            //Get the updated end of the set
            var rightSetSize = objArray[num + 1];
            if(rightSetSize){
                currentSetEnd = num + rightSetSize;
            }
    
            //Update the endpoints
            var currentSetSize = currentSetEnd - currentSetStart + 1;
            objArray[currentSetStart] = currentSetSize;
            objArray[currentSetEnd] = currentSetSize;
    
            //Update if longest set
            if(currentSetSize > longestSetSize){
                longestSetSize = currentSetSize;
                longestSetStart = currentSetStart;
            }
        }
    }
    
    var longestSetEnd = longestSetStart + longestSetSize - 1;
    

答案 7 :(得分:0)

诀窍是将项目视为一组而不是列表。这允许您识别位于连续范围的开头或结尾的项目,因为一个集合允许您检查是否存在item-1或item + 1。这样,您就可以在线性时间和空间中解决问题。

<强>伪码:

  • 枚举集合中的项目,查找范围开头的项目(当x-1不在集合中时,x开始范围)。
  • 对于作为范围起点的每个值,向上扫描直到找到相应的范围结束值(x在x + 1不在集合中时结束范围)。这为您提供了所有相关的连续范围。
  • 返回距其开头最远的连续范围。

C#代码:

static Tuple<int, int> FindLargestContiguousRange(this IEnumerable<int> items) {
    var itemSet = new HashSet<int>(items);

    // find contiguous ranges by identifying their starts and scanning for ends
    var ranges = from item in itemSet

                 // is the item at the start of a contiguous range?
                 where !itemSet.Contains(item-1)

                 // find the end by scanning upward as long as we stay in the set
                 let end = Enumerable.Range(item, itemSet.Count)
                           .TakeWhile(itemSet.Contains)
                           .Last()

                 // represent the contiguous range as a tuple
                 select Tuple.Create(item, end);

     // return the widest contiguous range that was found
     return ranges.MaxBy(e => e.Item2 - e.Item1);
}

注意:MaxBy来自MoreLinq

<强>测试

小理智检查:

new[] {3,6,4,1,8,5}.FindLargestContiguousRange().Dump();
// prints (3, 6)

大连续列表:

var zeroToTenMillion = Enumerable.Range(0, (int)Math.Pow(10, 7)+1);
zeroToTenMillion.FindLargestContiguousRange().Dump();
// prints (0, 10000000) after ~1 seconds

大碎片列表:

var tenMillionEvens = Enumerable.Range(0, (int)Math.Pow(10, 7)).Select(e => e*2);
var evensWithAFewOdds = tenMillionEvens.Concat(new[] {501, 503, 505});
evensWithAFewOdds.FindLargestContiguousRange().Dump();
// prints (500, 506) after ~3 seconds

<强>复杂性

该算法需要O(N)时间和O(N)空间,其中N是列表中项目的数量,假设设置操作是恒定时间。

请注意,如果将集合作为输入,而不是由算法构建,我们只需要O(1)空间。

(有些评论说这是二次时间。我认为他们假设所有项目,而不仅仅是范围开始时的项目,触发扫描。如果算法以这种方式工作,那确实是二次的。)

答案 8 :(得分:-1)

我想我会将它们分类为连续整数列表(假设每个数字只能出现一次)

取第一个号码

如果数字1比现有列表中的数字低1或高1?

是:预先/后发现有列表

否:创建一个以当前号码

开头的新列表

如果有更多数字,请返回顶部

显示最长的列表

答案 9 :(得分:-1)

免责声明:由于该解决方案基于哈希表,因此可以预期运行时间,而非最坏情况。

这个O(n)解决方案取决于唯一的整数。如果它们不是唯一的,请使用O(1)插入和成员资格查找创建一个哈希集,并在浏览列表时跳过已遇到的数字。

  1. 创建一个O(1)查找/插入散列图,其中值是范围的开头,键是适合这些范围末尾的数字。对于值v和密钥k,这意味着从v开始并以k-1结尾的范围位于密钥k处。

  2. 浏览数字列表。对于每个数字n,检查地图在关键字n处是否具有值v。这对应于存在从v开始的范围,其将允许结尾处的n。如果有,请将v移至键n + 1并删除键n处的条目。如果没有任何范围,请在键n + 1处插入n。

  3. 由于数字是唯一的,因此最后没有一个范围重叠,但可能存在一些连续的范围。运行地图的键/值对。对于每个密钥k和值v,如果映射在密钥k1 = v处具有值v1,则意味着存在从v1到k-1的范围。在k处插入v1,并删除条目k1 / v1。

  4. 浏览地图的k / v条目,找到大小为k-v的最大范围[v,k-1],使用最大值。

  5. 对于你的例子:

    setup:
    l = [1,3,5,7,4,6,10]
    m = {}
    
    iteration:
    process 1  : m = {2->1}
    process 3  : m = {2->1, 4->3}
    process 5  : m = {2->1, 4->3, 6->5}
    process 7  : m = {2->1, 4->3, 6->5, 8->7}
    process 4  : m = {2->1, 5->3, 6->5, 8->7}
    process 6  : m = {2->1, 5->3, 7->5, 8->7}
    process 10 : m = {2->1, 5->3, 7->5, 8->7, 11->10}
    
    concatenation of contiguous ranges:
    initial:              m = {2->1, 5->3, 7->5, 8->7, 11->10}
    first concatenation:  m = {2->1,       7->3, 8->7, 11->10}, k=7, v=5, k1=5, v1=3
    second concatenation: m = {2->1,             8->3, 11->10}, k=8, v=7, k1=7, v1=3
    
    result:
    largest range : [3,7] of size 5