1.插入排序(insertion_sort)O(n2)
对少量数据排序很有效,不需要额外的存储空间。待排序列已经是从小到大,最坏就是逆序的时候了。且是稳定的。
#include <stdio.h> #include <assert.h> int exchange(int *a, int i, int j); int insert(int *a, int s, int e); int main(int argc, char *argv[]) { int a[] = {4, 2, 5, 1, 3}; insert(a, 0, 4); int i; for(i=0; i<5; i++) printf("%d\n", a[i]); } int exchange(int *a, int i, int j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; return 0; } int insert(int *a, int s, int e) { int i; for(i=s+1; i<e+1; i++) { int key = a[i];//这里的key是必须保存的,不能为了省一个变量,key用a[i]代替,因为第一次a[j+1]=a[j]时候,a[i](a[j+1])的值被覆盖了 int j=i-1; while(j>=0 && a[j]>key) { a[j+1] = a[j]; j--; } a[++j] = key;//这里的++很重要,可以测试当正好有序时,while条件不成立,必须是自己的值赋给自己,key为a[i],所以a[++j]也必须为a[i] } }
2.归并排序(merge_sort) O(nlogn) 还是稳定的,这个在时间复杂度为O(nlogn)不常见
需要用到递归和分治(两种方法经常结合起来使用来形成高效的算法),推导时间复杂度用到了主定理这个牛x的定理。
#include<iostream> using namespace std; void merge_sort(int *a, int s, int e); void merge(int *a, int s, int m, int e); int main() { int a[] = {2, 4, 1, 9, 7, 8, 10, 3, 5, 6}; merge_sort(a, 0, 9); for(int i=0; i<10; i++) printf("%d\n", a[i]); return 0; } void merge_sort(int *a, int s, int e) { if(s<e) { int m = s + (e - s)/2; merge_sort(a, s, m); merge_sort(a, m+1, e); merge(a, s, m, e); } } void merge(int *a, int s, int m, int e) { int left_len = m-s+1; int right_len = e-m; int *left = (int *)malloc(left_len*sizeof(int)); int *right = (int *)malloc(right_len*sizeof(int)); for(int i=0; i<left_len; i++) left[i] = a[i+s]; for(int j=0; j<right_len; j++) right[j] = a[j+m+1]; int p = 0; int q = 0; int k = s; while(p<left_len && q<right_len) { if(left[p] < right[q]) a[k++] = left[p++]; else a[k++] = right[q++]; } if(p == left_len) while(q < right_len) a[k++] = right[q++]; if(q == right_len) while(p < left_len) a[k++] = left[p++]; free(left); free(right); }
3.冒泡排序O(n2)
稳定,代码容易敲。
#include <stdio.h> #include <assert.h> int exchange(int *a, int i, int j); int maopao(int *a, int s, int e); int main(int argc, char *argv[]) { int a[] = {4, 2, 5, 1, 3}; maopao(a, 0, 4); int i; for(i=0; i<5; i++) printf("%d\n", a[i]); } int maopao(int *a, int s, int e) { int len = e-s+1; int i; int j; for(j=len-1; j>0; j--) for(i=s; i<j+s; i++) if(a[i]>a[i+1]) exchange(a, i, i+1); return 0; } int exchange(int *a, int i, int j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; return 0; }
4.堆排序O(nlogn)
不稳定。但是时间复杂度很稳定,不管什么情况都是nlogn,可以确切知道在什么时间范围内。有特殊的应用像什么优先级队列,前多少大的问题。
#include <stdio.h> #include <iostream> using namespace std; int build_max_heap(int *a, int heap_size); //这里heap_size理解为下标的最大值加一,即为堆里面元素的个数 int heap_sort(int *a, int len); //这里排序的时候,数组a的下标必须是从0开始,否则不会满足父结点和左右孩子结点的关系 int max_heapify(int *a, int i, int heap_size); //调整使得a[i]为父节点的堆为大顶堆 int exchange(int *a, int i, int j); int main(int argc, char *argv[]) { int a[] = {1, 3, 5, 2, 4}; int len = 5; heap_sort(a, 5); int i; for(i=0; i<5; i++) printf("%d\n", a[i]); return 0; } int exchange(int *a, int i, int j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; return 0; } int max_heapify(int *a, int i, int heap_size) { int largest = i; if(2*i+1 <= heap_size-1 && a[2*i+1] > a[i]) //这里,<=非常容易写成>=!! largest = 2*i+1; if(2*i+2 <= heap_size-1 && a[2*i+2] > a[largest]) largest = 2*i+2; if(largest != i) { exchange(a, i, largest); //这一句不要忘了,在递归前交换 max_heapify(a, largest, heap_size); } return 0; } int build_max_heap(int *a, int heap_size) { int i; for(i = heap_size/2; i>=0; i--) max_heapify(a, i, heap_size); return 0; } int heap_sort(int *a, int len) { build_max_heap(a, len); int i; for(i= len-1; i>0; i--) { exchange(a, 0, i); max_heapify(a, 0, i); //这里的heap_size参数是变化的 } }
5.快排O(nlogn)
用的最多了,虽然与堆排序时间复杂度一样,但大多数情况下,前面的常数较小,比堆排序多数情况下速度快。
#include <stdio.h> #include <assert.h> void quickSort(int *a, int s, int e); int partition(int *a, int s, int e); void exchange(int *a, int i, int j); int main(int argc, char *argv[]) { int a[] = {1, 3, 4, 2, 5}; quickSort(a, 0, 4); int i; for(i=0; i<5; i++) printf("%d\n", a[i]); return 0; } void exchange(int *a, int i, int j) { assert(a != NULL); int temp = a[i]; a[i] = a[j]; a[j] = temp; } void quickSort(int *a, int s, int e) { if(s < e) { int temp = partition(a, s, e); quickSort(a, s, temp-1); //这里,是递归调用quickSort,有时候写成partition quickSort(a, temp+1, e); } } int partition(int *a, int s, int e) { int key = a[e]; int i = s - 1; //注意i的开始位置,很容易写成i = s。可以想象一种极端的情况,s到e-1的位置上都比e位置的大,如果i为s结果就错了 int j; for(j=s; j<e; j++) { if(a[j]<key) { i++; exchange(a, i, j); } //有时候在if后面会多写一行j++,忘记for循环里面的++ } i++; exchange(a, i, e); return i; }
6.计数排序
对数据有要求,数据必须知道数据的范围,假设每个数都在0~k之间,当k=O(n)时侯,计数排序的运行时间为O(n),空间复杂度O(n)。为稳定的,对数据有特定的需求,例如当对年龄排序的时候就特别的合适。(与上面的排序算法有本质的不同,不需要比较)
7.基数排序
n个d位数,每一位有k中可能的取值,如果所用的稳定排序需要O(n+k)时间,基数排序能以O(d(n+k))的时间对这些数排序。
8.桶排序
桶排序
这个排序算法其实还是有很多应用的,桶排序算法假设输入由一个随机过程产生,该过程将元素均匀而独立额分布在一个区间上。像实际应用中,并不是严格的要求均匀分布,大量学生成绩,年龄这些有范围的数据,都可以尝试用桶排序来,最佳的时间复杂度为O(n),最差的为O(n2)完全在一个桶内。一般用一个链表来表示一个桶,空间复杂度低为O(n),否则用数组的话空间复杂度要O(n*k),k为桶的个数。
算法步骤也简单:
1.根据数据特征划分桶
2.把数据输入到各个桶中
3.各个桶排序(一般2,3两步可以一起用插入排序做了)
4.把各个桶元素按照顺序列出来
#include <iostream> using namespace std; typedef struct element { int value; struct element *next; }Element; void bucket_sort(int *values, int s, int e); int main() { int values[] = {23, 12, 34, 55, 66, 43, 67, 89, 90, 11, 20, 30, 40, 55, 99, 33, 22, 77, 44, 11}; bucket_sort(values, 0, 19); for(int i=0; i<20; i++) printf("%d\t", values[i]); printf("\n"); return 0; } void bucket_sort(int *values, int s, int e) { int low_end = 0; int high_end = 100; //不能等于100 int interval = 10; int bucket_num = (high_end - low_end)/interval; Element **bucket = (Element **)calloc(bucket_num, sizeof(Element *)); //-----把数据放入桶并排序----- for(int i=s; i<=e; i++) { Element *temp = bucket[values[i]/interval]; Element *insert = (Element *)malloc(sizeof(Element)); insert->value = values[i]; if(temp == NULL) //桶内没有元素 { bucket[values[i]/interval] = insert; insert->next = NULL; } else //桶内有元素 { Element *pre = NULL; while(temp != NULL) { if(temp->value > values[i]) break; pre = temp; temp = temp->next; } //while结束后有三种情况,一是temp为空,元素最大应插入桶末尾,二是找到比待插入元素大的值,pre不为空,元素应插入到pre后面。三是当pre为NULL时,插入到桶开头 if(temp == NULL) { //插入到桶末尾 pre->next = insert; insert->next = NULL; } else { if(pre == NULL) { //插入到桶开头 bucket[values[i]/interval] = insert; insert->next = temp; } else { //插入到桶的中间 pre->next = insert; insert->next = temp; } } } } //-----排序后输入到values中----- int j = 0; for(int i=0; i<bucket_num; i++) { Element *temp = bucket[i]; while(temp) { values[j++] = temp->value; temp = temp->next; } } }
稳定性总结,选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。选择算法时不能光看时间复杂度,要结合数字的特征。一般情况下基数排序时间复杂度为O(n),看上取要比快速排序的O(nlgn)要好,然而,在两个时间中隐含在O记号中的常数因子是不同的,尽管基数排序执行的变数要比快排少,但是每一遍所需的时间都要长的多。哪个排序方法好取决于底层机器的实现(快排通常比基数排序更为有效的利用硬件缓存),同时还取决于输入的数据。另外利用计数排序作为中间稳定排序的基数排序不是原地排序,而很多O(nlgn)的比较排序算法则可以做到原地排序。因此当内存资源容量比较宝贵的时候,像快排这样的原地排序算法可能是更为可取的。