十大排序算法(c++版)
一 综合
1.1、相关概念
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
1.2、算法分类
十大经典排序算法可以分为两大类: 0、非线性时间排序:通过比较来决定元素间的相对次序。时间复杂度最快为O(logN) 1、线性时间排序:通过创建有序的空间,将元素按照一定的规则放入有序空间,再依次取出。以空间来换取时间,可以突破O(logN)

1.3、各算法的时间复杂度

1.4、通用函数及其他
(1)、求数组长度(需要传数组,不要传数组指针)
/* 这里的注意点:在计算数组大小的时候,
一定要注意传入的数组是否为数组指针,
如果传入的是数组指针,sizeof后出来的值为8(64位下),读者应注意。
*/
template <class T>
int len(T& arr){
int length = (int)sizeof(arr)/sizeof(arr[0]);
return length;
}
(2)、大数据样本下四钟最快算法的比较
/*
数据是随机整数,时间单位是秒
数据规模|快速排序 归并排序 希尔排序 堆排序
1000万 | 0.75 1.22 1.77 3.57
5000万 | 3.78 6.29 9.48 26.54
1亿 | 7.65 13.06 18.79 61.31
*/
(3)、时间复杂度的大小比较
N!> x^N >...>3^N >2^N > N^x>...>N^3 >N^2>NlogN>N>logN>1
二 、排序算法的实现
2.1、冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
2.1.1 算法描述
比较相邻的元素。如果第一个比第二个大,就交换它们两个;
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
针对所有的元素重复以上的步骤,除了最后一个;
重复步骤1~3,直到排序完成。
2.1.2 动图演示

#include<iostream> using namespace std; void BubbleSort1(int *a, size_t length) //void BubbleSort(int a[5], int length) //void BubbleSort(int a[], size_t length) { if (a == NULL) return; if (length <= 1) return; //i是次数,j是具体下标 for (int i = 0; i < length - 1; ++i) { for (int j = 0; j<length - 1 - i; ++j) { if (a[j] > a[j + 1]) swap(a[j], a[j + 1]); } } return; } //优化算法1(外层循环优化) void BubbleSort2(int a[5], int length) { if (a == NULL) return; if (length <= 1) return; //i是次数,j是具体下标 for (int i = 0; i < length - 1; ++i) { bool flag = true; for (int j = 0; j<length - 1 - i; ++j) { if (a[j] > a[j + 1]) { swap(a[j], a[j + 1]); flag = false; } } if(flag) break; } return; } //优化算法2(外+内层循环优化) void BubbleSort3(int a[5], int length) { if (a == NULL) return; if (length <= 1) return; int k = length-1,pos = 0; for (int i = 0; i < length - 1; ++i) { bool flag = true; for (int j = 0; j < k; ++j) { if (a[j] > a[j + 1]) { swap(a[j], a[j + 1]); flag = false; pos = j; } } k = pos; if(flag) break; } return; } int main() { int arr[5] = { 7,2,5,1,6 }; int len = sizeof(arr) / sizeof(int); BubbleSort3(arr, len); for (int i = 0; i < len; ++i) { cout << arr[i] <<" "; } getchar(); }
2.2 快速排序
快速排序的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序是一种不稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-Conquer Method)。
2.2.1 排法一 :
该方法的基本思想是:
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。
以一个数组作为示例,取区间第一个数为基准数。

初始时,i = 0; j = 9; X = a[i] = 72
由于已经将a[0]中的数保存到X中,可以理解成在数组a[0]上挖了个坑,可以将其它数据填充到这来。
从j开始向前找一个比X小或等于X的数。当j=8,符合条件,将a[8]挖出再填到上一个坑a[0]中。a[0]=a[8]; i++; 这样一个坑a[0]就被搞定了,但又形成了一个新坑a[8],这怎么办了?简单,再找数字来填a[8]这个坑。这次从i开始向后找一个大于X的数,当i=3,符合条件,将a[3]挖出再填到上一个坑中a[8]=a[3]; j--;
数组变为:

i = 3; j = 7; X=72
再重复上面的步骤,先从后向前找,再从前向后找。
从j开始向前找,当j=5,符合条件,将a[5]挖出填到上一个坑中,a[3] = a[5]; i++;
从i开始向后找,当i=5时,由于i==j退出。
此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将X填入a[5]。
数组变为:

