【问题标题】:N-way intersection of sorted enumerables已排序可枚举的 N 路交集
【发布时间】:2009-12-14 06:19:09
【问题描述】:

给定 n 个相同类型的枚举,它们按升序返回不同的元素,例如:

IEnumerable<char> s1 = "adhjlstxyz";
IEnumerable<char> s2 = "bdeijmnpsz";
IEnumerable<char> s3 = "dejlnopsvw";

我想有效地找到所有可枚举元素的所有值:

IEnumerable<char> sx = Intersect(new[] { s1, s2, s3 });

Debug.Assert(sx.SequenceEqual("djs"));

这里的“高效”是指

  1. 输入的可枚举项每个只能枚举一次,
  2. 应仅在需要时检索输入枚举的元素,并且
  3. 算法不应递归枚举其自身的输出。

我需要一些提示如何解决。


到目前为止,这是我的(天真的)尝试:

static IEnumerable<T> Intersect<T>(IEnumerable<T>[] enums)
{
    return enums[0].Intersect(
        enums.Length == 2 ? enums[1] : Intersect(enums.Skip(1).ToArray()));
}

Enumerable.Intersect 将第一个可枚举对象收集到一个 HashSet 中,然后枚举第二个可枚举对象并产生所有匹配的元素。 Intersect 然后递归地将结果与下一个可枚举相交。 这显然不是很有效(它不符合约束)。而且它根本没有利用元素已排序的事实。


这是我尝试将两个枚举相交。也许它可以推广到 n 枚举?

static IEnumerable<T> Intersect<T>(IEnumerable<T> first, IEnumerable<T> second)
{
    using (var left = first.GetEnumerator())
    using (var right = second.GetEnumerator())
    {
        var leftHasNext = left.MoveNext();
        var rightHasNext = right.MoveNext();

        var comparer = Comparer<T>.Default;

        while (leftHasNext && rightHasNext)
        {
            switch (Math.Sign(comparer.Compare(left.Current, right.Current)))
            {
            case -1:
                leftHasNext = left.MoveNext();
                break;
            case 0:
                yield return left.Current;
                leftHasNext = left.MoveNext();
                rightHasNext = right.MoveNext();
                break;
            case 1:
                rightHasNext = right.MoveNext();
                break;
            }
        }
    }
}

【问题讨论】:

  • 重新评论;为什么你需要将它与其他任何东西结合起来?它似乎“按原样”完成工作?
  • 重新“聚合”(评论) - 不完全;如果您使用 Empty() 作为种子,您的答案将始终为空...但除了初始条件之外 - 差不多!

标签: c# linq .net-3.5


【解决方案1】:

好的;更复杂的答案:

public static IEnumerable<T> Intersect<T>(params IEnumerable<T>[] enums) {
    return Intersect<T>(null, enums);
}
public static IEnumerable<T> Intersect<T>(IComparer<T> comparer, params IEnumerable<T>[] enums) {
    if(enums == null) throw new ArgumentNullException("enums");
    if(enums.Length == 0) return Enumerable.Empty<T>();
    if(enums.Length == 1) return enums[0];
    if(comparer == null) comparer = Comparer<T>.Default;
    return IntersectImpl(comparer, enums);
}
public static IEnumerable<T> IntersectImpl<T>(IComparer<T> comparer, IEnumerable<T>[] enums) {
    IEnumerator<T>[] iters = new IEnumerator<T>[enums.Length];
    try {
        // create iterators and move as far as the first item
        for (int i = 0; i < enums.Length; i++) {
            if(!(iters[i] = enums[i].GetEnumerator()).MoveNext()) {
                yield break; // no data for one of the iterators
            }
        }
        bool first = true;
        T lastValue = default(T);
        do { // get the next item from the first sequence
            T value = iters[0].Current;
            if (!first && comparer.Compare(value, lastValue) == 0) continue; // dup in first source
            bool allTrue = true;
            for (int i = 1; i < iters.Length; i++) {
                var iter = iters[i];
                // if any sequence isn't there yet, progress it; if any sequence
                // ends, we're all done
                while (comparer.Compare(iter.Current, value) < 0) {
                    if (!iter.MoveNext()) goto alldone; // nasty, but
                }
                // if any sequence is now **past** value, then short-circuit
                if (comparer.Compare(iter.Current, value) > 0) {
                    allTrue = false;
                    break;
                }
            }
            // so all sequences have this value
            if (allTrue) yield return value;
            first = false;
            lastValue = value;
        } while (iters[0].MoveNext());
    alldone:
        ;
    } finally { // clean up all iterators
        for (int i = 0; i < iters.Length; i++) {
            if (iters[i] != null) {
                try { iters[i].Dispose(); }
                catch { }
            }
        }
    }
}

【讨论】:

  • 太棒了。谢谢!有趣的是,对于 n=2,我的第二次尝试比此解决方案更快,但对于任何 n!=0,此解决方案比我的第二次尝试链接更快。任何涉及 Enumerable.Intersect 的解决方案都比两者都慢。
  • 对这个算法的复杂度有粗略的估计吗?我很想说它是O(n0+n1+..nn),但我有一种感觉是错误的......
  • 它从不倒带;您可能会争辩说它是 O(m * min(n[1],n[2],...n[m])), (m = 序列数,每个序列的长度为 n[i]);因为它只运行到 any 序列用尽,并以相同的速率迭代所有序列直到那时。
  • 好的,谢谢。看起来问题无法比这更有效地解决,复杂性方面:-)
【解决方案2】:

您可以使用 LINQ:

    public static IEnumerable<T> Intersect<T>(IEnumerable<IEnumerable<T>> enums) {
        using (var iter = enums.GetEnumerator()) {
            IEnumerable<T> result;
            if (iter.MoveNext()) {
                result = iter.Current;
                while (iter.MoveNext()) {
                    result = result.Intersect(iter.Current);
                }
            } else {
                result = Enumerable.Empty<T>();
            }
            return result;
        }
    }

这将是简单,尽管它确实多次构建哈希集;一次推进所有 n(以利用 sorted)会很困难,但您也可以构建单个哈希集并删除丢失的东西?

【讨论】:

  • 我正在寻找一个不那么简单的解决方案 :-) 问题基本上是:我如何处理一个同时推进所有 n 的解决方案(以利用 sorted)。
  • 你的第二个版本,结合我的第二次尝试,看起来还不错。我会喝杯咖啡,试着理解它为什么会起作用。
  • 哦。这基本上是enums.Aggregate(Enumerable.Empty&lt;T&gt;(), Enumerable.Intersect)(如果枚举为非空,则以小优化为模)。
  • 你是对的。然后是enums.DefaultIfEmpty(Enumerable.Empty&lt;T&gt;()).Aggregate(Enumerable.Intersect); :-)
猜你喜欢
  • 2010-11-11
  • 2011-08-29
  • 1970-01-01
  • 2015-05-02
  • 2020-03-03
  • 1970-01-01
  • 2014-07-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多