STL中的Sort

下面是对于STL中的sort进行一个小结,包含

insert_sort

bubble_sort

quick_sort

heap_sort

merge_sort

几种

1.stl_sort

这个是STL算法里面的标准sort,因为关联容器如果用红黑树实现本身已经排好序,如果用哈希表实现排序会破坏其结构。

在stl_sort实现中需要随机访问迭代器,所以不能用于list。

而stack又只能从头部访问。

所以对于常用的标准库容器,stl_sort就只适用于vector、deque

stl_sort的具体实现涉及一下细节:

(1)quick_sort

①Medium_of_Three

这是stl_sort算法首选的运行模式。但是为了避免元素输入不够随机造成的尴尬,采取的优化是medium_of_three(三点中值)

取整个序列的头、尾、中央三个位置的元素,然后取中值元素作为pivot元素。

②核心思想--Partition

令头端迭代器first向尾部移动,尾端迭代器last向头部移动。

当*first大于或等于pivot元素时就停下来,当*last小于或等于pivot元素就停下来。

然后检验两迭代器是否交错。

如果不交错,就将两者元素互换,然后各自调整一个位置,继续相同的行为。

如果叫错了,表示整个序列已经调整完毕,first迭代器就指向pivot元素。

(2)heap_sort

当stl_sort使用quick_sort的迭代次数达到一个上限(上限的值和排序元素个数有关),就改为heap_sort。

这里所说的heap都是max_heap即堆顶元素时最大的元素,这样排序下来,刚好是一个升序排序。

①heap构造

A。元素个数不确定,构造堆

每个元素都是先push_back到最后,然后swim上来。

这样需要对每一个元素都进行swim操作。

B。元素个数确定,构造堆

这个时候可以对堆中所有的非叶子节点,从下层到上层依次进行sink操作。

这样可以减少O(N)的操作次数。

②heap排序

在构造好堆以后,堆顶元素就是最大的元素,

将堆顶元素与最末尾的叶子节点对调,然后将heap的节点个数减少1,

然后在对当前的堆顶元素做一次sink操作。

然后重复该操作。

最终就会得到一个升序排序的数组。

注意:一旦对一个堆进行排序,其堆的结构就被破坏了。

(3)insert_sort

当快速排序或者堆排序所要排序的元素个数小于16个的时候,就切换到插入排序。

原因是快速排序、堆排序需要的额外操作蛮多的,对于小数组性能不佳。

而插入排序,针对小数组速度不错,并且对于大部分元素已经排好序的序列效果极好。

①基本原理

对于未排序的序列,从头开始检查,每次检查一个元素。

并且第N次检查的时候,维护一个长度为N的子序列。当N=元素个数的时候,整个序列就排好序了。

②与a[0]比较优化

每次在检查一个元素的时候,不是与已排序的字序列的最后一个元素比较。

而是与已排序的最开始一个元素a[0]比较。

如果value<=a[0],那么使用copy_backward把已排序数组,整体后移一格,然后令a[0]=value

如果value>a[0],再按照正常途径来。

好处:

A。如果新加入元素就是比a[0]小,会省下很多步骤。

B。如果新加入元素比a[0]大,那么后面循环也不用做下标跑到0前面之前的判断。

③固定保存新插入元素优化

将新加入元素保存在value之中,每一次比较并不是交换两个元素,而是把需要移位的元素后移一格,这样少掉很多步骤。

STL中的Sort_第1张图片


每一次swap是3次赋值+1次中间变量的构建与销毁,优化后每次需要一个赋值即可。

2.list::sort

list里面的sort是单独实现的。

在stl源码剖析里面,侯捷先生并没有把这个排序作为一个重点,所以注释很少。

我去看的时候非常烧脑,我就只有跟着代码,一步一步把图画下来看是怎么变化的。

最后看出来了,它其实采用的是一种变异版的merge_sort

在了解具体的实现之前,弄清楚为什么要实现成这样是非常重要的。

(1)为什么不用快排?

①迭代器比较操作

快排需要用到迭代器的比较操作(<,>,>=,<=),来移动迭代器的位置,来判断迭代器是否错位。

而list的迭代器不支持这些操作。

②中值选择

由于采用中间、头、尾元素比较得出中值元素,需要随机访问到中间元素,而list的迭代器,显然不支持随机访问。

(2)为什么不用堆排?

与不能使用快排的原因差不多,list根本无法随机访问,所以无法构建成堆那样来访问父节点与子节点。

(3)为什么不用归并排不用正常的方式?

这里所说的正常的方式是《算法 4th》里面介绍的,两种方式:

自顶向下:通过不断递归划分为子序列,当划分为1个元素大小的子序列时就相当于排好序了,然后进行回归。

自底向上:直接操控下标,将相邻2个元素归并,然后调整步长,归并4个元素的序列,这样logN次,就能归并最终的序列。

①不采用自底向上

这个很简单,因为自底向上需要迭代器随机访问,list迭代器并不能随机访问。

②不采用自顶向下

这个是因为在进行递归的时候,仍然需要计算mid的元素从而进行随机访问,list迭代器是不能随机访问的。

(4)实现细节

①carry元素

每次从需要排序的list中取出一个元素,然后作为中间过程的临时变量。

每一次完成的迭代,最后都会把已经排好序的子序列存放在counter[x]里面,所以每次迭代结束都会把carry交换为空list

②counter数组

这个数组作用是存放中间产生的已排序list子序列。

开始我还觉得这个数组只有64个元素,够用吗?

原来人家是存储的个数按指数增长,最后一个可以存储2^63个元素,就问你怕不怕!

③过程

每次从总list里面,取得一个元素放入carry。

中间有两个标志,来标示现在用到了哪个counter[],并且,这一次需要用到哪个。

然后如果下面的counter放满了,就开始merge,merge过后存放到下一个counter之中。

counter[0]要么null,要么放1个

counter[1]要么null,要么放2个

counter[2]要么null,要么放4个

counter[3]要么null,要么放8个

以此类推,反正就是每次就想上merge成更大的序列,然后放入下一个counter

同时这样放也能表示出任意的个数的list

最后list去玩了,将所有用到了的counter[]进行最后一次merge得到的list就是排好序的list。

3.bubble_sort

这个是参考了简书上面的一篇文章。

笔试时,冒泡排序也要写得优雅出众

普通冒泡排序,进行N次遍历,每次遍历N,N-1,N-2,...,1个元素。

主要就是提出了2点改进

(1)设置已经排好序标志

有一个排好序标志,以其作为循环条件。

初始状态为true。

每次进入循环后,将其设置为false。

如果有交换发生,就将其设置为true。

这样当某次循环中,没有交换发生,表明已经排好序。

那么标志就会提前结束循环。

(2)设置已经排好序位置

因为每次都会冒泡一个元素,所以每次可以确定一个元素的位置。

下一次循环时就可以少遍历一个顺序。

这种做法,每次交换时就记录交换的是哪个位置。

这样最后一次交换记录的就是最后一个失序的位置,之后的位置都已经是有序的了。

那么下次村换就能直接将检索范围,缩小自仍然失序的位置,这样可以节省一些步骤。

你可能感兴趣的:(算法与数据结构)