排序
1.partial_sort
和partial_sort_copy
:
它的功能是将[first,last)中的middle-first个最小元素以递增顺序排序,放在[first, middle)中,剩下的元素不保证顺序,放在剩下的位置中。
使用heap容器相关算法。
make_heap(first, middle);
for (RandomAccessIterator i = middle; i < last; ++i) {
if (*i < *first)
_pop_heap(first, middle, i, T(*i), distance_type(first));
sort_heap(first, middle);
}
回忆这里的_pop_heap
算法的几个参数,first和middle说明了堆的元素的位置范围;i代表的是旧的堆顶元素被弹出后要放在那里,T(*i)则是新的要插入堆中的元素。
2.sort
:
旧版本的排序算法使用快速排序和插入排序。在待排元素数量比较多的时候使用快速排序,递归处理;而如果发现当前待排元素数量比较小时,转而使用插入排序。
之所以这样设计,是因为:
(1)对于小型序列,产生过多的函数递归调用是不划算的
(2)插入排序对于已经基本有序的序列来说十分好用。
而新版本的排序算法使用__introsort_loop
代替了旧版本的快排。由于不当的pivot选择导致不当的分割,快排的时间复杂度可能恶化为
O
(
N
2
)
O(N^2)
O(N2)。而所谓的__introsort_loop
就是在分割行为有恶化为二次倾向的时候,能够自我检测,转而改用Heap Sort,使效率维持在O(nlogn)
。
如何判断这样的倾向呢?原理非常简单,首先根据待排序的元素的数量计算一个界限depth_limit(使用__lg
函数计算,相当于取对数),当发现现在递归的层数已经达到了这个上限depth_limit时,就不再递归处理左右段,而是转而使用partial_sort
函数(上面提到,这个函数使用heap排序)。
所以新版本的排序算法基本上是,优先用快排递归,如果发现递归层数过多就直接用heap排序,如果发现待排序元素的数量小于某个阈值,就直接用插入排序解决。
3.nth_element
:
重新排列[first,last),使迭代器nth所指向的元素,与整个[first, last)排序后该位置的元素一样,并且nth左边的元素都不大于nth右边的元素。
其实就是希望能恰好在快排时找到的pivot正好指向nth。
所以算法也是利用快排的_unguard_partition
算法,三点中值,先随机找到一个pivot,如果这个pivot在序列中的位置恰好就是nth,直接返回;如果nth在pivot的右边,就对右边递归使用_unguard_partition
,否则就在左边使用。当然,如果元素很少,就不用unguard_partition
算法,直接插入排序就行了。
4.mergesort
:归并排序。分治算法。
copy函数
各种方法,强化效率。
1.首先提供一个完全泛化版本:
template <class InputIterator, class OutputIterator>
inline OutputIterator copy(InputIterator first, InputIterator last,
OutputIterator result)
{
return __copy_dispatch<InputIterator, OutputIterator>()
(first, last, result);
}
另有两个重载函数,针对const char*
和const wchar_t*
,内存直接拷贝。
template char* copy(const char* first, const char* last, char* result) {
memmove(result, first, last - first);
return result + (last - first);
}
template wchar_t* copy(const wchar_t* first, const wchar_t* last, wchar_t* result) {
memmove(result, first, sizeof(wchar_t) * (last - first);
return result + (last - first);
}
2.完全泛化的版本使用了__copy_dispatch
,而它实际上是仿函数类。
__copy_dispatch
的完全泛化版本:
template <class InputIterator, class OutputIterator>
struct __copy_dispatch
{
OuputIterator operator() (InputIterator first, InputIterator last, OutputIterator result) {
typedef typename __type_traits<T>::hash_travial_assignment_operator t;
return __copy(first, last, result, iterator_category(first));
}
};
__copy_dispatch
的偏特化版本1,两个参数都是T*
指针形式
template <class T>
struct __copy_dispatch<T*, T*>
{
T* operator() (T* first, T* last, T* result) {
typedef typename __type_traits<T>::has_trivial_assignment_operator t;
return __copy_t(first, last, result, t());
}
};
__copy_dispatch
的偏特化版本2,第一个参数是const T*
形式,第二个参数是T*
指针形式
template <class T>
struct __copy_dispatch<const T*, T*>
{
T* operator() (const T* first, const T* last, T* result) {
typedef typename __type_traits<T>::has_trivial_assignment_operator t;
return __copy_t(first, last, result, t());
}
};
3.先看__copy_dispatch
的完全泛化版本。
它调用了__copy
函数。注意到这里传入__copy
函数的参数有一个iterator_category(first)
,它萃取出了迭代器的类型。根据迭代器的类型,会自动匹配不同版本的__copy
函数。
如果迭代器类型是InputIterator
类型,这种类型的迭代器没有提供减法操作,因此只能通过判断迭代器等同与否,来决定循环是否继续。速度慢。
for ( ; first != last; ++result, ++first)
而如果迭代器类型是RandomAccessIterator
,它提供了迭代器的减法操作,因此可以先算出要循环几次。速度快。
for (Distance n = last - first; n > 0; --n, ++result, ++first)
4.再看两个偏特化版本。
传入__copy_t
的参数有萃取出的类型t,它告诉我们元素类型T是不是具有travial assignment operator。如果是,那么元素的复制可以直接对内存操作,用memmove
完成 。否则,还是得像泛化版本那样处理。
5.copy
函数接收三个迭代器,前两个迭代器指明范围,最后一个指明目的地。实际上,允许这三个迭代器都指向同一个容器。于是存在可能,目的地会落在输入区间内。假如是底层使用的是memmove
,这个函数会先将输入区间的所有内容先复制下来,然后再存进目的地,没有任何问题;但是如果底层没有使用memmove
,那么它的拷贝过程是向前推进的,于是输入区间内后面的元素还没来得及拷贝,就被前面的元素给覆盖了。这种行为是未知的。
二分查询相关函数
1.lower_bound
:在已经排序的区间中寻找元素value。如果value存在于序列中,就返回指向第一个value的迭代器。如果没有,就返回第一个 “不小于value” 的元素的迭代器。
实际上,返回的迭代器指向的位置是value本应该存在的地方。
根据迭代器是单向的还是随机的,提供两个版本的算法。都是使用二分法查询的,唯一的不同就是怎样计算得到middle迭代器。单向的只能一步一步走到中点,随机的可以直接加n。
2.upper_bound
:它致力于找到序列中可以插入value的最后一个位置。它和lower_bound
的差别在于,假如序列中的确存在value,那么它返回的是value的下一个位置的迭代器。
和lower_bound
一样,有两个版本。在算法中体现upper_bound
和lower_bound
区别的点在于,lower_bound
在middle的值 小于 value的时候选择右区间,而 upper_bound
在middle的值 小于等于value的时候选择右区间。
3.binary_search
:利用lower_bound
函数来查询有序序列中是否存在value。
4.equal_range
:在已排序的[first,last)中寻找value。它返回一对迭代器
i
i
i和
j
j
j,在 [i,j) 范围内的每一个元素都等于value。
它会先利用二分法找到元素值是value的middle迭代器,然后分别在左半部分和右半部分使用lower_bound
和upper_bound
找到左右边界。
同样,根据迭代器是单向还是随机,提供了两个版本的函数。
5.implace_merge
:如果一个序列的[first,middle)和[middle,last)部分各自有序,将元素重排,使得整个序列都是有序的。
假如有足够的缓冲区,那么这个操作很容易完成。可以将其中一段完全复制到缓冲区,然后使用merge
操作,让另一个段和缓冲区合并,结果放到前半段中。
但是如果没有足够的缓冲区,两段都不能完全放进缓冲区里面,就需要额外的操作。这时候,我们希望能拆分出更多有序的、更小的段,期望它们能放进缓冲区。具体来说,对于1 3 5 7 | 2 4 6 8 10
,缓冲区放不下4个元素时,使用类似于rotate
的操作,将7 | 2 4
进行交换,于是原来的序列变成了1 3 5 2 4 7 6 8 10
,这样原来两段有序序列变成了4段:1 3 5 | 2 4 | 7 | 6 8 10
,并且每一段的元素数量都变少了。原来最少的一段有4个元素,现在最多只有3个元素。这时候可以分别递归处理1 3 5 | 2 4
和7 | 6 8 10
。这里需要解释,为什么我们之前挑选了前半段的7来做交换,因为7是前半段第一个大于6的,7之前的元素都比6小,再加上2和4也都比6小,所以递归处理后,我们能保证前半段元素肯定比后半段元素小。于是整个序列就变成了有序的。
find和search
find_end
和search
有点像,一个是寻找序列2在序列1中最后出现的位置,一个是寻找序列2在序列1中第一次出现的位置。
find_first_of
和search
不一样,前者是判断序列2中元素第一次出现在序列1中的位置,只要序列2中有任意一个元素出现在序列1中就可以,而后者要求整个序列2整体出现在序列1中。
remove函数
remove
:它并不是直接将所有等于value的元素删除,毕竟不是所有的容器都能方便地进行删除。假设[first,last)范围内一共有n个元素,等于value的有k个,那么它实现的是将n-k个不等于value的元素放在容器的最前面。而剩下的末尾k个元素时废弃的。
排列组合的两个函数
1.next_permutation
:找到下一个排列组合。
排列组合的顺序,举例来说就是:0 1 2 3 4
、0 1 2 4 3
、0 1 3 2 4
、0 1 3 4 2
…
如何找到下一个排列组合?我们不妨将某个排列看作是一个数字,要找出第一个比当前数字大的数。比如对于0 1 2 4 3
,它已经是开头为0 1 2
的数字中最大的了,再找下一个最大的就必须增大2
这一位,2
必须和某一位的数字交换。首先,这一位不能从前面选,否则会导致数字变小(因为虽然这一位变大了,但是高位变小了,高位的影响更大);所以只能从后面选,并且要选到第一个比它大的数。
而且我们还知道,2
这一位后面的元素必定是单调递减的(否则以0 1 2
开头的数字还能更大)。所以从后往前遍历第一个比2
大的元素x
就是我们要的。更神奇的是,把2
交换到这里后,后面这几位仍然是单调递减的(因为2
后面的肯定比它小,前面的肯定比x
大,也就肯定比2
大),于是再进行一次翻转,我们就得到了下一个排列。
...
i = last;
--i;
for (; ;) {
BidirectionalIterator ii = i;
--i;
if (*i < *ii) { // 找到第一对非逆序的相邻元素,i就是我们要交换的高位
BidirectionalIterator j = last;
while (*i < *--j); // 找到第一个比i元素大的元素
iter_swap(i, j); // 交换
reverse(ii, last); // 翻转后面几位
return true;
}
if (i == first) { // 整个序列已经完全逆序,说明已经是最大了,下一个只能回到最小
reverse(first, last);
return false;
}
2.prev_permutation
:前一个排列组合,和next_permutation
的算法基本一样。
...
i = last;
--i;
for (; ;) {
BidirectionalIterator ii = i;
--i;
if (*i > *ii) { // 找到第一对逆序的相邻元素,i就是我们要交换的高位
BidirectionalIterator j = last;
while (*--j < *i); // 找到第一个比i元素小的元素
iter_swap(i, j); // 交换
reverse(ii, last); // 翻转后面几位
return true;
}
if (i == first) { // 整个序列已经完全正序,说明已经是最小了,下一个只能回到最大
reverse(first, last);
return false;
}
自定义的comp函数
comp必须做到,如果comp(a,b)
和comp(b,a)
都返回false,则一定能代表a==b
。 否则,可能会导致某些算法语意错误。
针对迭代器类型设计不同算法的函数:
__copy_dispatch
:
如果迭代器类型是InputIterator类型,这种类型的迭代器没有提供减法操作,因此只能通过判断迭代器等同与否,来决定循环是否继续。速度慢。
而如果迭代器类型是RandomAccessIterator,它提供了迭代器的减法操作,因此可以先算出要循环几次。速度快。
lower_bound
、upper_bound
:
根据迭代器是单向的还是随机的,提供两个版本的算法。都是使用二分法查询的,唯一的不同就是怎样计算得到middle迭代器。单向的只能一步一步走到中点,随机的可以直接加n。
reverse
:
对于有随机定位能力的迭代器和只能双向运动的迭代器,提供了两种不同的算法。同样是两个指针从两边开始往中间运动,一边运动一边交换。区别在于指针什么时候停下,随机定位迭代器提供了<
号,可以在first < last
的时候停下,而双向运动迭代器只能一直判断first != last
。
rotate
:
根据迭代器类型是单向、双向还是随机,提供三种算法。
- 单向算法:前后段元素一一交换,并同时前进。如果有一个走到了终点,而另一个没有走到终点,就重新界定新的前后段元素,继续进行交换。
比如对于a b c | d e f g h
,交换前3个元素和后5个元素,那么经过第一轮交换,变成了d e f | a b c g h
。这时候我们就需要调整后5个元素的位置,于是第二轮的交换任务是交换a b c
和g h
,一直到最后完成所有任务。 - 双向算法:先
reverse
前半段,然后reverse
后半段,最后reverse
整体。 - 随机算法:随机迭代器能直接计算两个迭代器之间的距离。rotate的random access iterator版本