排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。分内部排序和外部排序,若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。内部排序的过程是一个逐步扩大记录的有序序列长度的过程。
稳定排序:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
不稳定排序:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
内部排序:排序不占用外存
外部排序:数据过大,排序占用外存
时间复杂度:排序消耗的时间
空间复杂度:排序所需的内存
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
#include
void bubble_sort(int arr[], int len) {
int i, j, temp;
for (i = 0; i < len - 1; i++)
for (j = 0; j < len - 1 - i; j++)
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
int main() {
int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
int len = (int) sizeof(arr) / sizeof(*arr);
bubble_sort(arr, len);
int i;
for (i = 0; i < len; i++)
printf("%d ", arr[i]);
return 0;
}
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
void swap(int *a,int *b){ //交换两个变量
int temp = *a;
*a = *b;
*b = temp;
}
void selection_sort(int arr[], int len) {
int i,j;
for (i = 0 ; i < len - 1 ; i++) {
int min = i;
for (j = i + 1; j < len; j++) //访问未排序的元素
if (arr[j] < arr[min]) //找到目前最小值
min = j; //记录最小值
swap(&arr[min], &arr[i]); //做交换
}
}
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
void insertion_sort(int arr[], int len){
int i,j,key;
for (i=1;i<len;i++){
key = arr[i];
j=i-1;
while((j>=0) && (arr[j]>key)) {
arr[j+1] = arr[j];
j--;
}
arr[j+1] = key;
}
}
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率; 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
t1,t2,……,tk
,其中 ti > tj, tk = 1
;k
,对序列进行 k 趟排序;ti
,将待排序列分割成若干长度为 m
的子序列,分别对各子表进行直接插入排序。仅增量因子为 1
时,整个序列作为一个表来处理,表长度即为整个序列的长度。void shell_sort(int arr[], int len) {
int gap, i, j;
int temp;
for (gap = len >> 1; gap > 0; gap >>= 1)
for (i = gap; i < len; i++) {
temp = arr[i];
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)
arr[j + gap] = arr[j];
arr[j + gap] = temp;
}
}
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代;
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是
O(nlogn)
的时间复杂度。代价是需要额外的内存空间。
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);
}
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序
n
个项目要Ο(nlogn)
次比较。在最坏状况下则需要Ο(n²)
次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(nlogn)
算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了
O(n²)
,但是人家就是优秀,在大多数情况下都比平均时间复杂度为O(nlogn)
的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:
快速排序的最坏运行情况是
O(n²)
,比如说顺序数列的快排。但它的平摊期望时间是O(nlogn)
,且O(nlogn)
记号中隐含的常数因子很小,比复杂度稳定等于O(nlogn)
的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
pivot
);partition
)操作;recursive
)把小于基准值元素的子数列和大于基准值元素的子数列排序;void quickSort() {
int arr[10] = {11, 7, 9, 3, 4, 6, 2, 8, 5, 3};
quick_sort(arr, 0, 9);
for (int i = 0; i < 10; i++)
printf("%d\t", arr[i]);
}
int partition(int arr[], int start, int end) {
int temp = arr[start];
int li = start, ri = end;
while (li < ri) {
while (li < ri && arr[ri] > temp)
ri--;
if (li < ri) {
arr[li] = arr[ri];
li++;
}
while (li < ri && arr[li] < temp)
li++;
if (li < ri) {
arr[ri] = arr[li];
ri--;
}
}
arr[li] = temp;
return li;
}
void quick_sort(int arr[], int start, int end) {
if (start < end) {
int index = partition(arr, start, end);
quick_sort(arr, start, index - 1);
quick_sort(arr, index + 1, end);
}
}
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为
Ο(nlogn)
。
H[0……n-1]
;shift_down(0)
,目的是把新的数组顶端数据调整到相应位置;void heapSort() {
int arr[] = {3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6};
int len = (int)sizeof(arr) / sizeof(*arr);
for (int i = len; i > 1; i--)
heap_Sort(arr, i); //建立堆 每次规模-1
for (int i = 0; i < len; i++)
printf("%d ", arr[i]);
return 0;
}
//构造一个大顶堆并将最大值换至最后一位
void heap_Sort(int arr[], int len) {
int dad = len / 2 - 1; //最后一个父节点
int son = 2 * dad + 1; //该父节点下的首个子节点
while (dad >= 0) {
//判断是否有两个子节点若有则在其中寻找最大子节点
if (son + 1 <= len - 1 && arr[son] < arr[son + 1])
son++;
if (arr[dad] < arr[son]) //若父节点小于子节点则交换位置
swap(&arr[dad], &arr[son]);
dad--; //回退到上一个父节点
son = 2 * dad + 1; //上一个父节点的首个子节点
}
swap(&arr[0], &arr[len - 1]);
}
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为
Ο(n+k)
(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(nlog(n))
的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(nlog(n))
, 如归并排序,堆排序)
i
的元素出现的次数,存入数组C
的第i项C
中的第一个元素开始,每一项和前一项相加)C(i)
项,每放一个元素就将C(i)
减去1
void countingSort() {
int arr[] = {3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6};
int len = (int)sizeof(arr) / sizeof(*arr);
counting_Sort(arr, len);
for (int i = 0; i < len; i++)
printf("%d ", arr[i]);
}
void counting_Sort(int arr[], int LEN) {
//寻找最大最小值
int max = arr[0], min = arr[0], i, j = 0;
for (i = 0; i < LEN; i++) {
if (arr[i] < min)
min = arr[i];
if (arr[i] > max)
max = arr[i];
}
//建立计数数组
int new_len = max - min + 1;
printf("%d\n", new_len);
int conunting_arr[new_len];
//计数
for (i = 0; i < new_len; i++) { //初始化
conunting_arr[i] = 0;
}
for (i = 0; i < LEN; i++) {
conunting_arr[arr[i] - min]++;
}
//根据计数结果进行排序
for (i = 0; i < new_len; i++) {
int index = conunting_arr[i];
while (index != 0) {
arr[j] = i + min;
index--;
j++;
}
}
}
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
void bucketSort() {
int arr[] = {3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6};
int len = (int)sizeof(arr) / sizeof(*arr);
bucket_Sort(arr, len);
for (int i = 0; i < len; i++)
printf("%d ", arr[i]);
}
void bucket_sort(int arr[], int LEN) {
int bucket[5][6] = {0}, i, j, k, temp; //初始化桶,每个桶存放6个数据
//寻找最大最小值
int min = arr[0], max = arr[0];
for (i = 0; i < LEN; i++) {
if (arr[i] < min)
min = arr[i]; //0
if (arr[i] > max)
max = arr[i]; //9
}
//遍历数组,将元素放到对应桶中
int index0 = 0, index1 = 0, index2 = 0, index3 = 0, index4 = 0;
for (i = 0; i < LEN; i++) {
if (arr[i] < min + (max - min + 1) / 5 * 1 && index0 < 7) {
bucket[0][index0] = arr[i];
index0++;
} else if (arr[i] < min + (max - min + 1) / 5 * 2 && (index1 < 7 || index0 >= 7)) {
bucket[1][index1] = arr[i];
index1++;
} else if (arr[i] < min + (max - min + 1) / 5 * 3 && (index2 < 7 || index1 >= 7)) {
bucket[2][index2] = arr[i];
index2++;
} else if (arr[i] < min + (max - min + 1) / 5 * 4 && (index3 < 7 || index2 >= 7)) {
bucket[3][index3] = arr[i];
index3++;
} else if (arr[i] < min + (max - min + 1) / 5 * 5 && (index4 < 7 || index3 >= 7)) {
bucket[4][index4] = arr[i];
index4++;
}
}
//在每个桶中使用冒泡排序
for (i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) { //从小到大
// 外循环为排序趟数,len个数进行len-1趟
for (int k = 0; k < 5 - i; k++) {
// 内循环为每趟比较的次数,第i趟比较len-i次,因为第一次已经将最大的元素冒泡到最后一个位置了
if (bucket[i][k] > bucket[i][k + 1]) {
//相邻元素比较,逆序则将交换位置
temp = bucket[i][k];
bucket[i][k] = bucket[i][k + 1];
bucket[i][k + 1] = temp;
}
}
}
}
//将桶中排序结果还原到原数组中
for (i = 0; i < 5; i++) {
for (j = 0; j < 6; j++) {
arr[i * 6 + j] = bucket[i][j];
}
}
}
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为
O (nlogm)
,其中r
为所采取的基数,而m
为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
arr
为原始数组,从最低位开始取每个位组成radix
数组;radix
进行计数排序(利用计数排序适用于小范围数的特点);void radixSort() {
int arr[] = {31, 25, 33, 40, 78, 26, 1, 52, 88, 63, 22, 44, 69, 42, 17, 10, 11, 28, 19, 47};
int LEN = (int)sizeof(arr) / sizeof(*arr);
int LEVEL = 0; //最大数的位数
//寻找最大值以确定位数
int max = arr[0], i;
for (i = 0; i < LEN; i++) {
if (arr[i] > max)
max = arr[i];
}
for (i = max; i > 0; i /= 10) {
LEVEL++;
}
// printf("%d",LEVEL);
for (i = 0; i < LEVEL; i++) {
radix_sort(arr, LEN, i);
for (int i = 0; i < LEN; i++)
printf("%d ", arr[i]);
printf("\n");
}
for (int i = 0; i < LEN; i++)
printf("%d ", arr[i]);
return 0;
}
void radix_sort(int arr[], int LEN, int level) {
int bucket[10] = {0}, temp[LEN], i, j;
int flag = pow(10, level); //用于确定当前比较的是什么位
//获取个位并进行第一次基数排序
//获取个位存储入桶
for (i = 0; i < LEN; i++) {
bucket[arr[i] / flag % 10]++;
}
bucket[0]--;
for (i = 1; i < 10; i++) {
bucket[i] += bucket[i - 1];
}
//根据桶结果将原数组进行第一次排序后复制到temp数组
//注意这里必须要反向遍历
for (i = LEN - 1; i >= 0; i--) {
temp[bucket[arr[i] / flag % 10]] = arr[i];
bucket[arr[i] / flag % 10]--;
}
//利用temp数组修改原数组
for (i = 0; i < LEN; i++) {
arr[i] = temp[i];
}
}
部分算法动画转载自:十大经典排序算法 | 菜鸟教程