可以看出a[5]前面的数字都小于它,a[5]后面的数字都大于它。因此再对a[0…4]和a[6…9]这二个子区间重复上述步骤就可以了。
对挖坑填数进行总结
1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。
2.j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
2.2.2 排法二:(原文地址)




这里有一点疑问!假如3那个位置是12呐?
这种排法选择最左边的6作为了基准点,还有一个重要的前提是哨兵j先开始走,j--,直到找到比6小的数字停下来,然后哨兵i开始走,直到找到比6大的数字停下来,i++,所以二者一定会相遇(因为i=j的时候就停下来不走了);所以假如3的位置是12,哨兵j就会跨过12,和i在4的位置相遇,同样4和6会交换,最终还是左边小于6,右边大于6。
上图中的流程走完之后(也就是i和j相遇后),接着就对6左边的和右边的数分别执行同样的操作。
2.2.3 随机快速排序
1)最坏情况下分析
当输入序列是正序或者是反序的时候,效率最坏,这时效率是Θ(n2)
2) 最优情况下分析
其他情况下的算法效率趋向于Θ(nlgn)
那么我们如何保证我们总是效率处于最优情况下的呢?这就是随机化快速排序需要解决的问题。
3)、随机化快速排序
我们已经知道,若输入本身已被排序,那么对于快排来说就糟了。那么如何避免这样的情况?一种方法时随机排列序列中的元素;另一种方法时随机地选基准。这便是随机化快速排序的思想,这种快排的好处是:其运行时间不依赖于输入序列的顺序。
经分析, 随机化快排的算法效率是Θ(nlgn)。
实现:我们只需在选取主元时加入随机因素即可。其他与快排一样
#include<iostream> using namespace std; //快速排序第一种排法 void quickSort(int s[],int l,int r) { if(l<r) { int i = l,j = r,x=s[l]; while(i<j) { while(i<j && s[j]>=x) j--; if(i<j) s[i++]=s[j]; while(i<j && s[i]<x) i++; if(i<j) s[j--]=s[i]; } s[i] = x; quickSort(s,l,i-1); quickSort(s,i+1,r); } } //随机化第一种快速排序 void randomquickSort(int s[],int l,int r) { if(l<r) { int i = l,j = r; int m = rand()%(r-l+1)+l; swap(s[m],s[l]); int x=s[l]; while(i<j) { while(i<j && s[j]>=x) j--; if(i<j) s[i++]=s[j]; while(i<j && s[i]<x) i++; if(i<j) s[j--]=s[i]; } s[i] = x; quickSort(s,l,i-1); quickSort(s,i+1,r); } } //快速排序的第二种排法 void quickSort2(int s[],int l,int r) { int i,j,t,temp; if(l>r) return; temp = s[l];//temp中存的就是基准数 i = l; j = r; while(i!=j) { //顺序很重要,要先从右边开始找 while(s[j]>=temp && i<j) j--; //再找左边的 while(s[i]<=temp && i<j) i++; //交换两个数在数组中的位置 if(i<j) swap(s[i],s[j]); } //最终将基准数归位 s[l]=s[i]; s[i]=temp; quickSort2(s,l,i-1); quickSort2(s,i+1,r); } int main() { int array[]={34,65,12,43,67,5,78,10,3,70},k; int len=sizeof(array)/sizeof(int); cout<<"The orginal arrayare:"<<endl; for(k=0;k<len;k++) cout<<array[k]<<","; cout<<endl; // quickSort(array,0,len-1); // randomquickSort(array,0,len-1); quickSort2(array,0,len-1); cout<<"The sorted arrayare:"<<endl; for(k=0;k<len;k++) cout<<array[k]<<","; cout<<endl; system("pause"); return 0; }
2.3 插入排序
直接插入排序(straight insertion sort),有时也简称为插入排序(insertion sort),是减治法的一种典型应用。其基本思想如下:
- 对于一个数组A[0,n]的排序问题,假设认为数组在A[0,n-1]排序的问题已经解决了。
- 考虑A[n]的值,从右向左扫描有序数组A[0,n-1],直到第一个小于等于A[n]的元素,将A[n]插在这个元素的后面。
很显然,基于增量法的思想在解决这个问题上拥有更高的效率。
直接插入排序对于最坏情况(严格递减的数组),需要比较和移位的次数为n(n-1)/2;对于最好的情况(严格递增的数组),需要比较的次数是n-1,需要移位的次数是0。当然,对于最好和最坏的研究其实没有太大的意义,因为实际情况下,一般不会出现如此极端的情况。然而,直接插入排序对于基本有序的数组,会体现出良好的性能,这一特性,也给了它进一步优化的可能性。(希尔排序)。直接插入排序的时间复杂度是O(n^2),空间复杂度是O(1),同时也是稳定排序。

