【问题标题】:How can I quickly tell if a list contains a list?如何快速判断列表是否包含列表?
【发布时间】:2010-11-16 10:04:30
【问题描述】:

有多个相关问题,但我正在寻找针对我的案例的解决方案。有一个(通常)14 个整数的数组,每个整数的范围在 1 到 34 之间。如何快速判断特定静态列表中的每个 int 是否在该数组中至少出现一次?

作为参考,我目前正在使用这段代码,它被编写为尽可能接近规范,因此它当然可以大大改进:

if (array.Count < 13) {
    return;
}

var required = new int[] {
    0*9 + 1,
    0*9 + 9,
    1*9 + 1,
    1*9 + 9,
    2*9 + 1,
    2*9 + 9,                
    3*9 + 1,
    3*9 + 2,
    3*9 + 3,
    3*9 + 4,
    3*9 + 5,
    3*9 + 6,
    3*9 + 7,
};

IsThirteenOrphans = !required.Except (array).Any ();

所需的列表不是动态的,即它在运行时总是相同的。使用 Linq 是可选的,主要方面是性能。

编辑:

  • 输入数组未排序。
  • 输入值可能会出现多次。
  • 输入数组将包含至少 14 个项目,即比所需数组多 1 个。
  • 只有 1 个必需的数组,它是静态的。
  • required 中的值是不同的。
  • 您可能会认为创建直方图的成本很低。

更新:我也对排序输入数组的解决方案感兴趣。

【问题讨论】:

  • 这14个整数是否不同?
  • 我认为您的代码也有错误。如果 required 包含重复但 array 不包含某个 int,它将通过您的测试。
  • @Code 确实,我忘了说required 不包含骗子。
  • 附带说明,如果您在每微秒之后,一个普通的 for 循环比 foreach 更快。
  • 我的算法是 O(n + 34) 而不是 34 * n 你看到了吗,考虑一下?如果没有,我会删除它。

标签: c# .net algorithm list mahjong


【解决方案1】:

想法 1
如果您需要与几个required 列表进行比较,那么您可以对输入列表进行排序,然后通过迭代进行简单的比较。但是排序当然不会太快,但也不会太慢。但是,如果您与几个必需的列表进行比较,排序的开销可能会很快摊销。

一旦数组被排序,比较就很简单了:

for(int i = 0; i < 14; i++)
  if(arr[i] != required[i]) return false;

return true;

想法 2
或者,如果 14 个整数是不同的/唯一的,您可以简单地将 required 设为 HashSet 并执行

input.Count(i => required.Contains(i)) == 14

但如果不进行实际测试,我不知道这是否比排序更快。

想法 3
计算一个在 14 个整数的排列下不变的快速哈希,并将其与 require 的已知值进行比较。仅当哈希匹配时才进行更昂贵的比较。

//Prepare/precalculated
int[] hashes = new int[34];
Random random = new Random();
for(int i = 0; i < 34; i++)
  hashes[i] = random.NextInt();

//On each list
int HashInts(int[] ints)
{
  int result = 0;
  foreach(int i in ints)
    result += hashes[i - 1];

  return result;
}

明智地选择hashes 的值可能会有所改善,但随机值应该没问题。

想法 4
创建直方图:

int[] CreateHistogram(int[] ints)
{
  int[] counts = new int[34];
  foreach(int i in ints)
  {
    counts[i - 1]++;
  }

  return counts;
}

如果性能真的很重要,您可以通过重用现有数组来避免创建数组。

