十大排序及C++实现
No.1 冒泡排序
从左至右依次消除相邻逆序对。
1 void swap(int &a, int &b) 2 { 3 int temp = a; 4 a = b; 5 b = temp; 6 } 7 void bubble_sort(int *array,int length) 8 { 9 for( int i=0; i<length-1; i++ ) 10 { 11 for( int j=0; j<length-i-1; j++ ) 12 { 13 if( array[j]>array[j+1] ) 14 { 15 swap(array[j],array[j+1]); 16 } 17 } 18 } 19 }
每次内层循环后最大值都来到未排序数组最右端,就像大泡泡依次向上冒出。
双重循环,时间复杂度O(n^2)。
冒泡排序简单易理解,但每次都不管三七二十一直接两重循环。一个优化方法是每次内层循环都判断剩余数组是否有序,若有序则排序完成。
1 void bubble_sort(int *array,int length) 2 { 3 for( int i=0; i<length-1; i++ ) 4 { 5 bool is_sorted = true; 6 for( int j=0; j<length-i-1; j++ ) 7 { 8 if( array[j]>array[j+1] ) 9 { 10 swap(array[j],array[j+1]); 11 is_sorted = false; 12 } 13 } 14 if( is_sorted ) break; 15 } 16 }
No.2 选择排序
每次选择最小值,将其与未排序数组最左端交换。即每次内层循环都在未排序数组中使用FindMin()函数,并与未排序数组左端交换。
1 void select_sort(int *array,int length) 2 { 3 for( int i=0; i<length-1; i++ ) 4 { 5 int min = i; //array[i,...,length]最小值下标 6 for( int j=i+1; j<length; j++ ) 7 { 8 if( array[min]>array[j] ){ 9 min = j; 10 } 11 } 12 swap(array[i],array[min]); 13 } 14 }
双重循环,时间复杂度O(n^2)。
No.3 插入排序
算法过程类似于打扑克抓手牌,每次从牌堆抓一张牌后,从右至左在手牌中找到合适位置插入新手牌,保持手牌有序。
1 void insert_sort( int *array, int length ) 2 { 3 for( int i=1; i<length; i++ ) 4 { 5 int j = i - 1; 6 int key = array[i]; /*暂时保存新的手牌 */ 7 while( j>=0 && array[j]>key )/*找到合适位置*/ 8 { 9 array[j+1] = array[j]; 10 j--; 11 } 12 array[j+1] = key; /*将新手牌放入合适位置*/ 13 } 14 }
在最好情况即有序情况下时间复杂度为外层循环O(n),在最坏情况下即逆序情况下时间复杂度为双重循环O(n^2)。
No.4 希尔排序
插入排序的改进版本,又称为缩减增量排序。其思想是每次用增量ht(>=1)作为间隔将数组划分为子问题进行插入排序,每次排序后增量缩减,
重复上述过程,直到增量减为1,此时的排序与插入排序相同。
1 void shell_sort( int *array, int length ) 2 { 3 /*计算初始增量*/ 4 int gap = 1; 5 while( gap<length ) gap = gap * 3 + 1; 6 /*以增量为间隔排序*/ 7 while( gap>0 ) 8 { 9 for( int i=gap; i<length; i++ ) 10 { 11 /*当gap=1时程序与插入排序相同*/ 12 int j = i - gap; 13 int key = array[i]; 14 while( j>=0 && array[j]>key ) 15 { 16 array[j+gap] = array[j]; 17 j -= gap; 18 } 19 array[j+gap] = key; 20 } 21 gap /= 3; /*增量递减*/ 22 } 23 }
通过将完全无序的初始序列,通过缩减增量逐步将序列有序,最后一步的插入排序只需交换个别元素位置,使时间复杂度优于插入排序。
增量的初值及缩减规则没有最优标准。
No.5 归并排序
分治法思想,即划分,排序,合并。每次将序列两两分组,直至序列长度为1,接着逐步合并子序列并保持合并序列有序。
1 const int INF = 10000000; 2 void merge(int *array, int p, int q, int r ) 3 { 4 int length1 = q - p + 1, length2 = r - q; 5 int *A = new int[length1+1]; 6 int *B = new int[length2+1]; //临时数组 7 8 for( int i=p; i<=q; i++ ) 9 { 10 A[i-p] = array[i]; 11 } 12 for( int i=q+1; i<=r; i++ ) 13 { 14 B[i-q-1] = array[i]; 15 } 16 A[length1] = B[length2] = INF; //为统一操作 17 18 int i = 0, j = 0; 19 for( int k=p; k<=r; k++ ) 20 { 21 if( A[i]<B[j] ) 22 { 23 array[k] = A[i]; 24 i++; 25 } 26 else 27 { 28 array[k] = B[j]; 29 j++; 30 } 31 } 32 33 delete []A; delete []B; //释放内存 34 } 35 36 void merge_sort( int *array, int p, int r ) 37 { 38 if( p<r ) 39 { 40 int q = (p+r)/2; 41 merge_sort(array,p,q); 42 merge_sort(array,q+1,r); 43 merge(array,p,q,r); 44 } 45 }
算法每次将问题划分为两个相等的子问题,合并与排序同时进行,需要O(n)时间。
T(n) = 2T(n/2) + O(n) = T(nlogn)。
No.6 快速排序
分治法思想,每次在序列中选取一个基准值,经过一次排序操作后,位于基准值左边的元素不大于基准值、位于基准值右边的元素不小于基准值。
接着在基准值左右两边分别选取新的基准值重复操作,直到左右元素个数不大于1,序列排序完成。
基准值的选取影响算法时间的好坏,这里统一选取数组的最右端作为基准值。
找到基准值后对元素的划分有多种方法。
单边扫描法:从左至右遍历元素,若元素小于基准值,则与左端元素交换。
1 int patition( int *array, int p, int r ) 2 { 3 int x = array[r]; //以最右端元素作为基准值 4 int i = p - 1; 5 for( int j=p; j<r; j++ ) 6 { 7 if( array[j]<x ) 8 {/*将小于基准值元素放在左边*/ 9 i++; 10 swap(array[i],array[j]); 11 } 12 } 13 swap(array[i+1],array[r]); 14 return i+1; 15 } 16 17 void quick_sort( int *array, int p, int r ) 18 { 19 if( p<r ) 20 { 21 int q = patition(array,p,r); 22 quick_sort(array,p,q-1); 23 quick_sort(array,q+1,r); 24 } 25 }
双边扫描法:先从左至右找到大于基准值元素,再从右至左找到小于基准值元素,交换元素,重复上述过程直至左右指针相遇。
1 int patition( int *array, int p, int r ) 2 { 3 int x = array[r]; //以最右端元素作为基准值 4 int i = p, j = r - 1; 5 while( i<=j ) 6 {/* 保证i左边的元素<=x,j右边元素(不包括r)>=x */ 7 while( i<=j && array[i]<=x ) i++; 8 while( i<=j && array[j]>=x ) j--; 9 if( i<=j ) 10 { 11 swap(array[i],array[j]); 12 } 13 } 14 /* 此时array[i]>x */ 15 swap(array[r],array[i]); 16 return i; 17 } 18 19 void quick_sort( int *array, int p, int r ) 20 { 21 if( p<r ) 22 { 23 int q = patition(array,p,r); 24 quick_sort(array,p,q-1); 25 quick_sort(array,q+1,r); 26 } 27 }
快排在最坏情况下:每次划分后一段元素个数为0,另一端为length-1 时间复杂度为O(n^2),在一般情况下时间复杂度为O(nlogn)。
No.7 堆排序
利用数据结构排序,每次取堆顶元素。
自己的思路是利用堆插入元素构建堆,接着依次取出堆顶元素,实现排序。
1 const int Max_N = 10000; 2 3 int sz = 0;//堆元素个数 4 int heap[Max_N]; //堆 5 6 void swap(int &a, int &b) 7 { 8 int temp = a; 9 a = b; 10 b = temp; 11 } 12 13 void push( int x ) 14 { 15 //自己节点编号 16 int i = sz++; 17 18 while( i>0 ) 19 { 20 //父亲节点编号 21 int p = (i-1)/2; 22 23 //如果没有大小颠倒则推出 24 if( heap[p]<=x ) break; 25 26 //把父亲节点数值放下来,将自己下标与之交换 27 heap[i] = heap[p]; 28 i = p; 29 } 30 31 heap[i] = x; 32 } 33 34 int pop() 35 { 36 //最小值 37 int ret = heap[0]; 38 39 //提到根的数值 40 int x = heap[--sz]; 41 42 //从根开始向下交换 43 int i = 0; 44 while( i*2+1<sz ) 45 { 46 int a = 2*i+1, b = 2*i+2; 47 if( b<sz && heap[b]<heap[a] ) a = b; /*a为孩子较小值下标*/ 48 49 //如果没有大小颠倒则退出 50 if( heap[a]>=x ) break; 51 52 //将儿子提上来 53 heap[i] = heap[a]; 54 i = a; 55 } 56 57 heap[i] = x; 58 59 return ret; 60 } 61 62 void heap_sort( int *array, int length ) 63 { 64 for( int i=0; i<length; i++ ) 65 { 66 push(array[i]); 67 } 68 69 for( int i=0; i<length; i++ ) 70 { 71 array[i] = pop(); 72 } 73 }
这种思路比较耗时,构建堆过程中每一个元素都要从堆尾到堆顶做交换比较。
堆排序思路:先将元素加入堆中,再对堆非叶子节点做调整以构建堆,接着依次重复将堆顶下沉到堆尾(与堆尾交换)、重建堆建构,实现排序。
1 void adjust_heap(int *heap,int i,int length) 2 { 3 int x = heap[i]; 4 int sz = length; 5 6 while( i*2+1<sz ) 7 { 8 int a = i*2+1, b = i*2+2; 9 if( b<sz && heap[b]>heap[a] ) 10 { 11 a = b;/*a为孩子较大值下标*/ 12 } 13 14 //如果没有大小颠倒,退出 15 if( heap[a]<=x ) break; 16 17 //将孩子提上来 18 heap[i] = heap[a]; 19 i = a; 20 } 21 22 heap[i] = x; 23 } 24 25 void heap_sort( int *array, int length ) 26 { 27 /*调整构建大顶堆*/ 28 //第一个非叶子节点是(length-1)/2是因为它是最后一个节点的父亲 29 for( int i=(length-1)/2; i>=0; i-- ) 30 {/*从第一个非叶子节点从下至上,从右至左调整结构*/ 31 adjust_heap(array,i,length); 32 } 33 34 for( int i=length-1; i>0; i-- ) 35 { 36 /*将堆顶元素下沉*/ 37 swap(array[0],array[i]); 38 adjust_heap(array,0,i);//重写调整堆结构 39 } 40 }
对于每个元素其调整操作最多需要O(logn),共有n个元素,所以堆排序时间复杂度为O(nlogn)。
堆排序实际上也是一种选择排序,是一种树形选择排序。普通排序在选择最小值过程中已经与此小值做过了比较,但没有保存,所以每次都要重新比较一遍。
而堆排序利用树形结构保存了中间比较的结果,减少比较次数。
No.8 计数排序
不同于上述算法的是计数排序不是基于比较的排序算法。算法的思路是用一个数组count[ i ] : 记录在数组中小于等于元素 i 的个数,即元素 i 的升序排名。
1 void count_sort( int *array, int length ) 2 { 3 int max = array[0]; 4 for( int i=1; i<length; i++ ) 5 { 6 max = max < array[i] ? array[i] : max; 7 } 8 9 int *count = new int[max+1]; //count[0,...,max] 10 memset(count,0,sizeof(int)*(max+1)); 11 for( int i=0; i<length; i++ ) 12 { 13 /*记录 array[i] 出现的次数*/ 14 count[ array[i] ]++; 15 } 16 17 for( int i=1; i<=max; i++ ) 18 { 19 /* 记录 i 在array 中的排名*/ 20 count[i] = count[i-1] + count[i]; 21 } 22 23 int *temp = new int[length]; 24 for( int i=0; i<length; i++ ) 25 { 26 /* 以array[i]的排名放入临时数组 --因为数组从0开始*/ 27 temp[ --count[array[i]] ] = array[i]; 28 } 29 30 for( int i=0; i<length; i++ ) 31 { 32 /* 将排序后元素放回原数组 */ 33 array[i] = temp[i]; 34 } 35 36 delete []count; delete []temp; 37 }
计数排序采用用空间换时间的方法,使用简单,时间复杂度为O(n+max)。同时具有局限性,比如数值的差距很大,那么有大量的空间没有被使用、因为使用
数组下标计数,所以只适用于整数。
计数排序适用于一定范围内的整数排序,且时间复杂度能达到O(n)。
No.9 桶排序
桶排序可以看作是计数排序的优化,算法将待排数据放入一定大小(数值范围)的有序桶里,再对桶里数据进行排序(可自己选择排序算法),
最后按序从桶中取出数据。
1 const int BUCKET_SIZE = 5;//默认用5个桶 2 3 void bucket_sort( int *array, int length ) 4 { 5 /*最大/最小值*/ 6 int max_element,min_element; 7 max_element = min_element = array[0]; 8 for( int i=1; i<length; i++ ) 9 { 10 max_element = max( max_element, array[i] ); 11 min_element = min( min_element, array[i] ); 12 } 13 14 /*差值*/ 15 int dif = max_element - min_element; 16 /*用二维向量作为桶*/ 17 vector<int> bucket_list[BUCKET_SIZE]; 18 19 /*单个桶的区间值*/ 20 float section = (float)dif/(BUCKET_SIZE); 21 22 /*数据入桶*/ 23 for( int i=0; i<length; i++ ) 24 { 25 /*(当前数-min)/区间-1 -> 桶的下标*/ 26 int num = (int)((array[i]-min_element)/section)-1; 27 if( num<0 ) num = 0; 28 bucket_list[num].push_back(array[i]); 29 } 30 31 //桶内排序 32 for(int i=0; i<BUCKET_SIZE; i++ ) 33 { 34 sort(bucket_list[i].begin(),bucket_list[i].end()); 35 } 36 37 int index = 0; 38 for( int i=0; i<BUCKET_SIZE; i++ ) 39 { 40 for( int j=0; j<bucket_list[i].size(); j++ ) 41 { 42 array[index++] = bucket_list[i][j]; 43 } 44 } 45 }
桶排序是非比较整数排序。
最优情况为数据均匀分布于桶中,最坏情况为所有数据在一个桶中,此时的时间复杂度取决于桶内排序算法的选取。所以桶的大小与个数选取十分重要。
当所有数值都有唯一对应的桶时,此时无需做桶内排序,可以看作是计数排序。
No.10 基数排序
非比较整数排序,其思想为按位数逐步划分数据。
1 const int size = 10; 2 void radix_sort( int *array, int length ) 3 { 4 int max_element = array[0]; 5 for( int i=1; i<length; i++ ) 6 { 7 max_element = max( max_element, array[i] ); 8 } 9 10 int pow = 1; 11 while( true ) 12 { 13 vector<int> bucket_list[size]; 14 15 for( int i=0; i<length; i++ ) 16 { 17 int radix = (array[i]/pow)%10; /*当前排序位*/ 18 bucket_list[ radix ].push_back( array[i] ); 19 } 20 21 int index = 0; 22 for( int i=0; i<size; i++ ) 23 { 24 for( int j=0; j<bucket_list[i].size(); j++ ) 25 { 26 array[index++] = bucket_list[i][j];//放回数据 27 } 28 } 29 30 pow *= 10; 31 if( pow>max_element ) break; 32 } 33 }
基数排序可以看作是固定10个桶的桶排序,其排序规则是按照位数,不断的将数据放入桶和从桶中取出数据。
可以按位数从左向右排序,也可从右向左。
主要参考:https://mp.weixin.qq.com/s/Qf416rfT4pwURpW3aDHuCg