#include<iostream> using namespace std; void insertsort(int s[],size_t length) { if(s==NULL) return; if(length<=1) return; for(int i=1;i<length;i++) { for(int j=i;j>0;j--) if(s[j]<s[j-1]) swap(s[j],s[j-1]); } return; } int main() { int arr[5] = { 7,2,5,1,6 }; int len = sizeof(arr) / sizeof(int); insertsort(arr, len); for (int i = 0; i < len; ++i) { cout << arr[i] <<" "; } getchar(); }
2.4 希尔排序(参考文章)
对中等规模的数据表现不错。
首先它把较大的数据集合分割成若干个小组(逻辑上分组),然后对每一个小组分别进行插入排序,此时,插入排序所作用的数据量比较小(每一个小组),插入的效率比较高

可以看出,他是按下标相隔距离为4分的组,也就是说把下标相差4的分到一组,比如这个例子中a[0]与a[4]是一组、a[1]与a[5]是一组...,这里的差值(距离)被称为增量。
每个分组进行插入排序后,各个分组就变成了有序的了(整体不一定有序)

此时,整个数组变的部分有序了(有序程度可能不是很高)

然后缩小增量为上个增量的一半:2,继续划分分组,此时,每个分组元素个数多了,但是,数组变的部分有序了,插入排序效率同样比高。

同理对每个分组进行排序(插入排序),使其每个分组各自有序

最后设置增量为上一个增量的一半:1,则整个数组被分为一组,此时,整个数组已经接近有序了,插入排序效率高

同理,对这仅有的一组数据进行排序,排序完成。
对各个组进行插入排序的时候并不是先对一个组排序完再对另一个组排序。如下图。

#include<iostream> using namespace std; void shellSort(int s[],int len) { //开始分组,最开始的增量(gap)为数组长度的一半 for(int gap = len/2;gap>0;gap/=2) { //对各个分组进行插入排序,从这里i++(而不是i+=gap)可知, //并不是先把一组排好再排下一组,而是按数组中的顺序排,轮到 //某个元素时,根据gap它的分组自然就清楚了。 for(int i=gap;i<len;i++) { //将s[i]插入到所在分组的正确位置上 int j,value = s[i]; //插入的时候按组进行插入(组内元素两两相隔gap) for(j=i-gap;j>=0&&value<s[j];j-=gap) { s[j+gap]=s[j]; // swap(s[j+gap],s[j]); } s[j+gap]=value;//?因为上边用的是s[j+gap]=s[j],而非交换,换成 swap(s[j+gap],s[j])就不需要了 //下边写法更清楚 // for(int j=i-gap;j>=0;j-=gap) // { // if(s[j]>s[j+gap]) // { // swap(s[j+gap],s[j]); // } // } } } } int main() { int arr[] = { 7,2,5,1,6,9,8,10,3,4,33,23,2,5,6,3,9,4}; int length = sizeof(arr) / sizeof(int); shellSort(arr, length); for (int i = 0; i < length; ++i) { cout << arr[i] <<" "; } getchar(); }
2.5 选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
2.5. 1.算法步骤
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
2.5.2 动图演示

#include<iostream> using namespace std; void selectionsort(int h[], size_t len) { if(h == NULL) return; if(len<=1) return; int minindex; //i是次数,也即排好的个数;j是继续排 for(int i=0;i<len-1;i++) { minindex = i; for(int j = i+1;j<len;j++) { if(h[j]<h[minindex]) minindex = j; } swap(h[i],h[minindex]); } return; } int main() { int arr[5] = { 7,2,5,1,6 }; int len = sizeof(arr) / sizeof(int); selectionsort(arr, len); for (int i = 0; i < len; ++i) { cout << arr[i] <<" "; } getchar(); }
2.6 堆排序
堆是一种特殊的树形数据结构,即完全二叉树。堆分为大根堆和小根堆,大根堆为根节点的值大于两个子节点的值;小根堆为根节点的值小于两个子节点的值,同时根节点的两个子树也分别是一个堆。


