Что бы Big O был вложенным циклом с Any() внутри него?

Эти вопросы в основном следуют из моего ответа здесь. Я действительно хотел сказать, что такое Big-O этого алгоритма, но я не был уверен, что моя претензия была полностью звуковой.

Итак, заданы два массива:

B = [ "Hello World!", "Hello Stack Overflow!", "Foo Bar!", "Food is nice...", "Hej" ]
A = [ "World", "Foo" ]

что такое Big O of:

List<string> results = new List<string>();
foreach (string test in B)
{
   if (A.Any(a => test.Contains(a))
      results.Add(test);
}

Я считаю, что он находится где-то между O(n) и O(n^2), поскольку он зависит от того, где результат Any() соответствует...

Ответы

Ответ 1

Пусть длина A be N и длина B be M. У нас есть два экстремальных случая:

  • Худший:

    a => test.Contains(a) 
    

    возвращает false на каждом A, поэтому A.Any должен сканировать весь A, и мы имеем

    O(N * M) 
    
  • Самый лучший:

    a => test.Contains(a) 
    

    возвращает true в самом первом элементе A и, следовательно, A.Any возвращается немедленно, и мы имеем только

    O(M)
    

Фактическая сложность находится где-то посередине (включая обе границы):

    [O(M)..O(M*N)]

Ответ 2

Он закрывается, однако, как говорят другие, это будет O(n*m), поскольку размеры каждой из ваших коллекций различаются (при лучшем случае O(n) и худшем O(n*m)), в противном случае, если бы вы повторяли то же самое дважды, вы получите O(n^2).

Взгляд за сценами в Any()

Вы можете взглянуть на источник метода Enumerable.Any(), чтобы вникнуть в это немного дальше. Вы увидите цикл foreach для итерации до тех пор, пока не найдет совпадение для предиката, который усиливает ваше предположение о том, что оно O(n):

public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
   if (source == null) throw Error.ArgumentNull("source");
   if (predicate == null) throw Error.ArgumentNull("predicate");
   // Another loop to iterate through the collection until the predicate is matched
   foreach (TSource element in source) {
        if (predicate(element)) return true;
   }
   return false;
}

Как вы можете видеть, Any() сам по себе будет O(n), поскольку длина и вложенность внутри существующего цикла O(m) должны дать вам O(n*m) для всего кода. Однако возможно, что он может быть как O(m).

Ответ 3

.Any() должен быть O(n), когда он ищет контейнер, пока не найдет первый соответствующий экземпляр. Таким образом, в цикле foreach должен быть O(n^2).

Ответ 4

Я предполагаю, что вы предоставляете A и B только, например, их содержимое, и вы знаете, что сложность имеет смысл только для наборов входов (например, средних, худших и лучших случаев), а не для отдельных входов.

Моя точка зрения, в зависимости от требований проблемы и использования, вы можете сделать очень разные оценки сложности кода.

Пусть n be A.Length и m be B.Length. Сложность данного кода затем может быть рассчитана несколькими способами:

  • Предположим, что string.Contains есть O(1). На практике такое сильное предположение может быть сделано, например, если мы точно знаем, что ни одна строка не длиннее некоторой фиксированной заранее длины. Тогда сложность кода, конечно, O(n*m).

  • Предположим, что string.Contains есть O(x*y), где x и y - это длины стога сена и иглы. Пусть самая длинная строка в A имеет длину k_A, а самая длинная строка в B имеет длину k_B. Тогда сложность кода была бы O(n*m*k_A*k_B)

Во втором случае существует другой (более практичный) подход:

Пусть S_A - сумма длин для всех строк в A, а S_B - сумма длины для всех строк в B. Тогда сложность кода была бы O(S_A * S_B).

Что это за худший случай. Однако для среднего случая, а для наиболее практических случаев string.Contains - O(x + y). Таким образом, средняя сложность кода будет O(n*m*(k_A + k_B)) или O((S_B + k_A) * n + (S_A + k_B) * m).

Ответ 5

он по существу является вложенным циклом, поэтому большой O должен быть O (n ^ 2) для наихудшего сценария