常用排序算法及C++实现
一、相关概念介绍
1.1 排序算法的稳定性
大家在排序时会遇到序列中存在重复元素的情况,此时排序的结果就会不唯一,于是也就引出了算法稳定性的定义。
官方定义如下:
假设ki=kj(1≤i≤n,1≤j≤n,i≠j),且在排序前的序列中ri领先于rj(即i<j)。
如果排序后ri仍领先于rj,则称所用的排序方法是稳定的;反之,若可能
使得排序后的序列中rj领先ri,则称所用的排序方法是不稳定的。
1.2 内排序与外排序
根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为:
内排序和外排序。
内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。
外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过
程需要在内外存之间多次交换数据才能进行。我们这里主要就介绍内排
序的多种方法。
本文主要讲述的对象是内排序。
1.3 排序算法的性能因素
时间性能
排序算法的时间开销是衡量一个排序算法优劣最核心的标志,一个高效率的排序算法一般离不开尽可能少的关键字比较次数和尽可能少的记录移动次数。
辅助空间
辅助存储空间是除了存放待排序所占用的存储空间之外,执行算法所需要
的其他存储空间。
1.4 排序算法的分类
根据排序过程中借助的主要操作,将内排序分为: 插入排序、交换
排序、选择排序和归并排序。
按照算法的复杂度分为两大类,冒泡排序、简单选择排序和直接插入排序属于简单算法,而希尔排序、堆排序、归并排序、快速排序属于改进算法。
二、简单排序算法实现
2.1 冒泡排序
冒泡排序的关键词就是两两比较相邻记录的关键词,如果反序则交换,直到没有反序的记录为止。
原理很简单,咱直接上代码:
vector<int> MySort(vector<int>& arr) {
for(int i = 0; i < arr.size(); ++i){
for(int j = arr.size() - 1; j > i; --j){
if(arr[j-1] > arr[j])
swap(arr[j-1], arr[j]);
}
}
return arr;
}
冒泡排序没有跳跃性的改变元素位置,是稳定的排序算法。
最好情况下时不需要排序,仅需要n-1次的比较,时间复杂度为O(n);最坏情况下时是逆序的,需要n(n-1)/2次比较;总的时间复杂度为O(n2)。
2.2 简单选择排序
简单选择排序的核心思想就是每次用第i个值与序列后面的值去比较,得到最小值的位置并交换位置。
上代码:
vector<int> MySort(vector<int>& arr) {
for(int i = 0; i < arr.size(); ++i){
int min = i;
for(int j = i + 1; j < arr.size(); ++j){
if(arr[j] < arr[min])
min = j;
}
if(min != i){
swap(arr[i], arr[min]);
}
}
return arr;
}
简单选择排序同样没有跳跃性的改变元素位置,是稳定的排序算法。
简单选择排序相比于冒泡排序,减少了交换的次数。而时间复杂度无论是最好还是最坏的比较次数都一样多,总的时间复杂度为O(n2)。
2.3 直接插入排序
直接插入排序则是将一个记录插入到已排序的列表中,得到记录数+1的新列表,这个过程需要将已排序列表中的部分记录向左/右移动。
上代码:
vector<int> MySort(vector<int>& arr) {
int i , j;
for(i = 1; i < arr.size(); ++i){
if(arr[i] < arr[i-1]){ // 只有反序的元素需要排序
int node = arr[i];
for(j = i - 1; arr[j] > node; --j){
arr[j + 1] = arr[j];
}
arr[j+1] = node;
}
}
return arr;
}
直接插入排序没有跳跃性的改变元素位置,是稳定的排序算法。
不难看出,直接插入排序的时间复杂度也是O(n2),但是性能比冒泡和简单选择排序好一点。
三、改进排序算法的实现
3.1 希尔排序
希尔排序是基于直接插入排序的优化,该方法的核心是分割待排序记录,减少待排序记录的个数,采取跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
举个例子说明下,有序列{9,1,5,8,3,7,4,6,2},使用增量increment=3,然后再使用直接插入排序的方法,此时针对的不是整个序列,而是以一个增量间隔的序列,如下标0、3、6,下标1、4、7。这样得到相对有序的序列{4,1,2,8,3,5,9,6,7},注意哈,这里的有序指的是前后间隔增量increment的序列。
好了,理解了希尔排序之后,我们上代码:
vector<int> MySort(vector<int>& arr) {
int i, j;
int increment = arr.size();
do{
increment = increment / 3 + 1;
for(i = increment; i < arr.size(); ++i){
if(arr[i] < arr[i - increment]){ // 只有反序的元素需要排序
int node = arr[i];
for(j = i - increment; j >= 0 && arr[j] > node; j -= increment){
arr[j + increment] = arr[j];
}
arr[j + increment] = node;
}
}
}while(increment > 1);
return arr;
}
很容易理解,直接插入排序其实就是希尔排序中increment为1的一种特殊情况,希尔排序的关键并不是随便分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列,实现跳跃式的移动,使得排序的效率提高。由于记录是跳跃式的移动,希尔排序是一种不稳定的排序算法。
希尔排序的时间复杂度处于O(NlogN)~O(n2),优于前面三种简单排序算法。
3.2 堆排序
堆排序实际上就是利用一个叫“堆”(假设使用大根堆)的特殊数据结构进行排序的方法。其基本思想为:将待排序的序列构成一个大根堆,此时,整个序列的最大值就是堆顶的根节点,将它移走(其实是与堆数组的末尾元素进行交换),再将剩余的n-1个序列重新构造成一个堆,这样就得到n个元素的次大值,如此反复执行就能得到一个有序序列。
关于堆的数据结构是怎么样的,大家可以自行百度了解,咱直接上代码:
vector<int> MySort(vector<int>& arr) {
int i;
for(i = arr.size()/2; i > 0; --i){
HeapAdjust(arr, i, arr.size()-1);
}
for(i = arr.size()-1; i > 0; --i){
swap(arr[0], arr[i]);
HeapAdjust(arr, 0, i-1);
}
return arr;
}
void HeapAdjust(vector<int> &arr, int s, int m){
int tmp, j;
tmp = arr[s];
for(j = 2*s; j <= m; j *= 2){
if(j < m && arr[j] < arr[j+1])
++j;
if(tmp >= arr[j])
break;
arr[s] = arr[j];
s = j;
}
arr[s] = tmp;
}
因为初始构建堆所需的比较次数较多,堆排序它并不适合待排序序列个数较少的情况。由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlogn)。 由于记录的比较与交换是跳跃式进行, 因此堆排序也是一种不稳定的排序方法。
3.3 归并排序
归并排序就是利用归并的思想实现的排序方法。原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到|n/2|(|x|表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。
关于归并排序算法实现,这里上一份其他博主写的:
https://blog.csdn.net/qq_37941471/article/details/80710099
如下:
void __MergeSort( int *a, int left, int right, int * tmp )
{
if( left >= right ) //退出条件
return;
int mid = left+((right-left)>>1);
__MergeSort(a,left,mid,tmp); // 递归左半数组
__MergeSort(a,mid+1,right,tmp); // 递归右半数组
//将排好序的两部分数组归并(排序)
int begin1 = left,end1 = mid;
int begin2 = mid+1,end2 = right;
int index = left;
while( begin1<=end1 && begin2<=end2 )// 循环条件:任一个数组排序完,则终止条件,最后将没有比较完的数组直接一一拷过去
{
if( a[begin1] <= a[begin2] )
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
while( begin1 <= end1 )//右半数组走完了
{
tmp[index++] = a[begin1++];
}
while( begin2 <= end2 )//左半数组走完了
{
tmp[index++] = a[begin2++];
}
//tmp数组已经排好序,将数组内容拷到原数组,递归向上一层走
index = left;
while( index <= right )
{
a[index] = tmp[index];
++index;
}
}
void MergeSort( int *a,size_t n )
{
int *tmp = new int[n]; // 开一个第三方数组来存取左右排好序归并后的序列
__MergeSort(a,0,n-1,tmp);
delete[] tmp; // 最后释放第三方空间
}
归并排序总的时间复杂度为O(nlogn),而且这是归并排序算法中最好、最坏、平均的时间性能。其中需要两两比较,不存在跳跃,因此归并排序是一种稳定的排序算法。
由于归并排序在归并过程中需要与原始记录序列同样数量的存储空间存放归并结果以及递归时深度为log2n的栈空间,因此空间复杂度为O(n+logn)。
归并排序是一种比较占用内存,但却效率高且稳定的算法。
3.4 快速排序
快速排序的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
准确来说,快速排序的核心就是一个根据序列中间值将序列划分成相对有序的过程,如中间值左边均小,中间值右边均大,递归的遍历即可实现整个序列有序。
上代码:
vector<int> MySort(vector<int>& arr) {
QSort(arr, 0, arr.size()-1);
return arr;
}
void QSort(vector<int> &arr, int low, int high){
int pivot;
if(low < high){
pivot = Partition(arr, low, high); // 选出序列的中间值,此时序列的左边均比中间值小,序列的右边均比中间值大
QSort(arr, low, pivot - 1);
QSort(arr, pivot + 1, high);
}
}
int Partition(vector<int> &arr, int low, int high){
int pivot = arr[low];
while(low < high){
while(low < high && arr[high] >= pivot) high--;
swap(arr[high], arr[low]);
while(low < high && arr[low] <= pivot) low++;
swap(arr[high], arr[low]);
}
return low;
}
关于快速排序的优化,大家有兴趣可以去了解下哈,如优化寻找中间值(三数取中)。
由于关键字的比较和交换是跳跃进行的,因此快速排序是一种不稳定的排序方法。
四、算法性能指标总结
这里直接上图: