【问题标题】:Is there any way to make this a faster algorithm?有什么办法可以使这个算法更快吗?
【发布时间】:2013-08-13 20:22:28
【问题描述】:

如果我理解大 O 表示法,并且相信我在这一点上的理解可能远低于大多数人,以下代码行是 O(n2) 根据 Keyser 的评论,这实际上已经是一个 O(n) 操作:

"Hello, World!".ToLower().Contains("a");

因为ToLower()O(n) 操作,Contains 也是。可能是O(n + n),再说一遍,我的理解还是很模糊。

注意:下面列出了在 Release 构建中运行的测试方法,并利用 Stopwatch 类跟踪运行时间。

但是,我想让它更快,因此请考虑以下三种测试方法:

private static void TestToLower(int i)
{
    var s = "".PadRight(i, 'A');

    var sw = Stopwatch.StartNew();
    s.ToLower().Contains('b');
    sw.Stop();

    _tests.Add(string.Format("ToLower{0}", i), sw.ElapsedMilliseconds);
}

private static void TestHashSet(int i)
{
    var s = "".PadRight(i, 'A');

    var sw = Stopwatch.StartNew();
    var lookup = new HashSet<char>(s.ToLower().AsEnumerable());
    lookup.Contains('b');
    sw.Stop();

    _tests.Add(string.Format("ToHashSet{0}", i), sw.ElapsedMilliseconds);
}

private static void TestHashSet2(int i)
{
    var s = "".PadRight(i, 'A');

    var sw = Stopwatch.StartNew();
    var lookup = new HashSet<char>(s.ToLower().ToArray());
    lookup.Contains('b');
    sw.Stop();

    _tests.Add(string.Format("ToHashSet2{0}", i), sw.ElapsedMilliseconds);
}

现在考虑执行这样的操作:

TestToLower(1000000);
TestToLower(2000000);
TestToLower(4000000);

TestHashSet(1000000);
TestHashSet(2000000);
TestHashSet(4000000);

TestHashSet2(1000000);
TestHashSet2(2000000);
TestHashSet2(4000000);

结果如下:

ToLower1000000: 22.00 ms
ToLower2000000: 40.00 ms
ToLower4000000: 84.00 ms
ToHashSet1000000: 48.00 ms
ToHashSet2000000: 73.00 ms
ToHashSet4000000: 145.00 ms
ToHashSet21000000: 58.00 ms
ToHashSet22000000: 107.00 ms
ToHashSet24000000: 219.00 ms

他们每个人显然仍然必须使用ToLower 方法,但我正在尝试使用HashSet 来加快查找速度。理想情况下,您不必扫描整个字符串。此外,我真的认为第二个整体测试TestHashSet 会更快,因为它不必创建大量内存来分配HashSet

回想起来,我认为为什么最后两种方法较慢。我相信它们更慢,因为我有与第一个相同的算法(即我必须至少遍历整个字符串两次),但除此之外,我还在进行查找。

我怎样才能使这个算法更快?我们经常使用它,我们必须不分大小写地比较字符串。

【问题讨论】:

  • 连续做两个独立的O(n)操作确实是O(n+n)也就是O(n)。思考过程是这样的:相对于nn^2,迭代次数是否增加?
  • @Keyser,所以唯一更快的操作是log(n)?如果是这样,那log到底是什么意思,因为我显然不明白n+nn
  • Big O "math" 声明 O(n+n) 等价于 O(n)
  • @TheSolution 我建议阅读算法复杂性度量的介绍。 n+n 不是 n,但 O(n+n) 与 O(n) 相同。
  • 对于初学者,请确保您的基准测试反映了您的用例。你真的每个“干草堆”字符串只搜索一次,你的字符串真的很大吗(以百万字节为单位)?如果您多次搜索,请更改您的基准以解决此问题(通过重新使用您从字符串构建的任何数据结构)。如果您的字符串更小,请缩小您的测试字符串以更接近真实交易(如果结果太快,则多次运行一次)。

标签: c# .net algorithm big-o


【解决方案1】:

无意冒犯,但你不懂大O。 O(n + n) 与 O(n) 相同。 big-O 的全部意义在于“隐藏”常数因子。在这个问题上,使用一个处理器你不能做得比 O(n) 更好。通过将字符串拆分为 k 个片段并使用单独的线程搜索它们,您可能会在 k 个核心上得到 O(n/k)。

将字符转换为小写是一个常数时间操作。检查与所需字符的匹配是一种廉价的恒定时间操作。在散列集中插入一个字符是一个相当昂贵的常数时间操作。在您的哈希集测试中,您在处理每个字符时添加了这个相当大的常量成本。由于它大于仅查看字符以查看它是否与模式字符串匹配的恒定成本,因此您的运行时间会变长。

仅当您要查找多个值时,使用散列集进行查找才有意义。如果您需要对同一字符串进行多次查找 以查看它是否包含任何或所有 k 个不同的字符,那么您可能会通过构建散列集受益,因为 k 次查找将花费 O(k ) 时间而不是 O(kn) 时间来扫描每个字符的整个字符串。

如果您只在每个字符串中查找一个字符,请忘记 big-O。恒定的因素是您最大的希望。您应该考虑一个低级循环。它会是这样的:

static bool findChar(string str, char charToFind) {
  char upper = Char.toUpper(charToFind);
  char lower = Char.toLower(charToFind);
  for (int i = 0; i < str.length; i++) {
    if (str[i] == upper || str[i] == lower) {
      return true;
    }
  }
  return false;
}

对于语法问题,请提前道歉。我不是 C# 程序员。请注意,这会最多扫描一次字符串。如果角色被提前找到,它就会停止。检查的预期字符数是字符串中字符的一半。这个函数也不会产生垃圾。

另一方面,预期的字符数被

str.ToLower().Contains("a");

str长度的1.5倍,会产生垃圾。所以你可能会用显式循环获胜

如果这仍然太慢,本机函数可能会产生少量增益。您必须尝试一下才能找到答案。

【讨论】:

  • +1。当然,如果 OP 给出了常见的用例(s 有多长?您是在寻找子字符串还是字符?您希望多久找到一次?),那将会很有帮助。
【解决方案2】:

我相信您的代码是 O(2n) = O(n)。那是因为每次调用都会遍历输入字符串 2 次。为了减少运行时间的算法界限,您需要一个具有对数界限的算法,或 O(n^k),使用 k 算法,我认为这在您的场景中是不可能的.我能建议的最好方法是利用不变的特定信息:例如,如果您知道字符串的第一个字母总是大写,则只更改字符串中的第一个字符。这是一个如何利用特定领域知识的示例。

【讨论】:

  • “为了减少运行时间的算法限制,你需要一个 O(log n) 算法”没有意义。任何复杂度低于 O(n) 的算法都会降低算法界限。当然 O(0) 是最好的,O(1)、O(log* n)、O(log log n)、O(sqrt n)、O(n^⅔) 等任何一个都会减少绑定。
  • @jwpat7 O(0) == O(1)。并且 O(log(n) * n) > O(n),不小于。除此之外,是的。
  • @Servy, O(1) = 由常数限制的函数集。 O(0) = 包含常量函数 0 的集合。它们不一样。
  • @Joni 1 != 0, 但 O(1) == O(0),原因与 n + 1 != n 但 O(n + 1) == O(n )。如果两个大 O 函数的执行时间相差一个常数值,则认为它们是等效的。
  • @Servy,log* n表示Iterated Logarithm函数,与n log n无关。此外,如果“O(1) == O(0)”,那么每个 O(1) 函数都是 O(0),但事实并非如此;见big O definition。没有正实数 M 使得 c ≤ M·0,(其中 c 表示任何特定 O(1) 函数所花费的时间)。你可能会认为 O(0) 是被编译掉并且永远不会执行的代码,而 O(1) 是被执行的基本块。
猜你喜欢
  • 2011-12-03
  • 2020-03-05
  • 1970-01-01
  • 2019-02-04
  • 1970-01-01
  • 2023-03-18
  • 1970-01-01
  • 2014-05-23
  • 1970-01-01
相关资源
最近更新 更多