其他算法
一.单纯的数据处理
这一小节所列的算法都是很简单的关于数据的算法,都是对数据做一些基本的操作。
adjacent_find(查找相邻而重复的元素)
算法找出第一组满足条件(相等或自设条件)的相邻元素。
count(计数)
将迭代器所指区间的每一个元素拿来和指定值比较并计数。
count_if(在特定条件下计数)
提供一个条件进行计数。
find(循序查找)
循序查找到第一个符合条件的元素位置。
find_if(循序查找符合特定条件者)
提供一个特定条件,循序找到第一个符合的元素位置。
find_end(查找某个子序列的最后一个出现点的首位置)
提供两个序列,在序列一中查找序列二的最后一次出现点,可以自行提供函数。
注意,找的是序列二出现的最后一块地方的第一个位置,不是末尾位置。
因为迭代器型别不同可以采用不同的查找方法,所以这里也是通过提取迭代器特性和函数重载实现多个函数。
这里分为两个函数,一个是forward iterators版,一个是bidirectional iterators版,后一个用到了逆向迭代器,以后看第8章的时候再说。
find_first_of(查找某些元素的最初出现点)
在序列一中查找序列二种任一元素第一次出现的位置,只要是序列二中任何一个元素出现都行。这是一个O(n^2)的算法。可以自行传递函数。
for_each(对区间内的每一个元素实行某操作)
传入一个区间和一个函数,对区间内的每一个元素实行函数操作,但是该函数不能改变元素内容。
generate(以特定操作之结果覆盖序列内的每个元素)
传入一个特定操作,对每个元素使用*first = gen(),注意,这个不算是对每一个元素执行操作,因为这个并没有以每个元素作为参数,而是单纯地对每一个元素执行某一个特定操作,且规定函数操作本身不得有任何参数。
generate_n(以特定操作之结构覆盖序列内前n个元素)
和上一个差不多,不过上一个是每个元素,这个是前n个元素。
includes(是否涵盖于某序列之中)
特别注意,此算法应用于有序区间。
判断序列二中的每一个元素是否出现于序列一,如果序列二有m个相同元素,那么序列一必须有不少于m个的该元素。
有两个版本,第一个版本默认以less升序来判断相同,第二个版本自行传入函数,不过该函数要能表示相等且符合具体的序列情况,即如果序列是升序的话就要用默认的less,如果是降序的话就要用greater。
merge(合并两个序列)
特别注意,此算法应用与有序区间。
将两个有序序列合并至另一序列,合并的结果也是有序的。有两个版本,第一个用于升序,第二个版本可执行传入函数,不过原序列为降序的话最好还是传入greater。
min_element(最小值所在位置)
找到序列中最小值所在的位置,有两个版本,可自行传入函数。
partition(分割)
算法将符合条件的元素移至前端,将不符合条件的元素移至后端,这是通过不断的在前端后端找要交换的元素交换实现的(这个就是快排的那个)。
算法不保证保留元素的原始相对位置,有一个stable_partition可以保留原始相对位置。
remove(移除但不删除)
移除序列中所有与value相等的元素,不删除的意思是容器的大小并未改变,不等于value的元素依次放在first及之后的空间。也就说没有任何元素被删除,只有不与value相等的元素覆盖了前面的元素,所以会剩下残余元素,返回一个指向第一个残余元素的迭代器,可以用erase函数去掉后面的残余元素,remove函数是调用了remove_copy函数。
array不能用remove和remove_if,因为array的残余元素不能被去掉。
remove_copy(移除某类元素并将结果放到另一容器)
原序列不变,将所有不与value相等的元素放到另一容器中,这个容器可以是自身。
remove_if(移除符合特定条件的元素)
移除符合特定条件的元素,也存在残余元素问题。
remove_copy_if(移除符合特定条件的元素并将结果放到另一容器中)
和前面差不多,不过如果要以自身为另一容器,有可能会超出自身大小界限。
replace(替换某类元素)
将区间内所有old_value以new_value覆盖代替。
replace_copy(替换某类元素,将得到的序列复制到新容器)
和replace的行为类似,不过是把新序列复制到另一容器。
replace_if(替换符合特定条件的元素)
自行传入条件pred,符合条件的被替换为new_value。
replace_copy_if(替换符合特定条件的元素,并复制到新容器)
就那意思。
reverse(反转元素次序)
将序列的元素在原容器内反转,与迭代器特性有关,所以还是设计成双层架构。有bidirectional iterator版,还有random access iterator版。
reverse_copy(反转元素次序,并复制到新容器)
实现方式和reverse基本没有什么关联哦,只有一个版本。
rotate(旋转)
将[first,middle)内的元素和[middle,last)内的元素互换。与迭代器移动能力有关,还是设计成双层架构。
共有三个版本:
forward iterator:就前段、后段的元素一一交换,如果有没交换完的继续重复。
bidirectional iterator:先反转前段,再反转后段,再反转整体。
random access iterator:这个算法我不是很理解,但是实践了一下好像很厉害。可以看资料。
参考:
rotate_copy(旋转并复制到新容器)
实现超级简单,先把后段复制到新容器,再把前段接续复制到新容器。
search(查找某个子序列的首个出现点)
在序列一中查找序列二的首个出现点,代码逻辑比较简单,比较有意思的是find_end调用了search函数,并在外面加了层循环,这样就可以找到最后一个出现块的首个出现点了。
search_n(查找连续符合特定条件n次的子序列起点)
在序列中查找连续符合特定条件(默认是与传入值相等)n次的子序列起点,有两个版本,第一个是默认版本,第二个是自行传递函数,代码逻辑比较简单。
swap_ranges(交换)
功能更弱但效率更高的rotate,交换两个序列,序列一长度不大于序列二。当然,其实这个函数挺容易写,其实都不用调用自己都能写。
transform(两个序列交互产生新序列)
有两个版本,第一个不需要两个序列,就是操作序列一的每个元素然后把结果放入新序列。第二个需要两个序列,用二元操作得到新结果放入新序列。
unique(移除重复元素)
移除序列中的重复元素,存在残余元素,特别注意,unique只能移除相邻的重复元素,如果想移除不相邻的重复元素,要先排序。有两个版本,第二个版本是自行定义的相等函数。这个是先用adjacent找到相同元素起点,再调用unique_copy。
unique_copy(移除重复元素得到新序列放入新容器)
代码还是比较简单,也有两个版本,第二个是自行定义的相等。
二.较复杂的算法
lower_bound(在有序区间内插入的合适位置)
lower_bound是二分查找的一种版本,在有序区间内查找,返回合适插入的第一个位置。若有等于value的,返回等于value的第一个元素的迭代器,若没有,找到指向插入不影响有序位置的迭代器。
算法采用的是二分查找,跟迭代器移动特性有关,所以双层架构,同时也有两个版本,第二个版本自行传递函数。
upper_bound(在有序区间内最后一个合适的插入位置)
二分查找的一种版本,在有序区间查找,返回最后一个合适的插入位置,注意和lower_bound的不同,lower_bound可以说是返回第一个合适的插入位置,所以有相同的会指向的第一个相同的,而upper_bound是最后一个,就算有相同的,也会指向相同的下一个。
算法实现和lower_bound差不多,只是判断条件小变了一下,这里可以看出算法的精妙与细微。
binary_search(二分查找)
单纯的在有序区间中的二分查找,直接使用lower_bound,原理很简单就不细说了。
next_permutation(下一个排列组合)
所谓下一个排列组合,是比当前序列稍微大一点点的序列,我们要求的就是这样的序列,对于这个有一个专门的算法书上已经给出,如果当前序列已经是最大的序列,那么会返回false但是同时会把整个序列反转变成最小的序列。
prev_permutation(前一个排列组合)
所谓前一个排列组合,意思和上面相反,也有算法,总之和上面那个就是相反的,比较简单。
random_shuffle(序列随机排列)
算法使用随机函数使得序列随机排列。有两个版本,第二个版本是自行传递函数。
partial_sort/partial_sort_copy(按排序取部分元素至前端)
算法是将前middle-first个最小(或最大)元素移至前端,算是部分排序了,中间用了最大堆来实现。
有两个版本,可自行传递函数。
sort(排序)
sort接受的是randomaccessiterators,所有的关系型容器都有自己的排序算法,而list迭代器不满足要求,所以一般只有数组,vector,deque或其迭代器满足要求的容器使用sort。
sort的排序数据量大时采用quick sort,分段递归,如果分段后数据量小于阈值就用insertion sort,如果递归层次较深,就用heap sort。
Insertion Sort
插入排序,以双层循环的方式进行,为O(n^2),外循环遍历整个区间,内循环遍历子区间,完成每个子区间的排序,外循环结束则排序完成。
算法实现为首先一个外部循环,然后内部循环先判断是否比最左边(最小值)小,如果小直接整体移动即可,如果不小再从相邻的元素一一判断。
Quick Sort
快速排序的策略是选择一个枢纽,然后将比枢纽小的元素放在L段,将比枢纽大的元素放在R段,然后对L,R执行递归操作。
SGI STL使用的是median-of-three QuickSort。
median-of-three(三点中值)
取首尾及中央三个位置元素的中值(即大小为中间值的那个值),为了快速取到中央位置的元素,这里要求迭代器必须是random的。
partitioning(分割)
这里采用的分割方法是first和last向另一端靠近,如果first大于等于枢纽就停下来,如果last大于等于枢纽就停下来,然后两个交换,直到双方交错,此时L为左端到first,R为first到右端。
threshold(阈值)
阈值因设备而异,在数据量比较小的时候插入排序有可能好过快速排序,因为快速排序有大量的递归调用。
final insertion sort
在数据量小于某个阈值时,就可以使用插入排序了。
introsort
可以检测分割情况是否恶化,如果恶化,直接使用堆排序。
SGI STL sort
首先使用快速排序,算出最多允许分割的层数,如果还没达到阈值,且还没超过最多允许分割层数就继续快排,如果超过了就直接堆排序,如果达到阈值了就结束快排使用final insertion sort。
在final insertion sort中首先判断总元素个数是否超过16,如果没有超过直接使用插入排序。如果超过那么对前16个元素使用插入排序,对后面剩下的元素使用不检查边界的插入排序。
关于final insertion sort中超过16个元素的优化想了好久才想明白,首先我们要明确一件事情:不检查边界的插入排序比普通的插入排序效更高,而只有明确最小值在最左边才能不检查边界,因为超过了16个元素而最后分组数据量又小于16个元素,所以最小值肯定再前16个元素,所以对前16个元素进行普通插入排序,对后面剩下的使用不检查边界的插入排序,为什么?仔细看不检查边界的插入排序,它是可以一直比较的,即后面的不检查边界的插入排序只要没停止就可以和前面16个元素比较,而前面16个元素又有最小值,所以相当于最左边有最小值。看了这个,不得不感叹SGI STL中的优化已经丧心病狂了。
equal_range(在有序区间寻找某值区间)
这个算法有点意思,是lower_bound和upper_bound的结合体,即返回一个pair,first是lower_bound的结果,second是upper_bound的结果,即[frist, second)内都是某元素,如果没有某元素,first = second。有两个版本,可自行传递函数。
实现也比较简单,特别注意,同那两个算法一样,都是用于有序区间,也是二分查找的一个版本。
inplace_merge(合并连接序列)
特别注意,应用于有序区间。
merge是应用于两个不同序列,而inplace_merge是应用于连接的两个序列,所以(最好)需要缓冲区,首先将序列短的放入缓冲区,然后直接用merge就行,如果缓冲区放不下任何一个,那么就分割,不过分割前要平衡长度和小排序一下,即找到更长的那个序列,然后用lower_bound或upper_bound找到合适位置进行rotate,再对两段分别进行递归。有两个版本,可自行传递函数。
nth_element(重新安排第n个元素左右两段即第n个元素)‘
默认情况下,算法将第n个元素变为按排序顺序的第n个元素的值,小于第n个元素的排在第n个元素的左边,大于的排在右边。
有两个版本,只接受random型的迭代器。
实现比较有意思,算是小型版的快排,在分段数据量大于3时,使用三点中值法选取枢纽,然后partition,cut与nth进行比较,看nth能落在哪个区间就对哪个区间重复上述过程指导分段数据量小于等于3,那么就对nth所在的分段进行插入排序,这样排完序其他地方的顺序不保证,但是nth位置一定是排好序后相应位置的值,且左右两边符合要求。
这个算法关键是nth位置,中间时候nth的值是变化的。
merge sort(归并排序)
归并排序也是一种常用的排序算法,思想是利用分而治之,把一个区间不断划分,再用inplace_merge合并,并且递归执行直到分段为0或1。倒过来看就是为0或1的分组不断利用inplace_merge合并成有序区间。
归并排序也是O(nlogn),但要用到额外的内存,并且在内存中移动复制也比较耗时,不过概念和实现都比较简单。