std :: next_permutation实现说明


110

我很好奇如何std:next_permutation实现,所以我提取了gnu libstdc++ 4.7版本并清理了标识符和格式以产生以下演示...

#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()));
}

输出是预期的:http : //ideone.com/4nZdx

我的问题是:它如何运作?是什么意思ijk?它们在执行的不同部分具有什么价值?证明其正确性的草图是什么?

显然,在进入主循环之前,它只检查了琐碎的0或1元素列表情况。在主循环的入口处,我指向最后一个元素(末尾没有一个),并且列表的长度至少为2个元素。

主循环主体中发生了什么?


嘿,您是如何提取这段代码的?当我检查#include <algorithm>时,代码完全不同,它包含更多功能
Manjunath

Answers:


172

让我们看一下一些排列:

1 2 3 4
1 2 4 3
1 3 2 4
1 3 4 2
1 4 2 3
1 4 3 2
2 1 3 4
...

我们如何从一个排列转到下一个排列?首先,让我们以不同的方式来看待事情。我们可以将元素视为数字,并将排列视为数字。以这种方式查看问题,我们希望以“升序”顺序排列排列/数字

订购数字时,我们希望“以最小的数量增加它们”。例如,当我们计数时,我们不计算1、2、3、10,...,因为它们之间仍然还有4、5,...,尽管10大于3,但仍有一些缺失的数字可以通过将3增加一小部分。在上面的示例中,我们看到,1由于前3个“数字”的重排次数很多,“排列”次数变少了很多,因此很长时间以来它一直是第一个数字。

那么我们什么时候才能最终“使用” 1?仅当后3位没有更多排列时。
而且什么时候后三位数不再排列?当后三位数字降序排列时。

啊哈!这是理解算法的关键。我们仅在右侧的所有内容都按降序排列时更改“数字”的位置, 因为如果不按降序排列,那么还有更多的排列要走(即,我们可以将排列“增加”一小部分) 。

现在让我们回到代码:

while (true)
{
    It j = i;
    --i;

    if (*i < *j)
    { // ...
    }

    if (i == begin)
    { // ...
    }
}

从循环的前两行开始,j是一个元素,并且i是它之前的元素。
然后,如果元素按升序排列,请执行(if (*i < *j))。
否则,如果整个事物以降序(if (i == begin))排列,则这是最后一个排列。
否则,我们继续,我们看到j和i本质上是递减的。

现在,我们了解了该if (i == begin)部分,因此我们需要了解的只是该if (*i < *j)部分。

还要注意:“然后,如果元素是按升序...”,这支持了我们先前的观察,即“当右边的所有内容都按降序排列时,我们只需要对数字做些事情”。升序if语句本质上是在“最右边的东西都按降序排列”的最左边位置。

让我们再来看一些示例:

...
1 4 3 2
2 1 3 4
...
2 4 3 1
3 1 2 4
...

我们看到,当数字右边的所有内容都按降序排列时,我们找到下一个最大的数字并将其放在前面,然后将其余的数字按升序排列

让我们看一下代码:

It k = end;

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

iter_swap(i, k);
reverse(j, end);
return true;

好吧,由于右边的东西是降序排列的,所以我们只需要从头开始迭代就可以找到“下一个最大的数字”,这在前三行代码中可以看到。

接下来,我们用该iter_swap()语句在前面交换“下一个最大的数字” ,然后由于我们知道该数字是第二个最大的数字,因此我们知道右边的数字仍按降序排列,因此将其按升序排列,我们只需要这样做reverse()


12
惊人的解释

2
感谢您的解释!该算法称为按字典顺序生成。中有很多这样的算法Combinatorics,但这是最经典的算法。
2015年

1
这种算法的复杂性是什么?
user72708'1

leetcode有很好的解释,leetcode.com
problems /

40

gcc实现按字典顺序生成排列。维基百科解释如下:

在给定排列之后,以下算法按字典顺序生成下一个排列。它就地更改给定的排列。

  1. 找到最大索引k,以使a [k] <a [k + 1]。如果不存在这样的索引,则该排列为最后的排列。
  2. 找到最大索引l,使a [k] <a [l]。由于k +1是这样的索引,因此l定义明确,并且满足k <l。
  3. 将a [k]与a [l]交换。
  4. 反转从a [k +1]到最后一个元素a [n]的序列。

AFAICT,所有实施都生成相同的顺序。
MSalters

12

Knuth在“计算机编程艺术”的 7.2.1.2和7.2.1.3节中深入讨论了该算法及其推广。他称其为“算法L”-显然可以追溯到13世纪。


1
你能提一下书名吗?
Grobber 2014年

3
TAOCP =计算机程序设计的艺术

9

这是使用其他标准库算法的完整实现:

template <typename I, typename C>
    // requires BidirectionalIterator<I> && Compare<C>
bool my_next_permutation(I begin, I end, C comp) {
    auto rbegin = std::make_reverse_iterator(end);
    auto rend = std::make_reverse_iterator(begin);
    auto rsorted_end = std::is_sorted_until(rbegin, rend, comp);
    bool has_more_permutations = rsorted_end != rend;
    if (has_more_permutations) {
        auto next_permutation_rend = std::upper_bound(
            rbegin, rsorted_end, *rsorted_end, comp);
        std::iter_swap(rsorted_end, next_permutation_rend);
    }
    std::reverse(rbegin, rsorted_end);
    return has_more_permutations;
}

演示版


1
这强调了好的变量名和关注点分离的重要性。is_final_permutation比提供更多信息begin == end - 1。调用is_sorted_until/ upper_bound将置换逻辑与那些操作分开,并使此操作更容易理解。另外,upper_bound是二进制搜索,而while (!(*i < *--k));线性搜索,因此性能更高。
Jonathan Gawrych

1

使用可以对cppreference进行自我说明<algorithm>

template <class Iterator>
bool next_permutation(Iterator first, Iterator last) {
    if (first == last) return false;
    Iterator i = last;
    if (first == --i) return false;
    while (1) {
        Iterator i1 = i, i2;
        if (*--i < *i1) {
            i2 = last;
            while (!(*i < *--i2));
            std::iter_swap(i, i2);
            std::reverse(i1, last);
            return true;
        }
        if (i == first) {
            std::reverse(first, last);
            return false;
        }
    }
}

将内容更改为按字典顺序的下一个排列(就地),如果存在则返回true,否则进行排序,如果不存在则返回false。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.