本篇介绍了十大经典排序算法,包括冒泡排序,选择排序,插入排序,希尔排序,快速排序,归并排序,堆排序,计数排序,桶排序,基数排序.
代码的实现是c语言版的.例子中只是简单给出了对于整数数组的排序,若想对其他数据进行排序可以使用函数模板,对类中数据进行排序需要重载比较运算符.
1. 冒泡排序(Bubble Sort)
排序原理
冒泡排序是一种简单直观的排序方式,通过遍历要排序的数列,对其中元素进行两两比较,如果顺序不对就相互交换,重复这一过程直到没有元素需要交换,此时数列已完成排序.
动图演示
代码实现
非递归实现:
void bubblesort(int *arr,unsigned int n){
if(n<2) return;
int count,index; //count--排序的次数 index--要排序的元素下标
int temp,ifswap;
for(count = n-1;count > 0;count--){ //进行n-1次比较
ifswap = 0;
for(index = 0;index < count;index++){ //比较0~count之间的元素,count之后是排序好的
if(arr[index] > arr[index+1]){
temp = arr[index+1];
arr[index+1] = arr[index];
arr[index] = temp;
ifswap = 1;
}
}
if(0 == ifswap) return;
}
}
递归实现:
void bubblesort(int* arr,unsigned int n){
if(n<2) return;
int index;
int temp,ifswap = 0;
for(index = 0;index < n-1;index++){
if(arr[index] > arr[index+1]){
temp = arr[index+1];
arr[index+1] = arr[index];
arr[index] = temp;
ifswap = 1;
}
}
if(0 == ifswap) return;
bubblesort(arr,--n);
}
平均的时间复杂度:O()
平均的空间复杂度:O(1)
2. 选择排序(Selection Sort)
排序原理
选择排序是一种比较简单直观的排序,在未排序的数列中找到最小(大)元素,放到数列的起始位置,再从剩余未排序数列中找到最小(大)元素,放到已排序数列的末尾,一直重复此步骤直到所有元素都排序完毕.
动图演示
代码实现
非递归实现
void selectionsort(int* arr, unsigned int n) {
if (n < 2) return;
int left, right; //每次排序的最左和最右的下标
int index; //每次排序的元素位置
int minpos, maxpos; //每次循环选出的最小值,最大值下标
left = 0, right = n - 1;
while (left < right) {
minpos = maxpos = left;
for (index = left; index <= right; index++) { //在left到right之间选择元素
if (arr[index] < arr[minpos]) minpos = index; //记录更小的元素
if (arr[index] > arr[maxpos]) maxpos = index; //记录更大的元素
}
//如果本次循环的最小元素不是在最左边,就交换最小元素和最左元素的位置
if (minpos != left) swap(arr[left], arr[minpos]);
//如果最大值元素的位置是在最左边,上边已经把最小元素和最左边元素交换了位置,所以要把最大值下标修改成最小值下标
if (maxpos == left) maxpos = minpos;
//如果本次循环的最大元素不在最右边,就交换最右边的元素和最大值
if (maxpos != right) swap(arr[right], arr[maxpos]);
left++; right--;
}
}
递归实现
void selectionsort(int* arr, unsigned int n) {
if (n < 2) return;
int left, right; //每次排序的最左和最右的下标
int index; //每次排序的元素位置
int minpos, maxpos; //每次循环选出的最小值,最大值下标
left = 0, right = n - 1;
minpos = maxpos = left;
for (index = left; index <= right; index++) { //在left到right之间选择元素
if (arr[index] < arr[minpos]) minpos = index; //记录更小的元素
if (arr[index] > arr[maxpos]) maxpos = index; //记录更大的元素
}
//如果本次循环的最小元素不是在最左边,就交换最小元素和最左元素的位置
if (minpos != left) swap(arr[left], arr[minpos]);
//如果最大值元素的位置是在最左边,上边已经把最小元素和最左边元素交换了位置,所以要把最大值下标修改成最小值下标
if (maxpos == left) maxpos = minpos;
//如果本次循环的最大元素不在最右边,就交换最右边的元素和最大值
if (maxpos != right) swap(arr[right], arr[maxpos]);
n -= 2;
selectionsort1(++arr, n);
}
平均时间复杂度:O()
平均空间复杂度:O(1)
3. 插入排序(Insertion Sort)
排序原理
插入排序是一种简单直观的排序方式,原理也很容易理解,通过构建有序序列,对于未排序的数据,在已排序序列中从后向前扫描,找到相应位置并插入。
动图演示
代码实现
void insertsort(int* arr, unsigned int n) {
if (n < 2) return;
int temp, index, num;
for (index = 1; index < n; index++) {
temp = arr[index];
for (num = index - 1; num >= 0 && arr[num] > temp; num--) {
for (num = index - 1; num >= 0; num--) {
if (arr[num] <= temp) break;
arr[num + 1] = arr[num]; //逐个右移
}
arr[num + 1] = temp;
}
}
}
平均时间复杂度:O()
平均空间复杂度:O(1)
对于插入排序,寻找插入位置和移动元素是很浪费时间的操作,需要用一些优化方法:
1. 对已排好的序列使用二分查找法,可以提高寻找的效率.
2. 每次移动多个元素:假设每次拿出n个元素当成一个子序列,先将该n个元素排序,再从后往前寻找插入位置,此时不是一个一个找,而是n个间隔跳跃式寻找,当该子序列中的元素寻找到了位置插入之后,剩余元素按照n-1个间隔跳跃式寻找,直到子序列中每个元素都以插入到正确位置.
3. 数据链表化:当要排序的不是简单的数而是结构体,此时移动元素的开销就会变得很大.而链表的特性是对于插入操作的平均时间复杂度为O(1),这时使用链表对于数据的管理就会节省内存资源.
4. 使用希尔排序:在插入排序的基础上进行优化,性能更高的一种排序方式.
4. 希尔排序(Shell Sort)
排序原理
希尔排序,也称递减增量排序,算法基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列,然后再对每个组进行插入排序,先让数列整体大致有序,然后多次调整分组方式,使数列更加有序,最后再使用一次插入排序,整个数列将全部有序.
选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
按增量序列个数 k,对序列进行 k 趟排序;
每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
动图演示
代码实现
void groupsort(int* arr, unsigned int n, int pos, int step) { //pos--分组起始位置 step--增量
int temp, index, num;
for (index = pos + step; index < n; index += step) {
temp = arr[index];
for (num = index - step; num >= 0 && arr[num] > temp; num -= step) { //从已排序的最右边开始,把大于当前排序的元素后移
for (num = index - step; num >= 0; num -= step) {
if (arr[num] <= temp) break;
arr[num + step] = arr[num]; //逐个后移
}
arr[num + step] = temp; //插入当前排序元素
}
}
}
void shellsort(int* arr, unsigned int n) {
int step;
for (step = n / 2; step > 0; step /= 2) {
for (int i = 0; i < step; i++) { //一共step个组
groupsort(arr, n, i, step);
}
}
}
希尔排序在插入排序的基础上减少了查找次数和移动元素的次数,提高了性能.对于排序中增量的选取,这步骤是影响效率的关键.
通常增量序列选择为: , , 最小增量为1.
如果增量元素不互斥,那么有些增量就会不起作用,于是就有大佬提出了以下几个序列:Hibbard增量序列、Knuth增量序列、Sedgewick增量序列等等.
5. 快速排序(Quick Sort)
排序原理
快速排序是由东尼·霍尔所发展的一种排序算法。采用分治思想将冒泡排序进行改进,大幅提高排序效率.在平均状况下的时间复杂度为O(nlogn),最坏情况下需要O()(比如说对顺序数列进行快速排序).对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序.
基本思想:
1. 先从数列中取出一个元素作为基准数.
2. 扫描数列,将比基准数小的元素全部放到它的左边,大于或等于基准数的元素全部放到它的右边,得到左右两个区间.
3. 对左右区间重复第二步,直到各区间少于两个元素.
动图演示
代码实现
void quicksort(int* arr, unsigned int n) {
if (n < 2) return;
int left, right;
int mvnum;
int temp = arr[0]; //选取最左边的数为基准数
left = 0, right = n - 1, mvnum = 2; //当前应该移动的下标:1--左下标 2--右下标
while (left < right) {
if (2 == mvnum) {
if (arr[right] >= temp) { //右下标元素大于基准数,继续移动右下标
right--; continue;
}
arr[left] = arr[right]; //小于基准数填到左下标中
left++; mvnum = 1; //下次移动左下标
continue;
}
if (1 == mvnum) {
if (arr[left] <= temp) {
left++; continue;
}
arr[right] = arr[left];
right--; mvnum = 2;
continue;
}
}
arr[left] = temp; //左右下标重合,把基准数填入
quicksort(arr, left); //递归左边序列
quicksort(arr + left + 1, n - left - 1); //递归右边序列
}
快速排序的优化方法
1. 由于快速排序采用分治的思想,基准数的选取会影响到递归的深度,从而影响排序效率.
我们可以从数列中选出多个数构成子序列,在这个子序列中选出一个位于中间的数当作基准数,这样进行排序的时候不至于基准数的左右两边的数量差距过大,可以提升排序的效率.
2. 结合插入排序,对于区间小于10个元素的,采用插入排序会使效率更高.
6. 归并排序(Merge Sort)
排序原理
归并排序就是将已经有序的子序列合并,得到一个新的有序序列.作为分治思想的经典应用,归并排序的实现可采用递归和迭代两种方式.
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度.代价是需要额外的内存空间.
算法步骤:
-
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
-
设定两个指针,最初位置分别为两个已经排序序列的起始位置;
-
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
-
重复步骤 3 直到某一指针达到序列尾;
-
将另一序列剩下的所有元素直接复制到合并序列尾。
动图演示
代码实现
递归实现
void _mergesort(int* arr, int* arrtmp, int start, int end) { //arr--待排序数组首地址 arrtmp--用于排序的临时数组首地址
if (start >= end) return;
int mid = start + (end - start) / 2;
int istart1 = start, iend1 = mid;
int istart2 = mid + 1, iend2 = end;
_mergesort(arr, arrtmp, istart1, iend1);
_mergesort(arr, arrtmp, istart2, iend2);
int num = start; //已排序数组计数器
while (istart1 <= iend1 && istart2 <= iend2) { //把区间两边数列合并到已排序的数组中
arrtmp[num++] = arr[istart1] < arr[istart2] ? arr[istart1++] : arr[istart2++];
}
while (istart1 <= iend1) arrtmp[num++] = arr[istart1++]; //把左边数列其他元素追加到已排序数组
while (istart2 <= iend2) arrtmp[num++] = arr[istart2++]; //把右边数列其他元素追加到已排序数组
memcpy(arr + start, arrtmp + start, (end - start + 1) * sizeof(int));
}
void mergesort(int* arr, unsigned int len) { //排序主函数
int* arrtmp = (int*)malloc(len); //分配一个与待排序数组相同大小的数组
if (len < 2) return free(arrtmp);
_mergesort(arr, arrtmp, 0, len - 1); //调用递归函数进行排序
}
非递归实现
int min(int x, int y) {
return x < y ? x : y;
}
void merge_sort(int arr[], int len) {
int *a = arr;
int *b = (int *) malloc(len * sizeof(int));
int seg, start;
for (seg = 1; seg < len; seg += seg) {
for (start = 0; start < len; start += seg * 2) {
int low = start, mid = min(start + seg, len), high = min(start + seg * 2, len);
int k = low;
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
while (start1 < end1 && start2 < end2)
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
while (start1 < end1)
b[k++] = a[start1++];
while (start2 < end2)
b[k++] = a[start2++];
}
int *temp = a;
a = b;
b = temp;
}
if (a != arr) {
int i;
for (i = 0; i < len; i++)
b[i] = a[i];
b = a;
}
free(b);
}
7. 堆排序(Heap Sort)
排序原理
堆排序是利用堆这种数据结构而设计的一种排序算法,堆具备以下特点:
1. 完全二叉树.
2. 二叉树每个节点的值都大于或等于其左右子节点的值,称为最大堆;每个节点的值小于或等于其左右子节点的值,称为最小堆.
3. 最大堆用于升序,最小堆用于降序.平均时间复杂度为O(nlogn).
堆也可转换成数组来表示:
N[i] 的左节点 : N[2i+1] ; N[i] 的右节点 : N[2i+2] ; N[i]的父节点 : N[(i-1)/2].
算法步骤:
-
创建一个堆 H[0……n-1];
-
把堆首(最大值)和堆尾互换;
-
把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
-
重复步骤 2,直到堆的尺寸为 1.
动图演示
代码实现
void swap(int* a, int* b) { int t = *b; *b = *a; *a = t; }
void heapsort(int* arr, int len) {
int index;
for (index = (len - 1) / 2; index >= 0; index--) { //从最后一个父节点开始调整
heapify(arr, index, len - 1);
}
for (index = len - 1; index > 0; index--) { //将第一个元素和已排好的元素前一位做交换,再重新调整
swap(&arr[0], &arr[index]);
heapify(arr, 0, index - 1);
}
}
循环实现heapify
//采用循环实现heapify(元素下沉)
void heapify(int* arr, int start, int end) { //start--待heapify节点的下标 end--待排序数组最后一个元素的下标
int dad = start;
int son = dad * 2 + 1; //确认父节点和左子节点下标
while (son <= dad) {
if ((son + 1 <= end) && (arr[son] < arr[son + 1]))son++; //比较两个子节点大小,选择最大的
if (arr[dad] > arr[son]) return; //如果调整完毕,跳出函数
swap(&arr[dad], &arr[son]); //否则交换父子内容再进行比较
dad = son;
son = dad * 2 + 1;
}
}
递归实现heapify
//采用递归实现heapify
void heapify(int* arr, int start, int end) {
int dad = start;
int son = dad * 2 + 1; //确认父节点和左子节点下标
if (son > end) return;
if ((son + 1 <= end) && (arr[son] < arr[son + 1]))son++; //比较两个子节点大小,选择最大的
if (arr[dad] > arr[son]) return; //如果调整完毕,跳出函数
swap(&arr[dad], &arr[son]); //否则交换父子内容再进行比较
heapify(arr, son, end);
}
8. 计数排序(Counting Sort)
排序原理
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中.作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数.
要想将计数排序的时间复杂度降低到O(n+k),需要满足两个条件:
1. 需要排序的元素是整数.
2. 排序元素的取值要在一定范围内,并且比较集中.
算法的步骤如下:
1. 找出待排序的数组中最大和最小的元素
2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
动图演示
代码实现
基础的计数排序在某些情况下会存在空间浪费的问题,所以要优化就需要在创建计数数组地方做改动.将数组长度定为max-min+1,即不仅要找出最大值,也要找到最小值,根据两者的差确定计数数组的长度.
优化后:
void arrmax(int* arr, unsigned int len, int* min, int* max) {
int i;
*min = *max = arr[0];
for (i = 0; i < len; i++) {
if (*max < arr[i]) *max = arr[i];
if (*min < arr[i]) *min = arr[i];
}
}
void countsort(int* arr, int len) {
if (len < 2) return;
int min, max;
arrmax(arr, len, &min, &max);
int* arrtmp = (int*)malloc((max - min + 1)*sizeof(int)); //临时数组
memset(arrtmp, 0, sizeof(arrtmp));
int i, j, k;
for (i = 0; i < len; i++) { //临时数组计数
arrtmp[arr[i] - min]++;
}
i = 0;
for (j = 0; j < max - min + 1; j++) { //把临时数组计数的内容填充到arr中
for (k = 0; k < arrtmp[j]; k++) {
arr[i++] = j + min;
}
}
free(arrtmp);
}
9. 桶排序(Bucket Sort)
排序原理
假设输入数据服从均匀分布,将数据分到有限的桶中,然后对每个桶分别排序,最后把全部桶的数据都合并.
时间复杂度取决于对每个桶之间数据进行排序的时间复杂度,因为其他部分的时间复杂度都为O(n).显然,桶划分的越小,每个桶所需要的排序时间也越小,但空间消耗就会增大.
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
什么时候最快: 当输入的数据可以均匀分配到每个桶中.
什么时候最慢: 当输入的数据都被分配到了一个桶中.
桶思想:
要想把桶排序的效率发挥出来 , 就应该让它处理均匀分布的数据 . 如果在程序设计过程中处理的数据是分布均匀的那最好 , 如果不是均匀分布的就可以在设计时候让其均匀分布 , 或者转换成均匀的分布 . 比如在游戏账号的查找中 , 可能出现各种非整型的数据 , 可以给其分配一个整数的id , 有了这样一个映射, 查找这种均匀分布的id效率就会提升. 还有在数据库设计时候 , 为了提升效率,把大的表分到不同的库 , 分成不同的表 , 也是桶思想的一种体现 . 这种思想对于我们处理海量数据是非常重要的.
示意图
元素分布在桶中:
元素在每个桶中进行排序:
代码实现
对桶内元素进行排序使用的是冒泡排序
void bubblesort(int* arr, unsigned int n) {
if (n < 2) return;
int count, index; //count--排序的次数 index--要排序的元素下标
int temp, ifswap;
for (count = n - 1; count > 0; count--) { //进行n-1次比较
ifswap = 0;
for (index = 0; index < count; index++) { //比较0~count之间的元素,count之后是排序好的
if (arr[index] > arr[index + 1]) {
temp = arr[index + 1];
arr[index + 1] = arr[index];
arr[index] = temp;
ifswap = 1;
}
}
if (0 == ifswap) return;
}
}
void bucketsort(int*arr,int len) {
int bucket[5][5]; //分配五个桶
int bucketsize[5]; //每个桶中的元素个数计数器
memset(bucket, 0, sizeof(bucket));
memset(bucketsize, 0, sizeof(bucketsize));
//把数组arr数据放到桶中
for (int i = 0; i < len; i++) {
bucket[arr[i] / 10][bucketsize[arr[i] / 10]++] = arr[i]; //可以得到桶的下标
}
//对每个桶进行排序
for (int i = 0; i < 5; i++) {
bubblesort(bucket[i], bucketsize[i]);
}
int i, j, k = 0;
for (i = 0; i < 5; i++) { //把每个桶的数据填充到数组arr中
for (j = 0; j < bucketsize[i]; j++) {
arr[k++] = bucket[i][j];
}
}
}
10.基数排序(Radix Sort)
排序原理
基数排序是桶排序的升级版,也是一种非比较型排序算法.在排序时将整数按位数切割成不同的数字,然后按每个位数分别比较.
计数排序 --- 桶排序 --- 基数排序
这三种排序算法都利用了桶的概念,区别是对桶的使用方法:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
动图演示
代码实现
int arrmax(int* arr, unsigned int len) {
int i;
int max = arr[0];
for (i = 0; i < len; i++) {
if (max < arr[i]) max = arr[i];
}
return max;
}
void _radixsort(int* arr, int len, int exp) {
int num = 0;
int* result = (int*)malloc(len * sizeof(int));
int buckets[10] = { 0 };
if (result) {
for (num = 0; num < len; num++) // 遍历arr,将数据出现的次数存储在buckets中.
buckets[(arr[num] / exp) % 10]++;
for (num = 1; num < 10; num++) //调整buckets各元素的值,调整后的值就是arr中元素在result中的位置.
buckets[num] = buckets[num] + buckets[num - 1];
for (num = len - 1; num >= 0; num--) {
int iexp = (arr[num] / exp) % 10;
result[buckets[iexp] - 1] = arr[num];
buckets[iexp]--;
}
memcpy(arr, result, sizeof(int) * len);
free(result);
}
}
void radixsort(int* arr, int len) {
int max = arrmax(arr, len); //获取数组arr中的最大值
int exp; //排序指数,1 -- 个位排序 10 -- 十位排序...
for (exp = 1; max / exp > 0; exp *= 10) { //按指数位排序
_radixsort(arr, len, exp);
}
}
}