【问题标题】:O(N) Identification of PermutationsO(N) 排列的识别
【发布时间】:2016-08-20 07:15:10
【问题描述】:

This answer 通过比较它们的内容来确定两个字符串是否是排列。如果它们包含相同数量的每个字符,则它们显然是排列。这是在 O(N) 时间内完成的。

我不喜欢这个答案,因为它重新定义了 is_permutation 的设计目的。也就是说,is_permutation 的复杂性为:

至多 O(N2) 次应用谓词,或者如果序列已经相等,则恰好 N 次,其中N=std::distance(first1, last1)

所以我不能提倡使用is_permutation,它比手工旋转算法慢几个数量级。但是标准的实施者肯定不会错过如此明显的改进吗?那么为什么是is_permutationO(N2)

【问题讨论】:

  • 看起来像是时间/空间的权衡——更快的解决方案需要一个由您正在排列的值索引的计数数组。如果这些值是字节,则它是一个包含 256 个条目的数组,如另一个答案所示。如果它们更大,则数组可能会大到无法接受。
  • 为什么不先排序,然后比较呢?事实上,也许这就是std:: is_permutation 所做的? Nlog(N) 平均,但 N^2 在最坏的情况下?
  • @Aaron: std::is_permutation 无法排序,因为它不知道如何对元素进行相互排序(因为它不认为这样的事情是有意义的!),它只知道如何测试它们是否相等。
  • @moonshadow 非常棒且引人入胜的评论,直到现在我还没有注意到 is_permutation 不需要定义小于运算符。
  • "我不能提倡使用 is_permutation,因为它比手工旋转算法慢得多。" 除非你的手工旋转算法是 O(0),否则你可以请放心,is_permutation 不能以指数级速度变慢。

标签: c++ big-o permutation string-comparison standard-library


【解决方案1】:

您引用的答案适用于chars。它假设它们是 8 位的(不一定是这种情况),因此每个值只有 256 种可能性,并且您可以廉价地从每个值转到数字索引以用于计数查找表(对于 char in这种情况下,值和索引是一回事!)

它生成每个char值在每个字符串中出现的次数;那么,如果两个字符串的这些分布相同,则这些字符串是彼此的排列。

时间复杂度是多少?

  • 您必须遍历每个字符串的每个字符,因此对于长度为 M 和 N 的两个输入,需要 M+N 步
  • 每个步骤都涉及在 char 给定的索引处增加固定大小表中的计数,因此是常数时间

所以总的时间复杂度是 O(N+M):线性,正如你所描述的。

现在,std::is_permutation 没有对其输入做出这样的假设。它不知道只有 256 种可能性,或者它们确实是有界的。它不知道如何从输入值转换为可以用作索引的数字,更不用说如何在恒定时间内做到这一点。它唯一知道的是如何比较两个值是否相等,因为调用者提供了该信息。

所以,时间复杂度:

  • 我们知道它必须在某个时候考虑每个输入的每个元素
  • 我们知道,对于以前从未见过的每个元素(我将讨论如何确定它以及为什么它不会影响大 O 复杂性作为练习),它无法将元素转换为计数表的任何类型的索引或键,因此它无法计算该元素的出现次数,这比线性遍历两个输入以查看有多少元素匹配

所以复杂性充其量是二次方。

【讨论】:

  • “最好是二次的”。当然 Nlog(N) 是可能的(平均而言) - 先对范围进行排序,然后比较?
  • @Aaron: 没有签名;如果 BinaryPredicate 的输入相等,则返回 true,否则返回 false,但是要进行排序,您需要能够强制排序,而不仅仅是测试是否相等,因此您需要使 BinaryPredicate 能够返回(小于/大于/equal) 而不是 (equal/not equal),这可能会限制您可以使用的值类型。
  • 当然,如果你愿意承担更多的事情,你可以做得更好。您可以想象一种不太通用的 is_permutation 形式,它接受一个函数并产生一个哈希键,如果您将计数存储在哈希表中,这可能会让您回到平均情况线性复杂度。但这不是完全通用的 std::is_permutation 所做的。
  • 您实际上还必须比较这两个表。当然,如果你可以假设char 有 256 个可能的值,那就是 O(256)
  • @MSalters:比较两个固定大小的表是固定时间。
【解决方案2】:

这个答案是我写的。

当字符串的value_typechar 时,查找表中所需的元素数为 256。对于两字节编码,为 65536。对于四字节编码,查找表将刚好超过 4十亿个条目,大小可能为 16 GB!而且大部分都不会被使用。

所以首先要认识到,即使我们将类型限制为charwchar_t,它仍然可能站不住脚。同样,如果我们想对int 类型的序列执行is_permutation

对于大小为 1 或 2 字节的整数类型,我们可以专门化 std::is_permutation<>。但这有点让人想起std::vector<bool>,回想起来并不是每个人都认为这是个好主意。

我们还可以使用基于std::map<T, size_t> 的查找表,但这可能会占用大量内存,因此可能不会带来性能提升(或者至少,并非总是如此)。不过,可能值得实施一个进行详细比较。

总之,我不认为 C++ 标准没有为char 包含高性能版本的is_permutation。首先是因为在现实世界中我不确定它是模板的最常见用途,其次是因为 STL 不是算法的全部和全部,尤其是在可以使用领域知识来加速特殊计算的领域案例。

如果事实证明 is_permutationchar 在野外相当普遍,C++ 库实现者将有权为其提供专业化。

【讨论】:

  • “但这有点让人想起std::vector<bool>,回想起来并不是每个人都认为这是个好主意。” 我不同意。 std::vector<bool> 不是一个好主意的原因是它影响了内存布局并使 vector<bool> 不是真正的标准容器。 is_permutation<char> 不会有任何这些问题,并且会是一个完美的优化。
【解决方案3】:

is_permutation 几乎适用于任何数据类型。您链接中的算法仅适用于具有少量值的数据类型。

这与std::sort 是 O(N log N) 而计数排序是 O(N) 的原因相同。

【讨论】:

  • 我不能买那个,而不是 int[] 来计算字符我们可以使用 long long[] 并且永远不需要更多空间并且仍然运行在 O(N) i>.
  • @JonathanMee 你怎么知道要制作多少桶?如果数组的元素在INT_MININT_MAX 范围内,那么您将需要一个巨大的数组。
  • @doublep 否定数组是long long[numeric limits<char>::max - numeric_limits<char>::min ]。与性能提升相比,这实际上是一个非常小的数组。
  • @JonathanMee 对于char 类型的字符串,是的。在整数数组或long long 数组上使用它怎么样?
  • @JonathanMee:更糟糕的是,使用数组假定数据类型是整数。但是,如果您的收藏是 std::vector<std::string> 怎么办? IE。您不是在寻找单词中字母的排列,而是寻找句子中的单词?您需要 std::map<std::string, int> 来计算每个单词的出现次数,而该地图甚至没有 O(1) 访问权限。
猜你喜欢
  • 1970-01-01
  • 2017-01-19
  • 1970-01-01
  • 2014-07-16
  • 2022-01-11
  • 2012-05-09
  • 1970-01-01
  • 2017-07-24
  • 1970-01-01
相关资源
最近更新 更多