繁体   English   中英

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

[英]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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM