一、简介
STL允许在集合上做很多事情,其中之一就是对集合内的元素重新排序。也可以称为对集合执行排列。
在集合中移动元素通常需要编写大量复杂的代码,涉及循环和迭代器。而这也许是STL产生最令人瞩目改进的领域,因为它将这些复杂的操作封装在有意义的接口里面。
STL提供了以下的排列:
- Lexicographical 排列
- 循环排列
- 随机排列
- 反向
- 检查排列
- 还有更多的排列方式。
二、Lexicographical 排列
一个包含N
个元素的集合可以以多种不同的方式进行重新排序(准确地说,是N!
种方式)。是否可以遍历所有这些排列,并确保不遗漏任何一种呢?
为了实现这一点,可以在给定集合的排列集合上定义一个顺序。这样,可以从一个排列开始,然后到“下一个”,再到“下一个”,以此类推,直到回到起点。好麻烦,是否有一种更自然的排列方式呢?
确实有的:给定集合的排列可以按照字典顺序排序。想象一下,集合的每个排列都是一个“单词”,集合的元素是组成这个单词的“字母”。
然后可以按“字母顺序”对这些单词进行排序(使用引号是因为在这里讨论的不是实际的字符和字符串,只是为了了解大意)。要实现这一点,需要集合的元素实现一个operator<
来比较它们。
例如,集合{1,2,3,4,5}
按字典顺序递增的4种排列:
{1, 2, 3, 4, 5}
{1, 2, 3, 5, 4}
{1, 2, 4, 3, 5}
{1, 2, 4, 5, 3}
......
如何用STL做到这一点?可以使用std::next_permutation
:
vector<int> v = {1, 2, 3, 4, 5 };
std::next_permutation(v.begin(), v.end()); // v now contains {1, 2, 3, 5, 4}
std::next_permutation
返回一个bool
值,如果获得的排列在字典顺序上大于输入的排列,则返回true
,否则返回false
(在唯一的情况下,递增循环结束并且范围返回到第一个(最小)排列)。
要获得前一个,使用std::prev_permutation
:
vector<int> v = {1, 2, 3, 5, 4};
std::prev_permutation(v.begin(), v.end()); // v now contains {1, 2, 3, 4, 5 }
相对地,std::prev_permutation
返回一个bool
值,如果获得的排列在字典顺序上小于输入排列,则返回true
,否则返回false
(在唯一的情况下,范围被重置为最后(最大)排列)。
std::next_permutation
和std::prev_permutation
直接对传入参数的范围进行操作,这使得在一行中很容易多次使用它们:
std::vector<int> numbers = {1, 2, 3, 4};
do
{
for (int n : numbers) std::cout << n << ' ';
std::cout << '\n';
}
while (std::next_permutation(begin(numbers), end(numbers)));
输出:
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
2 1 4 3
2 3 1 4
2 3 4 1
2 4 1 3
2 4 3 1
3 1 2 4
3 1 4 2
3 2 1 4
3 2 4 1
3 4 1 2
3 4 2 1
4 1 2 3
4 1 3 2
4 2 1 3
4 2 3 1
4 3 1 2
4 3 2 1
这是{1, 2, 3, 4}
的所有排列,直到它回到初始位置为止。
三、循环排列
循环排列向下移动集合中的元素,并将集合末尾的元素移到集合的开始。例如:
{1, 2, 3, 4, 5}
{5, 1, 2, 3, 4}
{4, 5, 1, 2, 3}
{3, 4, 5, 1, 2}
{2, 3, 4, 5, 1}
对于N个元素的集合,有N个不同的循环排列。
3.1、基本使用
在C++中,循环排列是用std::rotate
来执行的。std::rotate
接受3个迭代器:
- 一个指向范围的开始。
- 一个指向希望
std::rotate
到第一个位置的元素。 - 一个指向范围的末端。
在C++ 11中,std::rotate
返回一个指向第一个元素所在位置的迭代器。函数原型:
template<typename ForwardIterator>
ForwardIterator rotate(ForwardIterator begin, ForwardIterator new_begin, ForwardIterator end);
这与C++ 98中的函数原型略有不同,因为它返回void
:
template<typename ForwardIterator>
void rotate(ForwardIterator begin, ForwardIterator new_begin, ForwardIterator end);
注意,std::rotate
直接对传递给它的范围进行操作。如果希望保持此范围不变,请使用std::rotate_copy
将输出写入另一个集合。
3.2、std::rotate的一个有趣用法
std::rotate
可以用来创建新的算法,揭示了使用STL算法的强大功能。
例如,给定一个范围,如何实现一个算法,“滑动”连续元素的子集在范围内的给定位置?
可以尝试着花一分钟想想如何实现它,就能理解这个问题的复杂性。
实际上,将元素从第一个到最后一个滑动到pos
,相当于在第一个到pos
的范围上执行循环排列,将最后一个放在开头。这正是std::rotate
所做的:
std::rotate(first, last, pos);
如果last
小于pos
,那么这种方法是有效的,这意味着元素被向前滑动。那么如何将它们向后滑动到pos
小于first
的位置呢?
向后滑动元素也可以归结为执行循环排列,在从pos
到last
的范围内,但这一次将第一个放在开始。所以实现是:
std::rotate(pos, first, last);
现在,如果pos
位于first
和last
之间,这意味着元素需要被滑动到它们已经在的位置,所以不需要做任何事情。
把所有这些放在一起,就是:
if (pos < first) std::rotate(pos, first, last);
if (last < pos) std::rotate(first, last, pos);
基于C++ 11接口,在应用std::rotate
之前返回元素在范围开始处的新位置,甚至可以在滑动发生后返回元素所在的范围:
- 如果
pos < first
,则滑动元素位于pos
和旋转范围(不是滑动范围)的第一个元素的新位置之间,该位置是std::rotate(pos, first, last)
的返回值。 - 如果
last < pos
,则滑动元素位于第一个元素的新位置和pos
之间。
示例:
template <typename RandomAccessIterator>
std::pair<RandomAccessIterator, RandomAccessIterator> slide(RandomAccessIterator first, RandomAccessIterator last, RandomAccessIterator pos)
{
if (pos < first) return { pos, std::rotate(pos, first, last) };
if (last < pos) return { std::rotate(first, last, pos), pos };
return { first, last };
}
即使它与集合本身的排列无关,也可以注意到,在这种情况下返回一对迭代器是有问题的。实际上,所说的“返回”实际上是一个范围,由它的起点和终点表示。
出于这个原因,可以考虑提高这个接口的抽象级别,并按照boost::iterator_range
或range-v3
的iterator_range
类的精神,返回一个更好地表达这个意图的类型。
四、随机排列
重新排序集合元素的一种简单方法是随机洗牌!为此,可以使用std::shuffle
来完成:
#include <random>
#include <algorithm>
#include <vector>
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(begin(numbers), end(numbers), generator);
for (int n : numbers) std::cout << n << ' ';
这里有一个重要的提示:在C++ 11之前,std::random_shuffle
允许实现这个特性。但它的随机性来源(rand()
)并不理想(尽管它有另一个重载,允许提供另一个生成器,但使用起来非常令人讨厌)。因此它在C++ 14中被弃用,并在C++ 17中被删除。所以不应该再用它了。
另一方面,它的替代品std::shuffle
已在C++ 11中引入。那么,如果使用C++ 98,如何在不引入技术债务的情况下对集合进行洗牌呢?
我没有遇到这样的情况,如果有人遇到过这种情况,能分享它就太好了。
五、反向
更简单的排列是对集合中的元素进行反转,可以使用…std::reverse
!
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::reverse(begin(numbers), end(numbers));
输出:
10 9 8 7 6 5 4 3 2 1
六、检查排列
要检查一个集合是否是另一个集合的排列,可以使用is_permutation
。
七、总结
在这里,是否涵盖了STL允许我们更改集合元素顺序的所有方法?没有!还有其他类型的排列,它们有足够的深度,值得单独写一篇文章。