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)的比较排序算法则可以做到原地排序。因此当内存资源容量比较宝贵的时候,像快排这样的原地排序算法可能是更为可取的。