stl算法总结(感觉有意义的)

排序

1.partial_sortpartial_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_boundlower_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_boundupper_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 47 | 6 8 10。这里需要解释,为什么我们之前挑选了前半段的7来做交换,因为7是前半段第一个大于6的,7之前的元素都比6小,再加上2和4也都比6小,所以递归处理后,我们能保证前半段元素肯定比后半段元素小。于是整个序列就变成了有序的。

find和search

find_endsearch有点像,一个是寻找序列2在序列1中最后出现的位置,一个是寻找序列2在序列1中第一次出现的位置。

find_first_ofsearch不一样,前者是判断序列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 40 1 2 4 30 1 3 2 40 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_boundupper_bound
根据迭代器是单向的还是随机的,提供两个版本的算法。都是使用二分法查询的,唯一的不同就是怎样计算得到middle迭代器。单向的只能一步一步走到中点,随机的可以直接加n。

reverse
对于有随机定位能力的迭代器和只能双向运动的迭代器,提供了两种不同的算法。同样是两个指针从两边开始往中间运动,一边运动一边交换。区别在于指针什么时候停下,随机定位迭代器提供了<号,可以在first < last的时候停下,而双向运动迭代器只能一直判断first != last

rotate
根据迭代器类型是单向、双向还是随机,提供三种算法。

  1. 单向算法:前后段元素一一交换,并同时前进。如果有一个走到了终点,而另一个没有走到终点,就重新界定新的前后段元素,继续进行交换。
    比如对于a b c | d e f g h,交换前3个元素和后5个元素,那么经过第一轮交换,变成了d e f | a b c g h。这时候我们就需要调整后5个元素的位置,于是第二轮的交换任务是交换a b cg h,一直到最后完成所有任务。
  2. 双向算法:reverse前半段,然后reverse后半段,最后reverse整体
  3. 随机算法:随机迭代器能直接计算两个迭代器之间的距离。rotate的random access iterator版本
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值