# 基本思路
- 步骤一:建立大根堆--将n个元素组成的无序序列构建一个大根堆,
- 步骤二:交换堆元素--交换堆尾元素和堆首元素,使堆尾元素为最大元素;
- 步骤三:重建大根堆--将前n-1个元素组成的无序序列调整为大根堆
重复执行步骤二和步骤三,直到整个序列有序。
# 图示说明
- 步骤一:建立大根堆
① 无序序列建立完全二叉树

② 从最后一个叶子节点开始,从左到右,从下到上调整,将完全二叉树调整为大根堆
a.找到第1个非叶子节点6,由于6的右子节点9比6大,所以交换6和9。交换后,符合大根堆的结构。

c.找到第2个非叶子节点4,由于的4左子节点9比4大,所以交换4和9。交换后不符合大根堆的结构,继续从右到左,从下到上调整。


- 步骤二:交换堆元素(交换堆首和堆尾元素--获得最大元素)

- 步骤三:重建大根堆(前n-1个元素)

- 重复执行步骤二和步骤三,直到整个序列有序

#include<iostream> #include<vector> using namespace std; // 递归方式构建大根堆(len是arr的长度,index是第一个非叶子节点的下标) void adjust(vector<int> &arr, int len, int index) { int left = 2*index + 1; // index的左子节点 int right = 2*index + 2;// index的右子节点 int maxIdx = index; if(left<len && arr[left] > arr[maxIdx]) maxIdx = left; if(right<len && arr[right] > arr[maxIdx]) maxIdx = right; if(maxIdx != index) { swap(arr[maxIdx], arr[index]); adjust(arr, len, maxIdx);//index和maxIdx都只是节点下标,下标是不会变动位置的 //这里参数是maxIdx?(试想当index是0;0,1,2节点中1节点值最大 //那么maxIdx是1,递归调用就会继续向下调整结构) } } // 堆排序 void heapSort(vector<int> &arr, int size) { // 构建大根堆,从第一个非叶子结点(在完全二叉树中, 第一个非叶子结点 其实就是 最后一个叶子结点的父节点) // 从下至上,从右至左调整结构。 for(int i=size/2 - 1; i >= 0; i--) { adjust(arr, size, i); } // 调整堆结构,这里跟构建大根堆不同,没有for循环,因为构建大根堆完成后,数据已经有了大致的大小顺序 //将堆顶元素与末尾元素进行交换后,只要再从堆顶自上而下调整结构即可 for(int i = size - 1; i >= 1; i--) { swap(arr[0], arr[i]); // 将当前最大的放置到数组末尾 adjust(arr, i, 0); // 将未完成排序的部分继续进行堆排序 } } int main() { vector<int> arr = {8, 1, 14, 3, 21, 5, 7, 10}; heapSort(arr, arr.size()); for(int i=0;i<arr.size();i++) { cout<<arr[i]<<endl; } return 0; }
2.7 二路归并排序
二路归并排序主要运用了“分治算法”,分治算法就是将一个大的问题划分为n个规模较小而结构相似的子问题。
这些子问题解决的方法都是类似的,解决掉这些小的问题之后,归并子问题的结果,就得到了“大”问题的解。
二路归并排序主旨是“分解”与“归并”
分解:
1.将一个数组分成两个数组,分别对两个数组进行排序。
2.循环第一步,直到划分出来的“小数组”只包含一个元素,只有一个元素的数组默认为已经排好序。
归并:
1.将两个有序的数组合并到一个大的数组中。
2.从最小的只包含一个元素的数组开始两两合并。此时,合并好的数组也是有序的。

