为啥 STL 的 next_permutation 实现不使用二分查找?

Posted

技术标签:

【中文标题】为啥 STL 的 next_permutation 实现不使用二分查找?【英文标题】:Why doesn't STL's implementation of next_permutation use the binary search?为什么 STL 的 next_permutation 实现不使用二分查找? 【发布时间】:2021-06-24 01:13:16 【问题描述】:

在阅读了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 之后的元素已经按降序(或在重复元素的情况下,非升序)顺序。假设我们有一个布尔数组,其中每个 j &lt;= idx &lt; end 的项目都是 *idx &gt; *i,那么我需要做的就是找到项目是 True 的最大索引。这样的索引必须存在,因为我们有*j &gt; *i,这意味着数组以True 开头。

我没有足够的 C++ 知识来自信地提供一个工作示例,但 here 是 next_permutation 在 Rust 中的完整实现。如果你不了解 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),但实际上二进制搜索至少应该还能稍微提​​高性能?

【问题讨论】:

由于内存预取器,Linear 在传染性容器(例如std::vectorstd::string)上可以更快。 @RichardCritten 我知道由于缓存,顺序访问比随机访问快得多,但是当他们需要分别查看 Nln(N) 元素时,这仍然适用吗? 这不是缓存,而是预取器 - 这是一个示例 - Herb Sutter 比较标准容器的 O(n) 与 O(log n)(猜猜哪个容器获胜) - 开始45:48 - channel9.msdn.com/Events/Build/2014/2-661 结论 - 你需要衡量你的工作量。 在链接的 Rust 实现中,let mid = (p + q) / 2; 是溢出的候选对象 - 我不知道 Rust 在溢出发生时会做什么,但通常最好避免。另请参阅上面的二分搜索实施问题链接。 @RichardCritten 感谢您的提示!我将其替换为 let mid = p + (q - p) / 2; 【参考方案1】:

正如@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,因为给定的范围是降序排列的。

需要注意的是,这个实现不是完全证明的,当有重复时不起作用。要点之一是证明即使对于简单的情况,原始实现也更快。

这是一个live ideone example,展示了每种方法的速度。在我们的测试中,我们测量了每种方法生成 10! = 3,628,800 排列 100 次所需的时间。

您会注意到线性实现的速度几乎是原来的两倍。

【讨论】:

以上是关于为啥 STL 的 next_permutation 实现不使用二分查找?的主要内容,如果未能解决你的问题,请参考以下文章

STL::next_permutation();

STL next_permutation 算法原理和自行实现

STL next_permutation 算法原理和自行实现

stl算法:next_permutation剖析

C++STL的next_permutation

STL next_permutation 全排列