简体   繁体   English

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

[英]Why doesn't STL's implementation of next_permutation use the binary search?

I came up with this question after reading the excellent answer of std::next_permutation Implementation Explanation .在阅读了std::next_permutation Implementation Explanation的优秀答案后,我提出了这个问题。 Please refer to that post for an explanation of the algorithm used by STL, but I'll replicate the code here for your reference请参阅该帖子以了解 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()));
}

Let's take a look at this part我们来看看这部分

It k = end;

while (!(*i < *--k))
    /* pass */;

iter_swap(i, k);

From my understanding, this linear scan could be replaced by a binary search, because by construction the elements after i are already in descending (or in case of duplicate elements, non-ascending) order.根据我的理解,这种线性扫描可以用二分搜索代替,因为通过构造i之后的元素已经是降序(或者在重复元素的情况下,非升序)。 Suppose we have a boolean array whose items are *idx > *i for each j <= idx < end , then all I need to do is to find the largest index whose item is True .假设我们有一个布尔数组,其项为*idx > *i对于每个j <= idx < end ,那么我需要做的就是找到项为True的最大索引。 Such an index must exist because we have *j > *i , which means the array starts with a True .这样的索引必须存在,因为我们有*j > *i ,这意味着数组以True开头。

I don't know enough C++ to confidently provide a working example, but here is a complete implementation of next_permutation in Rust.我对 C++ 的了解不足以自信地提供一个工作示例,但这里是 Rust 中next_permutation的完整实现。 If you don't know Rust, then the following pseudocode should give a good idea of what I mean by "binary search".如果您不了解 Rust,那么下面的伪代码应该可以很好地理解我所说的“二分搜索”是什么意思。 (Well yes, it's Python, which is readable enough to be referred to as pseudocode :) (嗯,是的,它是 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

Question: why doesn't STL's implementation of next_permutation use the binary search?问题:为什么 STL 的next_permutation实现不使用二分查找? I understand that finding i requires O(n) , and that both O(n) + O(n) and O(n) + O(ln(n)) are O(n) , but practically speaking binary search should still at least marginally improve performance?我知道找到i需要O(n) ,并且O(n) + O(n)O(n) + O(ln(n))都是O(n) ,但实际上二进制搜索应该仍然在最低限度地提高性能?

As @RichardCritten points out, just because we have better algorithmic complexity does not mean that the execution is faster.正如@RichardCritten 指出的那样,仅仅因为我们有更好的算法复杂性并不意味着执行速度更快。 Additionally, the implementation is a bit more complex.此外,实现有点复杂。

Below, we have made a very simple alteration to the original algorithm in the middle part.下面,我们对中间部分的原始算法进行了非常简单的改动。

Original Code:原始代码:

if (*i < *j)
{
    It k = end;
    
    while (!(*i < *--k))
        /* pass */;
    
    iter_swap(i, k);
    reverse(j, end);
    return true;
}

Simple Binary Approach简单的二进制方法

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;
}

We make use of std::lower_bound as well as std::make_reverse_iterator since the given range is in descending order.我们使用std::lower_bound以及std::make_reverse_iterator因为给定的范围是降序的。

It should be noted that this implementation is not full proof and does not work when there are repeats.应该注意的是,这种实现不是完全证明,并且在重复时不起作用。 One of the main points is to demonstrate that even for simple cases, the original implementation is faster.要点之一是证明即使对于简单的情况,原始实现也更快。

Here is a live ideone example demonstrating the speed of each approach.这是一个演示每种方法的速度的实时 ideone 示例 In our tests, we measure how long each approach takes to generate 10! = 3,628,800在我们的测试中,我们测量每种方法生成10! = 3,628,800 10! = 3,628,800 permutations 100 times. 10! = 3,628,800次排列 100 次。

You will note that the linear implementation is almost twice as fast.您会注意到线性实现的速度几乎是其两倍。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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