【发布时间】:2010-09-22 06:32:40
【问题描述】:
假设我有一个方法需要从包含 100 个元素的地图中提取 8 个值。你认为哪个更可取:
在for循环中从头到尾走一遍,通过按键拉出元素?
或者使用 find 8 次来获取这些值?
【问题讨论】:
标签: c++ performance stl iterator find
假设我有一个方法需要从包含 100 个元素的地图中提取 8 个值。你认为哪个更可取:
在for循环中从头到尾走一遍,通过按键拉出元素?
或者使用 find 8 次来获取这些值?
【问题讨论】:
标签: c++ performance stl iterator find
遍历列表将花费你 O(n) 时间来找到一个随机元素。
Map 是一个平衡二叉树,所以查找是 O(log n)。
因此,执行 8 会在 8*log2(n) 中找到结果,并且遍历列表是 (n)。列表越大,收益越大,但在所有随机情况下,查找比迭代更快。
在非随机情况下,如果有理由让您想要的项目在树中彼此靠近,或者靠近“开始”(左侧),那么步行/迭代会更快。但这似乎不太可能。
【讨论】:
虽然我会选择 find 选项,但人们对渐近性能过于强调。
事实上,对于可以接收相当大的输入的算法来说,渐近性能是一个方便的指南,但即便如此,它也不是万无一失的。对于任何合理的输入,渐近性能比其他算法更差的算法很有可能更快。
然后有时您的输入大小会相当小(甚至是固定的)。在这种情况下,渐近性能几乎没有意义。
【讨论】:
我会使用 find 8 次。这将是更少(更明显)的代码。
您应该尽量不要根据小数字做出性能判断,因为 (a) 在这种大小下,无论哪种方式都不太可能成为性能瓶颈,并且 (b) 数字将来可能会发生变化 - 选择算法最好的渐近性能。
注意:您提到键上的“切换”......这可能适用于您的情况,但通常您无法在地图中打开键值。允许这样做会使通过迭代在地图中搜索 M 项的代码更加复杂。
【讨论】:
8 次查找最好,因为代码更简单清晰。
不过,考虑性能更有趣,所以我也会这样做。
正如我在写这个答案时 Artelius 所说,忽略复杂性。这无关紧要,因为您知道 n=100。例如,插入排序的算法复杂度比快速排序更差(至少在平均情况下),但在几乎所有实现中,对于小的 n,插入排序都比快速排序快,因此快速排序在接近尾声时突破为插入排序。您的 n 也很小,因此 n -> 无穷大的限制并不重要。
由于这两个选项的代码很容易编写,我建议对其进行分析。这将 (a) 告诉你哪个更快,并且 (b) 证明两者都非常快,以至于你做什么都没有关系(除非它是你的程序唯一做的事情,而且它做了很多事情)。尤其是当您谈到打开键时 - 如果键是整数类型,那么限制因素更有可能是内存缓存问题而不是任何实际处理。
但是如果做不到这一点,通常比较搜索算法的方法是计算比较,假设它们比遍历结构要慢得多。如果不出意外,每次比较都会访问内存并且是一个不可预测的分支,这是 CPU 通常最不擅长的两件事。
如果你在开始之前对你的 8 个元素进行排序(大约需要 24 次比较)而不是你建议的 switch,那么因为地图也是排序的,你只需要在你遍历的每个节点上做一次比较,加上一个比较您正在搜索的每个项目(比较每个“边”中的一个节点。如果它们匹配,则增加两边。如果它们不匹配,则增加具有较小元素的边)。所以在最坏的情况下是 8+100,或者如果你在结束之前找到所有 8 个,则更少。但是 8 个中最后一个的平均位置,如果它们随机位于地图中,仍然是大约 8/9 的位置。所以称它为 8+88+24 = 120 次比较,最坏的情况是 132 次。最好的情况是 8(如果您要搜索的东西都在开头)+24(对于初始排序)= 32 次比较,或者如果您在排序上也很幸运或者您的 8 个搜索项是出于某种原因已准备好排序。
红黑树(通常是映射)中节点的平均深度略高于 log2(n)。在这种情况下称它为 7,因为 2^7 是 128。所以找到 8 个元素应该进行 56 次比较。 IIRC 红黑树的平衡特性是最深节点不超过最浅节点深度的两倍。所以最坏的情况深度是 floor(2*log2(n)),称之为 15,总共 120,最好的是 ceil(1/2 log2(n)),即 4。这又是 32 次比较。
因此,假设比较是唯一影响速度的因素,那么 8 次查找的速度将是线性遍历的 4 倍和 4 倍之间,平均要好 2 倍。
不过,线性遍历可能会占用更多内存,因此可能会更慢。但最终对于 n=100 你说的是毫秒时间,所以只要做最简单的代码(可能是 8 个发现)。我有没有提到,如果你真的想知道你无法预测的速度,你只需要分析它吗?
【讨论】:
正如其他人所指出的,我可能会在地图上使用 find() 八次并完成它。但是根据您的需要,可以考虑另一种选择。如果地图构建后地图中的项目不会发生太大变化,或者您不需要将插入与查找交错,您可以尝试将键/值对保留在排序向量中。如果这样做,则可以使用 lower_bound() 函数在对数时间内进行二进制搜索。这样做的好处是,如果可以对您要查找的键进行排序(并且您知道它们将始终存在),那么您可以使用从前一个查找返回的迭代器作为下一个查找的下限。例如,
矢量::迭代器 a = my_map.lower_bound(my_map.begin(), my_map.end(), "a"); 矢量::迭代器 b = my_map.lower_bound(a + 1, my_map.end(), "b"); 向量::迭代器 c = my_map.lower_bound( b + 1, my_map.end(), "c" ); // ...这两种方法都有对数查找,但这有助于在一定程度上减少常数。
【讨论】:
你应该使用 find 8 次。
将切换方法想象为对每个节点进行 8 次比较。那是 800 次比较,而您完全失去了键控地图的所有好处,它还可能是一个列表。
通过查找方法,您可以利用地图的优势遍历列表。我相信 std::maps 被实现为二叉树,这意味着搜索键只需要将您的键与树的深度进行比较,对于 100 个元素的二叉树,深度为 8~。现在,您只需 8*8 比较或 64 次比较即可找到所有 8 个元素。
【讨论】:
这里是对它们的时间复杂度的分析(n是地图中的项目数),保证查找find具有对数或更好的时间复杂度:
8 * log2 n 8 次查找 n 表示遍历所有
对于较小的数字,第一个较大(例如,n=2 时为 8),但在 43 左右,第一个将变得比第二个更好并保持不变。所以,你会想要使用第一种方法,因为它也更方便编码。
【讨论】:
如果这很关键,我会同时实施这两种方法并对性能进行基准测试。
理论上是这样
8 * lg(100) >?
其他考虑因素是这些数字中的任何一个是否会改变 - 是否会超过 100 个元素;你会进行超过 8 次搜索吗?
【讨论】:
让我们假设“find”在找到密钥时退出。
让我们进一步假设您对“开关”进行了明智的编码,并在找到匹配项后退出检查。我们还将假设您不在找到所有 8 个代码后,无需费心编写代码以在整个过程中保释(编写代码可能会很痛苦)。
“8 查找”方法可以预期(iow:平均)执行 50 * 8 = 400 次比较。
“切换”方法可以预期(iow:平均)执行 (8 * 100) - 28 = 772 次比较。
因此,在比较方面,8 次查找方法更好。但是,比较的数量足够少,您最好选择更容易理解的选项。不过,这也可能是 8 find 方法。
【讨论】: