std::next_permutation 实现说明
Posted
技术标签:
【中文标题】std::next_permutation 实现说明【英文标题】:std::next_permutation Implementation Explanation 【发布时间】:2012-07-14 01:06:54 【问题描述】:我很好奇 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
我的问题是:它是如何工作的? i
、j
和 k
是什么意思?它们在执行的不同部分有什么价值?什么是其正确性证明的草图?
很明显,在进入主循环之前,它只检查琐碎的 0 或 1 元素列表案例。在主循环的入口处,我指向最后一个元素(不是一个过去的结尾),并且列表至少有 2 个元素长。
主循环体内发生了什么?
【问题讨论】:
嘿,你是如何提取那段代码的?当我检查#include 时,代码完全不同,包含更多功能 我刚刚注意到该函数的底部没有返回子句,这是一个好习惯吗?为什么不返回 false? @Jeff:while (true)
语句是一个无限循环,该函数仅通过循环所包含的内部 return 语句返回。
【参考方案1】:
这是使用其他标准库算法的完整实现:
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 rupper_bound = std::upper_bound(
rbegin, rsorted_end, *rsorted_end, comp);
std::iter_swap(rsorted_end, rupper_bound);
std::reverse(rbegin, rsorted_end);
return has_more_permutations;
Demo
【讨论】:
这强调了良好的变量名称和关注点分离的重要性。is_final_permutation
比 begin == end - 1
提供更多信息。调用is_sorted_until
/upper_bound
将排列逻辑与这些操作分开,并使这更容易理解。另外,upper_bound 是二分搜索,而while (!(*i < *--k));
是线性的,因此性能更高。
@JonathanGawrych 复杂性更好,但在实际实践中不一定性能更高。【参考方案2】:
让我们看一些排列:
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 位数字不再有排列时。
什么时候不再有最后 3 位数字的排列?当最后 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;
好吧,因为右边的东西是按降序排列的,要找到“下一个最大的数字”,我们只需要从最后迭代,我们在前 3 行代码中看到。
接下来,我们用 iter_swap()
语句将“下一个最大的数字”交换到前面,然后由于我们知道该数字是第二大的,我们知道右边的数字仍然是降序的,所以按升序排列,我们只需要reverse()
它。
【讨论】:
感谢您的解释!该算法称为Generation in lexicographic order。Combinatorics
有很多这样的算法,但这是最经典的一种。
这种算法的复杂度是多少?
leetcode 有很好的解释,leetcode.com/problems/next-permutation/solution
我想我需要一个 Youtube 视频来理解它。但是解释太好了……我只看过一次,我不需要再读一遍(也没有yt视频)。很棒的解释!!!【参考方案3】:
在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。
【讨论】:
【参考方案4】:Knuth 在计算机编程的艺术的第 7.2.1.2 和 7.2.1.3 节中深入介绍了该算法及其概括。他称之为“算法 L”——显然它可以追溯到 13 世纪。
【讨论】:
你能说一下书名吗? TAOCP = 计算机编程的艺术【参考方案5】:gcc 实现按字典顺序生成排列。 Wikipedia解释如下:
以下算法生成下一个排列 在给定排列后按字典顺序排列。它改变了给定的 就地排列。
找到最大的索引 k 使得 a[k] 找到最大的索引 l 使得 a[k] 将 a[k] 与 a[l] 交换。 反转从 a[k + 1] 到最后一个元素 a[n] 的序列。
【讨论】:
AFAICT,所有实现都生成相同的顺序。以上是关于std::next_permutation 实现说明的主要内容,如果未能解决你的问题,请参考以下文章
C++ STL应用与实现62: 如何使用std::next_permutation
为啥 std::next_permutation 会跳过一些排列?