【讨论】:

    【解决方案2】:

    如果你有一个 34+ 位的整数类型和 C 风格的位操作,那么你可以从变量列表中计算出这样的整数 V(如果列表是 v[0]、v[1]、...那么 V 是 (1

    【讨论】:

      【解决方案3】:

      一种可能性是改变您存储数据的方式。由于可能值的范围限制为 1-34,因此您可以存储每个数字的计数,而不是存储数字列表:

      int[] counts = new int[34];
      

      如果您的列表有一个 1 和两个 3,则 counts[0] == 1 && counts[2] = 2(如果这样可以加快处理速度(减少减法),您可以交替使用基于 1 的索引。)

      现在要评估列表中的每个 int 至少出现一次,您只需为每个 x 顺序索引到数组中并验证所有 counts[x] > 0。从计数转换数据将产生相关开销表单到列表表单,但如果您还经常需要查看列表表单中的数据,这只是一个问题。这种存储格式的另一个优点是添加/删除计数永远不会涉及超过单个数组元素。在列表形式中,删除列表中间的元素需要复制多个元素。

      【讨论】:

        【解决方案4】:

        如果你想要快速的方式,你不应该使用 linq,如果给定的列表项都低于 35,你可以删除 if (lst[i] &lt; 35) 下面的答案最多遍历一次列表,行为类似于counting sort

        public bool FindExactMatch(int[] array, List<int> lst)
        {
            bool[] a34 = new bool[35];
        
            foreach(var item in array)
            {
                a34[item] = true;
            }
        
            int exact = 0;
        
            for (int i = 0; i < lst.Count; i++)
            {
                if (a34[lst[i]])
                {
                    exact++;
                    if (exact == array.Length) return true;
        
                    a34[lst[i]] = false;
                }
            }
        
            return false;
        }
        

        对于排序列表,如果列表大小很大,您可以执行lst.BinarySearch(array[i]),最多需要 14 * log(n) * c1,我认为它也足够高效,如果您实现它可能会变得更快,而我没有t 用我自己的实现测试二进制搜索,但是 linq 中的 Min、Max、Sort 比你自己的(好)实现慢(4 到 10 倍)。 如果排序列表的大小很小,我更喜欢使用上面的算法,因为常量c1 在上面的算法中很小,而在二进制搜索算法中可能更大。

        【讨论】:

        • 通过将a34中的true改为false,可以跳过初始化循环。由于所有项目都保证
        • @mafutrct,我想我不能做By changing true to false in a34,因为我使用它的默认false值,并且当相关位置有项目时初始化数组a34的项目,另外我通过您对 的评论更新了答案
        【解决方案5】:

        这看起来很适合按位运算,因为 required 中的值是不同的、静态的,并且介于 1 和 34 之间。与其将 required 保存为数组,不如将其保存为 const ulong。在要检查的数组中,创建一个新的 ulong,由左移每个值和按位或填充。

        const ulong comparator = (1UL << 1) | (1UL << 9) | (1UL << 10) | (1UL << 18) | (1UL << 19) | (1UL << 27) | (1UL << 28) | (1UL << 29) | (1UL << 30) | (1UL << 31) | (1UL << 32) | (1UL << 33) | (1UL << 34);
        
        public static bool ContainsDistinct13Values(int[] array)
        {
            ulong word = 0;
            foreach (int i in array)
            {
                word |= (1UL << i);
            }
            return word == comparator;
        }
        

        【讨论】:

        • 有趣的想法,因为它高度可并行化并且占用空间很小。很抱歉,我目前无法对其进行测试,但你有我的 +1。
        • @mafu 我目前也没有可用的开发环境,或者我会尝试找到更快的实现。需要考虑的一件事:如果要检查的数组的大小永远不会大于某个合理的长度(例如 16),则可以将其设为固定长度,用零填充未使用的索引并展开以提高性能。 en.m.wikipedia.org/wiki/Loop_unrolling
        【解决方案6】:

        编辑: 所以我理解你的问题,并且可能有一些很好的过于复杂的解决方案。另一个问题是,它的性能有多好。

        static void Main(string[] args)
        {
            var required = new int[]
                               {
                                   0*9 + 1,
                                   0*9 + 9,
                                   1*9 + 1,
                                   1*9 + 9,
                                   2*9 + 1,
                                   2*9 + 9,
                                   3*9 + 1,
                                   3*9 + 2,
                                   3*9 + 3,
                                   3*9 + 4,
                                   3*9 + 5,
                                   3*9 + 6,
                                   3*9 + 7,
                               };
        
            precomputed = required.Select((x, i) => new { Value = x, Offset = (UInt16)(1 << i) }).ToDictionary(x => x.Value, x => x.Offset);
        
            for (int i = 0; i < required.Length; i++)
            {
                precomputedResult |= (UInt16)(1 << i);
            }
        
            int[] array = new int[34]; // your array goes here..
            Console.WriteLine(ContainsList(array));
        
            Console.ReadKey();
        }
        
        // precompute dictionary
        private static Dictionary<int, UInt16> precomputed;
        // precomputed result
        private static UInt16 precomputedResult = 0;
        
        public static bool ContainsList(int[] values)
        {
            UInt16 result = 0;
            for (int i = 0; i < values.Length; i++)
            {
                UInt16 v;
                if (precomputed.TryGetValue(values[i], out v))
                    result |= v;
            }
        
            return result == precomputedResult;
        }
        

        【讨论】:

        • 他不需要更大的列表。 14 的列表大小直接遵循麻将规则。
        • 哦。行。那么问题来了。他真的需要担心表现吗?在这种情况下,我会寻求更好的可读性,除非他要重复该功能几百万次。
        • 实际上我要重复这个功能数百万次:)
        【解决方案7】:

        您可以轻松地遍历项目并找出是否缺少任何项目。通过您的示例,我了解您只是想知道数组中是否缺少必需的任何项目。所以你可以写

        bool notContains = false;
        
        foreach (var iddd in required)
        {
            foreach (var ar in array)
            {
                if (iddd == ar) notContains=true;
            }
        
            if (notContains == false) break;
        }
        

        这比你的方法快得多。查看下面添加了计时器的代码。你的方法用了 5 毫秒,但新方法用了 0 毫秒

        System.Diagnostics.Stopwatch aTimer = new System.Diagnostics.Stopwatch();
        aTimer.Start();
        
        var IsThirteenOrphans = !required.Except(array).Any();
        aTimer.Stop();
        
        System.Diagnostics.Stopwatch bTimer = new System.Diagnostics.Stopwatch();
        bTimer.Start();
        bool notContains = false;
        
        foreach (var iddd in required)
        {
            foreach (var ar in array)
            {
                if (iddd == ar) notContains=true;            
            }
        
            if (notContains == false) break;
        }
        
        bTimer.Stop();
        

        【讨论】:

        • 您不会停止 aTimer,而是启动它两次...而且他的代码也会显示 0ms 进行一次迭代。您需要对一百万次迭代进行计时以获得合理的结果。 3秒是不现实的结果,你一定是在测量时犯了大错。
        • @CodeInChaos,我更改了代码以停止计时器。仍然需要更多时间。
        • 是的,我在数组对象中使用的数据是 5。所以经过的时间是 5 毫秒。我测试了 200 个项目,它增加到 10 毫秒,但第二种方法仍然是 0 毫秒。考虑百万个项目,性能肯定会下降。
        • 您的基准测试不是那么好。您只运行一次,这意味着您必须为初始设置、JIT 以及可能的更多费用付费。而对于这么小的事情,需要完成的工作量非常小,你可能需要几十万次尝试才能获得一个有效的数字
        猜你喜欢
        • 2011-05-10
        • 1970-01-01
        • 1970-01-01
        • 2011-12-27
        • 2018-08-10
        • 2011-03-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多