图1. 归并排序过程 图2. 合并两个有序数组
举例说明:
1.图中原始数组为{7,2,5,1,6,9,8,10,3,4},数组中元素的个数为10个。首先将10个元素的数组二分,每次分解后,
数组中元素的数目为原数组的一半。直到分解为只含有一个元素的数组。
2.将小的数组按序合并,每次合并后数组的大小为上层数组的一倍。此时数组中的元素都是按序排列的。
3.在合并两个有序数组。如图2
下面的是自顶向下递归实现2路归并排序:
#include<iostream> using namespace std; void Merge(int *a,int low,int mid,int high) { int i = low,j=mid+1,p=0;//p对应a数组的下标 int *r = new int[high-low+1]; while(i<=mid&&j<=high) { r[p++]=(a[i]<=a[j])?a[i++]:a[j++]; } //当数组大小为奇数时下边两个while就起作用了 while(i<=mid) r[p++]=a[i++]; while (j<=high) r[p++]=a[j++]; for(p = 0,i=low;i<=high;p++,i++) { a[i] = r[p]; } delete [] r; } //自顶向下(递归实现) void Msort(int *a,int low,int high) { if(low<high) { int mid = (low+high)/2; Msort(a,low,mid);//先排左半部分,所有左半部分排好后a数组左半部分有序 Msort(a,mid+1,high); Merge(a,low,mid,high); } } int main() { int a[]={7,2,5,1,6,9,8,10,3,4}; int n = sizeof(a)/sizeof(int); Msort(a,0,n-1); for(int i = 0;i<n;i++) { cout<<a[i]<<" "; } cout<<endl; return 0; }
2.8 计数排序(原文)
计数排序是一种非比较排序:
利用一个 count 数组,统计每个元素 arr[i] 出现的次数 count[arr[i]],count 数组的大小代表的是能排序的元素的最大的值;
然后让count数组中每一个元素 count[i] 等于其与他前面一项 count[i-1] 相加,这时候 count[arr[i]] 表示的值就是小于等于 arr[i] 的元素的个数,这时就找到了 arr[i] 在输出数组中的位置;
最后通过反向填充目标数组tmp,将数组元素arr[i] 放在数组tmp的第count[arr[i]]个位置(下标为count[arr[i]]-1),每放一个元素就将count[arr[i]]递减,可以确保计数排序的稳定性;
看一个例子:

填充过程:

#include<iostream> using namespace std; // 计数排序 count 统计数组, tmp 目标填充数组 // 空间复杂度开销大 void countSort(int *a,int RANGE,int n)//数组中最大的元素不能超过 RANGE { int *count = new int[RANGE+1]();//这里必须初始化数组每一项为0 int *temp = new int[n]; for(int i=0;i<n;i++) { count[a[i]]++; } for(int i=1;i<=RANGE;i++) { count[i]+=count[i-1]; } // 从后向前扫描保证计数排序的稳定性(重复元素相对次序不变) // 当再遇到重复元素时会被放在当前元素的前一个位置上保证计数排序的稳定性 for(int i=n-1;i>=0;i--) temp[--count[a[i]]] = a[i]; //拷贝回原来的数组 for(int i=0;i<n;i++) a[i]=temp[i]; delete [] count; delete [] temp; } int main() { int a[]={7,2,5,1,6,9,8,10,3,4}; int n = sizeof(a)/sizeof(int); countSort(a,10,n); for(int i = 0;i<n;i++) { cout<<a[i]<<" "; } cout<<endl; return 0; }
2.9 基数排序
基数排序图文说明
通过基数排序对数组{53, 3, 542, 748, 14, 214, 154, 63, 616},它的示意图如下:

在上图中,首先将所有待比较树脂统一为统一位数长度,接着从最低位开始,依次进行排序。
1. 按照个位数进行排序。
2. 按照十位数进行排序。
3. 按照百位数进行排序。
排序后,数列就变成了一个有序序列。
从图中可以看出,排十位时,十位相同的情况下,个位也是有序的。
#include<iostream> #include<assert.h> using namespace std; int maxbit(int data[],int n)//辅助函数,求数据的最大位数 { assert(data); int digit = 1; int base = 10; for(size_t i=0;i<n;i++) { while(data[i]>=base) { ++digit; base*=10; } } return digit; } void radixSort(int data[],int n)//基数排序 { int d = maxbit(data,n); int *tmp = new int[n]; int *count = new int[10];//计数器 int i,j,k; int radix = 1; for(i = 1;i<=d;i++)//进行d次排序 { for(j = 0;j<10;j++) { count[j]=0;//每次分配前清空计数器 } for(j = 0;j<n;j++) { k=(data[j]/radix)%10;//统计每个桶中的记录数 count[k]++; } for(j=1;j<10;j++) { count[j] = count[j-1]+count[j]; } //个位数排好后放入原数组data中,排十位时,由于是从后往前放入temp, //所以个位相同的情况下,十位大的会放在前边。 for(j =n-1;j>=0;j--) { k = (data[j]/radix)%10; tmp[count[k]-1] = data[j]; count[k]--; } for(j = 0;j<n;j++) { data[j] = tmp[j]; } radix = radix*10; } delete []tmp; delete []count; } int main() { int a[]={123, 234, 543, 324, 568, 975, 547, 672, 783, 239}; int n = sizeof(a)/sizeof(int); radixSort(a,n); for(int i = 0;i<n;i++) { cout<<a[i]<<" "; } cout<<endl; return 0; }


浙公网安备 33010602011771号