十大经典排序算法
0、排序算法分类
说明:
比较:排序中需要对元素的大小做对比
稳定排序:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
非稳定排序:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
内排序:所有排序操作都在内存中完成;
外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
时间复杂度:排序所耗费的时间;
空间复杂度:排序所耗费的内存;
比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
排序算法分类
十大排序算法主要可以分为两大类:1.比较类排序 2.非比较类排序
1、冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。遍历所有的数据,每次对相邻元素进行两两比较,如果顺序和预先规定的顺序不一致,则进行位置交换;这样一次遍历会将最大或最小的数据上浮到顶端,之后再重复同样的操作,直到所有的数据有序。
数据是反序时,耗费时间最长O(n²);数据是正序时,耗费时间最短O(n)。冒泡排序属于交换排序,是稳定排序。
描述:
1.从左到右,依次比较相邻的元素大小,更大的元素交换到右边(大者右边);
2.从第一组相邻元素比较到最后一组相邻元素,这一步结束最后一个元素必然是参与比较的元素中最大的元素;
3.重复从左到后比较,而前一轮中得到的最后一个元素不参与比较,得出新一轮的最大元素;
4.按照上述规则,每一轮结束会减少一个元素参与比较,直到没有任何一组元素需要比较。
代码实现:
//
// 冒泡排序
//
#include<iostream>
using namespace std;
void bubbleSort(int arr[],int n){
for(int i=0;i<n-1;i++){ //排序趟数
for(int j=0;j<n-1-i;j++){
if(arr[j]>arr[j+1]){
int temp=arr[j]; //交换
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
}
//测试
int main(){
int arr[]={1,4,3,2,5,6,3,2};
int n=sizeof(arr)/sizeof(arr[0]);
bubbleSort(arr,n);
cout << "Sorted array: ";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
2、选择排序(普通选择排序+双向选择排序)
遍历所有数据,先在数据中找出最大或最小的元素,放到序列的起始;然后再从余下的数据中继续寻找最大或最小的元素,依次放到序列中直到所有数据有序。原始数据的排列顺序不会影响程序耗费时间O(n²),相对费时,不适合大量数据排序。
平均时间复杂度为O(n²),空间复杂度为O(1),是一种不稳定的排序算法。
其中双向选择排序: 每一次从无序区间选出最小 + 最大的元素,存放在无序区间的最前和最后,直到全部待排序的数据元素排完 。
描述:
1.在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
2.在剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾;
3.重复步骤 2,直到所有元素排序完毕。
代码实现:
/**
*
* 普通选择排序
*/
void selectionSort(int arr[],int n){
for(int i=0;i<n;i++){
int minIdx=i; //最小元素下标
for(int j=i+1;j<n;j++){
if(arr[minIdx]>arr[j]){
minIdx=j;
}
}
if(minIdx!=i){
int temp=arr[minIdx];
arr[minIdx]=arr[i];
arr[i]=temp;
}
}
}
/**
* 双向选择排序
*/
void biSelectionSort(int arr[],int n){
int left=0,right=n-1;
while(left<=right){
int maxIndex=left;
int minIndex=right;
//无序区间,选出最小 + 最大元素下标
for(int i=left+1;i<right;i++){
if(arr[i]<arr[minIndex]){
minIndex=i;
}
if(arr[i]>arr[maxIndex]){
maxIndex=i;
}
}
if(minIndex!=left){
int temp=arr[minIndex];
arr[minIndex]=arr[left];
arr[left]=temp;
}
if(maxIndex!=left){
int temp1=arr[maxIndex];
arr[maxIndex]=arr[left];
arr[left]=temp1;
}
left++;
right--;
}
}
3、插入排序(直接插入+折半插入)
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。已排序部分也为数组的前部,然后将未排序部分的第一个数插入到已排序部分的合适的位置。
两层循环,第一层划分边界,第二层循环将已排序部分的数从后向前依次与未排序部分的第一个数比较,若已排序部分的数比未排序部分的第一个数大则交换,这样未排序部分的第一个数就插入到已排序部分的合适的位置,然后向后移动边界,重复此过程,直到有序。
反序时,耗费时间最长O(n²);数据是正序时,耗费时间最短O(n)。适用于部分数据已经排好的少量数据排序。平均时间复杂度为O(n²),空间复杂度为O(1),是一种稳定的排序算法。
其中折半插入: 指在有序区间用二分法选择数据应该插入的位置时,因为区间的有序性,可以利用折半查找的思想。
描述:
1.将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2.从头到尾依次扫描未排序序列,将扫描到的每个元素与有序序列的每个元素进行比较,小于哪个有序序列的元素就进行交换,相当于插入到该元素索引位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
代码实现:
/**
* 直接插入排序
*/
void insertSort(int arr[],int n){
//无序
for(int i=1;i<n;i++){
int key=arr[i];
int j=i-1;
//有序
while(j>=0&&key<arr[j]){
arr[j+1]=arr[j];
j--;
}
arr[j+1]=key;// 插入
}
}
/**
* 插入排序+折半插入
*/
void binaryInsertSort(int arr[],int n){
for(int i=1;i<n;i++){
int key=arr[i];
int left=0;
int right=i-1;
while(left<=right){//折半查找
int mid=left+(right-left)/2;
if(arr[mid]<key){
left=mid+1;
}else{
right=mid-1;
}
}
for(int j=i;j>left;j--){//移动
arr[j]=arr[j-1];
}
arr[left]=key;//插入
}
}
4、希尔排序
希尔排序(Shell Sort)也称递减增量排序,是对插入排序的改进,以牺牲稳定性的方法提高效率。基本思路是先将整个数据序列分割成若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时,再对全部数据进行依次直接插入排序,直至所有数据有序。希尔排序算法的性能与所选取的分组长度序列有很大关系,复杂度下界为O(n log²n),在中等规模的数据中表现良好。
平均时间复杂度为O(n^3/2),空间复杂度为O(1),是一种不稳定的排序算法。
描述:
- 先选定一个整数gap(gap一般为数组长度的一半或1/3),把待排序数组以gap为间隔分成个组,个组之间内部使用插入排序
- 排序之后,再将gap/=2或gap/=3
- 重复上述流程,直到gap=1,此时数组已经近乎有序,利用插入排序对近乎有序的数组进行调整。
代码实现:
/**
* 希尔排序
*/
void shellSort(int arr[],int n){
for(int gap=n/2;gap>0;gap/=2){ //间隔
for(int i=gap;i<n;i++){
int temp=arr[i];
int j;
for(j=i-gap;j>=0&&arr[j]>temp;j-=gap){
arr[j+gap]=arr[j];
}
arr[j+gap]=temp;//插入
}
}
}
5、归并排序
归并排序(Merge Sort)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。 该算法时间复杂度为O(n log n)。
描述:
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
代码实现:
/**
* 归并排序merge Sort
*/
//合并,双指针 + 临时数组
void merge(int arr[],int left,int mid,int right){
int temp[right-left+1];//临时数组
int i=left,j=mid+1,k=0;
//在两个指针都没有越过边界的情况下,将两个数组中较小的数放入临时数组,并将指针后移
while(i<=mid&&j<=right){
temp[k++]=arr[i]<arr[j]?arr[i++]:arr[j++];
}
//将未到达边界的数组的剩余元素拷贝到临时数组尾部
while(i<=mid){
temp[k++]=arr[i++];
}
while(j<=right){
temp[k++]=arr[j++];
}
//将临时数组的元素拷贝到原数组
for (int l = 0; l < k; ++l) {
arr[left+l]=temp[l];
}
}
//归并排序,先分再合
void mergeSort(int arr[],int left,int right){
if(left>=right) return;
int mid=left+(right-left)/2;
mergeSort(arr,left,mid);
mergeSort(arr,mid+1,right);
merge(arr,left,mid,right);
}
6、快速排序
快速排序(Quick Sort),是冒泡排序的改进版,之所以“快速”,是因为使用了分治法。它也属于交换排序,通过元素之间的位置交换来达到排序的目的。
基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
**二路快排:**将小于基准值的元素全部放在数组的左边,大于基准值的元素放在数组的右边,不会让等于基准值的元素全部都集中在数组的一边。
描述:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
代码实现:
/**
* 快速排序quick Sort
*/
void quickSort(int arr[],int left,int right){
if(left>=right)return;//返回条件
int pivot=arr[left]; //选取基准问题(第一个元素)
int i=left,j=right;
while(i<j){
while(i<j&&arr[j]>=pivot)j--; // 从右往左找第一个小于基准元素的数
if(i<j)arr[i++]=arr[j];
while(i<j&&arr[i]<=pivot)i++;// 从左往右找第一个大于基准元素的数
if(i<j)arr[j--]=arr[i];
}
arr[i]=pivot;// 将基准元素放到正确的位置
quickSort(arr, left, i - 1); // 对左侧子数组进行快排
quickSort(arr, i + 1, right); // 对右侧子数组进行快排
}
/**
* 二路快排
*/
void biQuickSort(int arr[],int left,int right){
if(left>=right)return;//返回条件
int i=left,j=right;
int pivot=arr[left+(right-left)/2];
while(i<=j){
while(arr[i]<pivot)i++;// i从前向后扫描,碰到第一个大于基点的元素停止
while(arr[j]>pivot)j--;// j从后向前扫描,碰到第一个小于基点的元素停止
if(i<=j){
swap(arr[i],arr[j]);
i++;
j--;
}
}
quickSort(arr,left,j);
quickSort(arr,i,right);
}
7、堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆排序是一种基于二叉堆数据结构的排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。它是一种不稳定的排序算法,平均时间复杂度为 O(nlogn)。
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
具体来说,是从堆的最后非叶子结点元素开始,将它与子节点进行比较,使其满足大顶堆或者小顶堆。
描述:
- 创建一个堆;
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2和3,直到堆的尺寸为 1。
简化一下:①构建大顶堆 → ②交换元素 → ③重构大顶堆 → ④交换元素 → 循环③④ 步
实例:
1.
2.
3.
4.
代码实现:
**
* 堆排序 heapSort
*/
void heapify(int arr[],int n,int i){
int maxIndex=i;
int l=2*i+1;//左孩子,下标从0开始
int r=2*i+2;//右孩子
if(l<n&&arr[l]>arr[maxIndex])
maxIndex=l;
if(r<n&&arr[r]>arr[maxIndex])
maxIndex=r;
if(maxIndex!=i){
swap(arr[i],arr[maxIndex]);
heapify(arr,n,maxIndex);//其子树也调整构建
}
}
void heapSort(int arr[],int n){
for(int i=n/2-1;i>=0;i--){//从最后一个非叶子结点(i=n/2-1)开始
heapify(arr,n,i);
}
for(int i=n-1;i>0;i--){
swap(arr[0],arr[i]);//最后一个数和树顶交换
heapify(arr,i,0);
}
}
8、计数排序
计数排序(Counting Sort)不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
描述:
- 求出待排序数组array的最大值max和最小值min.
- 声明一个大小为 max − min + 1 的辅助数组.
- 将待排序数组从 [min,max] 映射到区间[0,max−min].
- 统计映射后的待排序数组值为 i 的元素的个数,并将其个数作为辅助数组下标为 i 的元素值.
- 利用辅助数组得到排序后的数组.
代码实现:
/**
* 计数排序 Counting Sort
*/
void countingSort(vector<int> v,int n){
auto mydata=minmax_element(v.begin(),v.end());
int min=*mydata.first;
int max=*mydata.second;
// 创建计数数组
vector<int> countArr(max-min+1);
for(int i=0;i<v.size();i++){
++countArr[v[i]-min];
}
//遍历计数数组,对原数组进行排序
int index=0;
for(int i=0;i<countArr.size();i++){
while(countArr[i]-->0){
v[index++]=i+min;
}
}
}
9、 桶排序
桶排序 (Bucket sort)是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
桶排序是稳定排序,但仅限于桶排序本身,假如桶内排序采用了快速排序之类的非稳定排序,那么就是不稳定的。
描述:
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来
代码实现:
/**
* 桶排序 Bucket sort
*/
void bucketSort(vector<int> nums,int n){
auto mydata=minmax_element(nums.begin(),nums.end());
int min=*mydata.first;
int max=*mydata.second;
// 创建桶,桶的编号从0到max-min
vector<vector<int>> buckets(max-min+1);
// 将每个数放入对应的桶中
for (int num : nums) {
buckets[num - min].push_back(num);
}
//对桶中每个元素排序
for (auto& bucket : buckets) {
sort(bucket.begin(), bucket.end());
}
//取出每个桶中的数
int index=0;
for(auto& bucket:buckets){
for(int num:bucket){
nums[index++]=num;
}
}
}
10、基数排序
基数排序(Radix Sort): 首先对每一个数按照最低位进行排序,然后按照下一个高位进行排序,直至排序完成。基数排序利用了桶的思想,基数排序聪明的地方在于他只用10个桶,因为任何十进制数的每一位数都在0-9之间。基数排序是稳定排序。
描述:
- 取得数组中的最大数,并取得位数;
- arr 为原始数组,从最低位开始取每个位组成 radix 数组;
- 对 radix 进行计数排序(利用计数排序适用于小范围数的特点)
代码实现:
/**
* 基数排序
*/
void radixSort(int arr[], int n) {
int maxNum = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > maxNum) {
maxNum = arr[i];
}
}
int digitNum = 1;
while (maxNum / 10 > 0) {
digitNum++;
maxNum /= 10;
}
int count[10];
int bucket[10][n];
int index;
for (int i = 0, radix = 1; i < digitNum; i++, radix *= 10) {
for (int j = 0; j < 10; j++) {
count[j] = 0;
}
for (int j = 0; j < n; j++) {
index = arr[j] / radix % 10; // 获取指定位数的值
bucket[index][count[index]] = arr[j]; // 将数据存入相应的桶中
count[index]++;
}
index = 0;
for (int k = 0; k < 10; k++) {
if (count[k] != 0) {
for (int j = 0; j < count[k]; j++) {
arr[index++] = bucket[k][j]; // 按照顺序将桶中的数据取出
}
}
}
}
}