[英]Why doesn't STL's implementation of next_permutation use the binary search?
在阅读了std::next_permutation Implementation Explanation的优秀答案后,我提出了这个问题。 请参阅该帖子以了解 STL 使用的算法的说明,但我将在此处复制代码以供您参考
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
template<typename It>
bool next_permutation(It begin, It end)
{
if (begin == end)
return false;
It i = begin;
++i;
if (i == end)
return false;
i = end;
--i;
while (true)
{
It j = i;
--i;
if (*i < *j)
{
It k = end;
while (!(*i < *--k))
/* pass */;
iter_swap(i, k);
reverse(j, end);
return true;
}
if (i == begin)
{
reverse(begin, end);
return false;
}
}
}
int main()
{
vector<int> v = { 1, 2, 3, 4 };
do
{
for (int i = 0; i < 4; i++)
{
cout << v[i] << " ";
}
cout << endl;
}
while (::next_permutation(v.begin(), v.end()));
}
我们来看看这部分
It k = end;
while (!(*i < *--k))
/* pass */;
iter_swap(i, k);
根据我的理解,这种线性扫描可以用二分搜索代替,因为通过构造i
之后的元素已经是降序(或者在重复元素的情况下,非升序)。 假设我们有一个布尔数组,其项为*idx > *i
对于每个j <= idx < end
,那么我需要做的就是找到项为True
的最大索引。 这样的索引必须存在,因为我们有*j > *i
,这意味着数组以True
开头。
我对 C++ 的了解不足以自信地提供一个工作示例,但这里是 Rust 中next_permutation
的完整实现。 如果您不了解 Rust,那么下面的伪代码应该可以很好地理解我所说的“二分搜索”是什么意思。 (嗯,是的,它是 Python,它的可读性足以被称为伪代码 :)
from typing import List
def bisearch(last: List[bool]) -> int:
p, q = 0, len(lst) - 1
while p + 1 < q:
mid = (p + q) // 2
if lst[mid]:
p = mid
else:
q = mid
return q if lst[q] else q - 1
if __name__ == '__main__':
for pos_count in range(1, 5):
for neg_count in range(5):
lst = [True] * pos_count + [False] * neg_count
assert bisearch(lst) == pos_count - 1
问题:为什么 STL 的next_permutation
实现不使用二分查找? 我知道找到i
需要O(n)
,并且O(n) + O(n)
和O(n) + O(ln(n))
都是O(n)
,但实际上二进制搜索应该仍然在最低限度地提高性能?
正如@RichardCritten 指出的那样,仅仅因为我们有更好的算法复杂性并不意味着执行速度更快。 此外,实现有点复杂。
下面,我们对中间部分的原始算法进行了非常简单的改动。
if (*i < *j)
{
It k = end;
while (!(*i < *--k))
/* pass */;
iter_swap(i, k);
reverse(j, end);
return true;
}
if (*i < *j)
{
auto test = std::lower_bound(std::make_reverse_iterator(end),
std::make_reverse_iterator(i), *i);
std::iter_swap(i, test);
std::reverse(j, end);
return true;
}
我们使用std::lower_bound以及std::make_reverse_iterator因为给定的范围是降序的。
应该注意的是,这种实现不是完全证明,并且在重复时不起作用。 要点之一是证明即使对于简单的情况,原始实现也更快。
这是一个演示每种方法的速度的实时 ideone 示例。 在我们的测试中,我们测量每种方法生成10! = 3,628,800
10! = 3,628,800
次排列 100 次。
您会注意到线性实现的速度几乎是其两倍。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.