C++算法从std::sort到排序算法
Posted 小张的code世界
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++算法从std::sort到排序算法相关的知识,希望对你有一定的参考价值。
“ 一个人追求的目标越高,他的才力就发展得越快。”
STL的std::sort是比较常用的一个算法,但是往往和排序相关的时候我们又很喜欢讨论是稳定的排序还是不稳定的排序。今天就揭秘一下std::sort的源码。同时复习一下常用的比较排序算法。
0. std::sort源码分析
std::sort源码分析
STL的std::sort函数是基于Musser在1996年提出的内省排序(Introspective sort)算法实现。这个算法是个缝合怪,它汲取了插入排序、堆排序以及快排的优点:
针对大数据量,使用快排,时间复杂度是O(NlogN);
若快排递归深度超过阈值__depth_limit ,改用堆排序,防止快排递归过深,同时保持时间复杂度仍是O(NlogN);
当数据规模小于阈值_S_threshold时,改用插入排序。
STL的sort()有如下两个版本,第一个版本默认按由小到大的顺序排序,第二个版本接受用户自定义一个仿函数作为排序标准。但是它们的共同点是都只接手两个_RandomAccessIterator(随机存取迭代器)。
template<typename _RandomAccessIterator>
inline void
sort(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
// concept requirements
__glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<
_RandomAccessIterator>)
__glibcxx_function_requires(_LessThanComparableConcept<
typename iterator_traits<_RandomAccessIterator>::value_type>)
__glibcxx_requires_valid_range(__first, __last);
__glibcxx_requires_irreflexive(__first, __last);
std::__sort(__first, __last, __gnu_cxx::__ops::__iter_less_iter());
}
// 可以自定义排序原则
template<typename _RandomAccessIterator, typename _Compare>
inline void
sort(_RandomAccessIterator __first, _RandomAccessIterator __last,
_Compare __comp)
{
// concept requirements
__glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<
_RandomAccessIterator>)
__glibcxx_function_requires(_BinaryPredicateConcept<_Compare,
typename iterator_traits<_RandomAccessIterator>::value_type,
typename iterator_traits<_RandomAccessIterator>::value_type>)
__glibcxx_requires_valid_range(__first, __last);
__glibcxx_requires_irreflexive_pred(__first, __last, __comp);
std::__sort(__first, __last, __gnu_cxx::__ops::__iter_comp_iter(__comp));
}
底层都调用函数std::__sort(),我们先整体看一下它的源代码:
template<typename _RandomAccessIterator, typename _Compare>
inline void
__sort(_RandomAccessIterator __first, _RandomAccessIterator __last,
_Compare __comp)
{
if (__first != __last)
{
// 优先执行内省排序
std::__introsort_loop(__first, __last,
std::__lg(__last - __first) * 2,
__comp);
// 再执行插入排序
std::__final_insertion_sort(__first, __last, __comp);
}
}
// 对std::__lg(__last - __first) * 2的解读
// 这里利用__lg找到2^k <= (__last - __first)的最大值k
// k值用来控制分割恶化的情况,2k为最大递归深度
可以看到首先进行std::__introsort_loop,然后再接一个std::__final_insertion_sort。
std::__introsort_loop()的源码分析如下,introsort的意思是内省式排序,这里是递归地进行内省式排序:
template<typename _RandomAccessIterator, typename _Size, typename _Compare>
void
__introsort_loop(_RandomAccessIterator __first,
_RandomAccessIterator __last,
_Size __depth_limit, _Compare __comp)
{
// enum { _S_threshold = 16 };当元素个数少于_S_threshold时就进行内省排序
while (__last - __first > int(_S_threshold))
{
// 当__depth_limit==0时快排递归深度达到限制
if (__depth_limit == 0) // 分割恶化,改用堆排序
{
std::__partial_sort(__first, __last, __last, __comp);
return;
}
--__depth_limit;
// 如果没有分割恶化,则直接按照快速排序法进行排序
_RandomAccessIterator __cut =
std::__unguarded_partition_pivot(__first, __last, __comp);
// 对右半段递归进行内省式排序
std::__introsort_loop(__cut, __last, __depth_limit, __comp);
// 把__cut赋给__last,对左半段递归进行内省式排序
__last = __cut;
}
}
当元素个数少于_S_threshold时就直接调用std::__final_insertion_sort:排序元素个数大于16,则对两个子列分别按std::__insertion_sort和std::__unguarded_insertion_sort排序,都是插入排序;否则直接按插入排序。
template<typename _RandomAccessIterator, typename _Compare>
void
__final_insertion_sort(_RandomAccessIterator __first,
_RandomAccessIterator __last, _Compare __comp)
{
if (__last - __first > int(_S_threshold))
{
std::__insertion_sort(__first, __first + int(_S_threshold), __comp);
std::__unguarded_insertion_sort(__first + int(_S_threshold), __last,
__comp);
}
else
std::__insertion_sort(__first, __last, __comp);
}
总结:
STL的sort算法,数据量大时采用快速排序法分段递归;但是当分段的元素数量小于某一个门限时,为了避免快排的递归调用带来过大的额外负担,改用插入排序法。在数据量大时,如果发现递归层次过深,就会改用堆排序。
STL sort中快排的实现如下所示:
template<typename _RandomAccessIterator, typename _Compare>
inline _RandomAccessIterator
__unguarded_partition_pivot(_RandomAccessIterator __first,
_RandomAccessIterator __last, _Compare __comp)
{
// 中间位置
_RandomAccessIterator __mid = __first + (__last - __first) / 2;
// 取三点的中值,并置于__first
std::__move_median_to_first(__first, __first + 1, __mid, __last - 1,
__comp);
// 对 [__first, __last) 进行分割,并返回分割点 __cut
return std::__unguarded_partition(__first + 1, __last, __first, __comp);
}
// __unguarded_partition
template<typename _RandomAccessIterator, typename _Compare>
_RandomAccessIterator
__unguarded_partition(_RandomAccessIterator __first,
_RandomAccessIterator __last,
_RandomAccessIterator __pivot, _Compare __comp)
{
while (true)
{
// 先从右往左,找到*__first < *__pivot的元素就停下来
while (__comp(__first, __pivot)) // *__first < *__pivot
++__first;
/*** 跳出循环时,满足:*__first >= *__pivot ***/
--__last;
// 先从做往右,找到*__pivot < *__last的元素就停下来
while (__comp(__pivot, __last)) // *__pivot < *__last
--__last;
/*** 跳出循环时,满足:*__pivot >= *__last ***/
// 表示左右边界相遇,此轮遍历结束,此时的__first就是分割点
if (!(__first < __last))
return __first;
/** 经过上面两个while循环,
* 此时:*__first >= *__pivot && *__pivot >= *__last
* 不满足分割要求,因此需要交换__first, __last 两点的值
*/
std::iter_swap(__first, __last);
++__first;
}
}
从源码中我们发现,没有检查边界的代码:
// 这两个迭代器的操作都没有检查边界
while (__comp(__first, __pivot)) ++__first;
while (__comp(__pivot, __last)) --__last;
这要从__unguarded_partition函数的入参说起:
std::__unguarded_partition(__first + 1, __last, __first, __comp);
//入参分别为:__first+1, __last, __filst
// __first实际上就是__pivot
// 而__pivot是(__first + 1, __mid, __last - 1)三个的中值
std::__move_median_to_first(__first, __first + 1, __mid, __last - 1,
__comp);
这样的话__first一定不是最大的,一定有一个点满足*__first < *__pivot。同样也会有一个点*__pivot < *__last。
std::sort使用陷阱
使用std::sort如果要自定义_Compare,一定要符合严格弱序性质。否则在某些数据下会导致coredump。因为__unguarded_partition中的while的停止条件是:
while (__comp(__first, __pivot)) ++__first;
while (__comp(__pivot, __last)) --__last;
严格弱序在++__first或者--__last过程中不会越界。
如果为非严格弱序,就会越界,如下代码:
#include <vector>
#include <algorithm>
int main(int argc, char const* argv[])
{
std::vector<int> vec{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1};
std::sort(vec.begin(), vec.end(),
[](const int& lhs, const int& rhs)
{
return lhs <= rhs;
});
return 0;
}
tips:
std::sort需要传入_RandomAccessIterator迭代器;
元素之间需要遵守严格弱序性质。
如果STL中std::sort的代码让你感到吃力,那就继续往下看,把排序算法都复习一遍吧。
1. 比较排序算法
在分析了STL中排序算法的源代码之后,我们应该可以深刻地体会到基础的重要性,因此这一节我们把常见的比较排序算法全部总结一遍。比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。比较排序适用于一切需要排序的情况。
冒泡排序法
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
特点:
时间复杂度为:O(n^2)
为稳定排序
算法步骤:
比较相邻的元素,如果前一个比后一个大,则交换讲个元素的位置;
对每一对相邻的元素执行同样的工作(第一步的操作),这样每一遍的最后一个元素将总是最大的;
除了上一遍地最有一个元素外,对剩余元素重复步骤1和步骤2;
直到剩余一个元素。
代码实现:
void bubbleSort(std::vector<int>& array)
{
size_t length = array.size();
if(length == 0 || length == 1)
{
return ;
}
for(size_t time = 0; time < length; ++time)
{
bool isChange = false;
for(size_t index = 0; index < length - 1 - time; ++index)
{
if(array[index + 1] < array[index])
{
int temp = array[index];
array[index] = array[index + 1];
array[index + 1] = temp;
isChange = true;
}
}
if(!isChange)
{
return ;
}
}
}
选择排序法
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小元素,存放到排序序列的起始位置;然后,再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。特点是已排序的最后一个元素总是小于未排序的任一元素。
特点:
时间复杂度为:O(n^2)
为不稳定排序
算法步骤:
初始状态:无序区为R[1..n],有序区为空;
第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
n-1趟结束,数组有序化了。
代码实现:
void selctionSort(std::vector<int>& array)
{
size_t length = array.size();
if(length == 0 || length == 1)
{
return ;
}
for(size_t time = 0; time < length -1; ++time)
{
size_t minIndex = time;
for(size_t index = time + 1; index < length -1; ++index)
{
if(array[index] < array[minIndex]) // 找到最小的元素
{
minIndex = index;
}
}
if(minIndex != time)
{
int temp = array[time];
array[time] = array[minIndex];
array[minIndex] = temp;
}
}
}
插入排序法
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
特点:
时间复杂度为:O(n^2)
为稳定排序
算法步骤:
从第一个元素开始,该元素可以认为已经被排序;
取出下一个元素,在已经排序的元素序列中从后向前扫描;
如果该元素(已排序)大于新元素,将该元素移到下一位置;
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
将新元素插入到该位置后;
重复步骤2~5。
代码实现:
void insertSort(std::vector<int>& array)
{
size_t length = array.size();
if(length == 0 || length == 1)
{
return ;
}
for(size_t time = 0; time < length -1; ++time)
{
int current = array[time + 1];
size_t preIndex = time;
// 向后扫描,找到待排序元素的位置
while(preIndex > 0 && current < array[preIndex])
{
array[preIndex + 1] = array[preIndex];
preIndex--;
}
// 有的编译器在preIndex == 0时再执行preIndex--会有问题
if(preIndex == 0 && current < array[preIndex])
{
array[preIndex + 1] = array[preIndex];
array[preIndex] = current;
continue ;
}
array[preIndex + 1] = current;
}
}
希尔排序法
希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
特点:
时间复杂度为:O(nlog2n)
为不稳定排序
算法步骤:
选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
按增量序列个数k,对序列进行k 趟排序;
每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
举一个示例如下图所示:
代码实现:
void shellSort(std::vector<int>& array)
{
size_t length = array.size();
if(length == 0 || length == 1)
{
return ;
}
int temp, gap = length / 2;
while(gap > 0)
{
for(size_t i = gap; i < length; i++)
{
temp = array[i];
size_t preIndex = i - gap;
while(preIndex >= 0 && array[preIndex] > temp)
{
array[preIndex + gap] = array[preIndex];
preIndex -= gap;
}
array[preIndex + gap] = temp;
}
gap /= 2;
}
}
归并排序法
std::list的sort函数就是使用归并排序算法()。归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。代价是需要额外的内存空间。
特点:
时间复杂度为:O(nlogn)
为稳定排序
算法步骤:
首先将每两个相邻的大小为1的子序列归并,然后将每两个相邻的大小为2的子序列归并,如此反复,直到只剩下一个有序序列。
如下图所示:
代码实现:
void merge(vector<int>& v, int left, int mid, int right)
{
vector<int> temp = v;
int i = left, j = mid + 1;
int index = left;
while(i <= mid || j <= right)
{
if(i > mid){
v[index++] = temp[j];
j++;
}
else if(j > right){
v[index++] = temp[i];
i++;
}
else if(temp[i] < temp[j]){
v[index++] = temp[i];
i++;
}
else{
v[index++] = temp[j];
j++;
}
}
}
void merge_Sort(vector<int>& v, int left, int right)
{
if(left >= right) return;
int mid = (left + right) / 2;
merge_Sort(v, left, mid);
merge_Sort(v, mid + 1, right);
if(v[mid] > v[mid + 1])
{
merge(v, left, mid, right);
}
}
void mergeSort(vector<int>& v)
{
int n = v.size();
merge_Sort(v, 0, n - 1);
}
快速排序法
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分小,然后对两部分进行递归的快速排序。
特点:
时间复杂度为:O(nlogn)
为不稳定排序
算法步骤:
从数组中挑出一个元素,称为“基准”(pivot);
重新排序数组,所有比基准小的数放在基准前面,所有比基准大的元素放在基准后面,这称为分区(partition)操作;
递归地对基准两边的子数组进行排序。
如下图所示,递归区间内使用最后一个元素为支点(pivot):
如图所示,以最左元素为支点:
代码实现:
int getPartition(std::vector<int>&array, int start, int end)
{
int pivot = start;
int current = array[pivot];
while(true)
{
while(array[start] < current)
{
start++;
}
end--;
while(current < array[end])
{
end--;
}
if(!(start < end))
{
return start;
}
int temp = array[start];
array[start] = array[end];
array[end] =temp;
start++;
}
}
void quick_sort(std::vector<int>& array, int start, int end)
{
int partition = getPartition(array, start, end);
if(partition > start)
{
quickSort(array, start, partition);
}
if(partition < end)
{
quickSort(array, partition + 1, end);
}
}
void quickSort(std::vector<int>& array)
{
size_t length = array.size();
if(length == 0 || length == 1)
{
return ;
}
quick_sort(array, 0, length);
}
堆排序法
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
特点:
时间复杂度为:O(nlogn)
为不稳定排序
算法步骤:
将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
代码实现:
void MinHeapFixdown(int *arr, int i, int n)
{
//找到左右孩子
int left = 2 * i + 1;
int right = 2 * i + 2;
//查看是否超出边界
if (left >= n)
return;
//寻找最小值
int min = left;
if (right < n && arr[left] > arr[right])
min = right;
//查看是否比节点小
if (arr[i] <= arr[min])
return;
//交换最小的节点
swap(arr[i], arr[min]);
MinHeapFixdown(arr, min, n);
}
void heapSort(int *arr, int n)
{
//首先进行堆化
for (int i = n / 2 - 1; i >= 0; i--)
MinHeapFixdown(arr, i, n);
for (int i = n - 1; i >= 0; i--)
{
//把堆顶元素和最后一个元素对调。
swap(arr[i], arr[0]);
//缩小堆的范围,对堆顶元素进行向下调整。
MinHeapFixdown(arr, 0, i);
}
}
关于比较排序的算法就到这里。
以上是关于C++算法从std::sort到排序算法的主要内容,如果未能解决你的问题,请参考以下文章
C++中 std::sort 时间复杂度是多少? 是用来sort vector的