常提到的排序算法有冒泡排序,选择排序(包括堆排序),插入排序(包括shell排序),快速排序,归并排序,桶排序,基数排序,表排序。每种排序算法都有各自的优缺点,没有绝对 的一种排序是最好的,排序算法效率与实际问题有关。
—冒泡排序
相对来说,冒泡排序是一种思路比较简单的排序,对于N个数进行排序时,共进行N-1趟循环,不断地交换相邻两元素位置,代码如下:
void M_sort(int *a, int n){
int i,p;
bool flag = 0;
for (p = n - 1; p >= 0; p--){
for (i = 0; i < p; i++){
if (a[i]>a[i + 1]){
flag = 1;
swap(a[i], a[i + 1]);
}
}
if (!flag)
break;
}
}
如上代码,显然算法时间平均复杂度为O(n2),考虑最好的情况下(倘若数组一开始便是有序的)则只需进行O(n)次比较后循环结束,最差的情况当然是O(n2),另外冒泡排序是一种稳定的排序,在交换时相同元素相对位置不发生改变。
二插入排序
插入排序可以分为简单的插入排序和shell排序两种。
1.简单插入排序的思想是,将数组分为有序和无序两个部分,一开始有序部分是最前面的1个元素,每次从未排序的部分中一次选择一个数插入到已排序部分的相对位置,最终达到整个序列有序的目的。例如对6 1 2 9 3进行插入排序
一开始 :6 1 2 9 3
第一趟:1 6 2 9 3
第二趟:1 2 6 9 3
第三趟:1 2 6 9 3
第四趟:1 2 3 6 9
代码如下:
void Insertion_Sort(int *a, int n){
int i, p,temp;
for (p = 1; p < n;p++){ //设p以前都是排好序列的
temp = a[p]; //当前要插入的数是a[p]
for (i = p; i >0&&temp<a[i-1]; i--) //从后向前扫描,
a[i] = a[i - 1];
a[i] = temp; //循环跳出时找到啦插入位置
}
}
看代码可知,插入排序的的平均时间复杂度为O(n2),最好情况下只要进行O(n)次排序就可以啦,最坏情况下是O(n2),简单插入排序也是稳定的排序
2.希尔排序
简单插入排序每次只能交换相邻元素,只改变一个逆序对,而shell排序时试图每次交换不相邻的元素,并希望一次交换可以减少多个逆序对。设排序增量序列依次为 DkDk-1...D1,满足D1=1,Dk序列递减。注意最后一趟间隔距离必须是1,这样才可以达到最终有序。一般的可以使用递增列Dk=2k-1,这样保证啦Dk两两互质,使用该序列最坏情况是O(n1.5),平均复杂度O(n5/4);
#define max 100
int Incre[max];
void Shell_Sort(int *a, int n){
int i,j,k,p,temp,D;
Incre[0] = 1;
k = 1;
while (Incre[k - 1] < n / 2){ //构造Hidbard增量序列
Incre[k++] = 2 * Incre[k - 1] + 1;
}
for (i = k - 1; i >= 0; i--){
D = Incre[i]; //选取增量
for (p= D;p< n;p++){ //向前插入
temp = a[p];
for (j = p; j - D >= 0 && temp < a[j - D]; j -= D)
a[j] = a[j - D];
a[j] = temp;
}
}
}
插入排序最坏情况是O(n2),平均时间复杂度也是O(n2).
3.选择排序
选择排序分为简单的选择排序和堆排序,选择排序的思想是,每次从未排序的元素中选取最小元素与与未排序序列首位元素交换,已排序序列元素个数为0.简单选择排序代码如下:
void SimpleSelection_Sort(int a[], int n){
int i, j,min;
for (i = 0; i < n; i++){
min = i; //i以前是排好的序列
for (j = i + 1; j < n;j++)
if (a[j] < a[min])
min = j; //找到最小值的下标
swap(a[i], a[min]); //交换
}
}
简单选择排序无论如何都要比较O(n2)次,时间复杂度总是O(n2).如果使用数据结构堆来求出每次未排序中最小元,情况会好很多,于是有啦堆排序。利用最大堆输出栈顶元素,将剩余元素继续调整为最大堆然后又输出最大元素,以此类推。可以额外开辟一个b[n]数组来存放排好的序列,但是此时需要的空间复杂度为O(n),当然也可以使用额外空间O(1)来完成:将无序的n个数生成最大堆,交换堆顶与数组最后一个元素,此时最后一个元素已经在正确的位置上,接下来向下调整成n-1个数的最大堆,以此类推,直到堆中只有1个元素时停止。
void AdjustHeap(int a[], int i, int n){ //n个元素中,从第i个元素开始向下过滤
int child,temp;
for (temp = a[i]; 2 * i + 1 < n; i = child){
child = 2 * i + 1;
if (child != n - 1 && a[child + 1] > a[child])
child++; //child指向左右儿子最大值
if (temp < a[child])
a[i] = a[child];
else
break;
}
a[i] = temp;
}
void Heap_Sort(int a[], int n){
int i;
for (i = (n - 1) / 2; i >= 0; i--)
AdjustHeap(a, i, n); //建立最大堆
for (i = n - 1; i > 0; i--){
swap(a[0], a[i]); //将最大元素调整到末尾
AdjustHeap(a, 0, i); //调整最大堆
}
}
可以看出无论数据怎样,堆排序的时间复杂度都是n(logn),额外空间复杂度为O(1),堆排序也是一种稳定的排序。
4,快速排序
快速排序需要选择一个基准key,将数组分为比key小和比key大的两子列,再分别去递归比key小和大的两个子列,key一般可以取首元素
void SimpleQuicksort(int a[], int n){ //通用接口
Mysort(a, 0, n-1);
}
void Mysort(int a[], int left, int right){
if (left >= right)
return; //递归出口
int key, low, high;
low = left, high = right;
key = a[low];
while (low < high){
while (low<high&&a[high]>=key) //找到比右边比key小则跳出
high--;
a[low] = a[high];
while (low < high&&a[low]<=key) //找到左边比key大则跳出
low++;
a[high] = a[low];
}
a[low] = key;
Mysort(a, left, low - 1);
Mysort(a, low + 1, right);
}
快速排序的平均复杂度是n(logn),最坏情况也是O(n2),在空间复杂度上,快速排序平均需要O(logn)深度的栈空间,最差情况下为O(n);
5.归并排序
归并排序采用递归分治的策略,将一个大的序列分为两个子序列 s1,s2,对于s1,s2是已经排好序的,合并s1和s2的结果得到排序好的s,分别递归s1和s2;
void Merge_sort(int a[], int n){ //接口函数
int*p = new int[n];
M_sort(a, 0, n - 1, p);
delete p;
}
void M_sort(int a[], int left, int right, int *p){
int mid, p1, p2, l;
if (right > left){ /*left<=right 是递归出口*/
mid = (right + left)>>1; //二分
M_sort(a, left, mid, p);
M_sort(a, mid + 1, right, p); //递归求左右子序列
p1 = left, p2 = mid + 1, l = left;
while (p1 <= mid&&p2 <= right){ //合并左右子序列
if (a[p1] < a[p2])
p[l++] = a[p1++];
else
p[l++] = a[p2++];
}
while (p1<=mid)
p[l++] = a[p1++];
while (p2<=right)
p[l++] = a[p2++];
for (l = left; l <= right; l++) //将数据导回
a[l] = p[l];
}
}
分析时间复杂度T(n)=2T(n/2)+O(n);
最后为T(n)=n(logn),额外空间O(n),归并排序最坏最好情况都是O(nlogn).排序是稳定的
6.桶排序
若已知n个关键字的范围是0-M-1的整数;且m<<n,这时候会有很多相等的元素,可以建立M个桶,在扫描关键字时候再按照桶来收集一次即可,此时排序算法复杂度O(n),实现的前提是知道关键字的范围,且M不可太大,否则系统无法承受。
例如一面试题目:有0-9999共10000个数,现在偷偷拿去其中某一个数,然后将剩下的数据随机打乱,如何求出拿走的数据?
.可以将数据按照快速排序来排序一遍,然后从0开始向后扫描,找到不存在的数据时返回。但是排序算法复杂度O(nlogn),然后在查找不存在数据平均查找为n/2次,于是一共需要计算次大约数为N=10000*log1000+1000/2=135000次
若采用桶排序,可以建立M[10000]个桶,将元素扫描一遍数字i放在M[i]中,然后再扫描一遍看哪个桶中没有元素就是要求的数,总共大约进行M=10000+5000=15000次
这样的话桶排序明显快好多。
7.基数排序
基数排序是一种桶排序的升级版,它所考虑的排序不只包含一个关键字,可能有多个关键字,比如对一副扑克牌进行排序,约定若面值大则一定大,若面值相等则按照 红>黑>梅花>方块 来排序,在这里面值可以看做主关键字,颜色可以看做次关键字,基数排序分为主位优先(Most Signficant Dagit Sort)MSD和次位优先(Least Significant Dagit Sort)LSD,若根据次位优先法则先建立4个花色桶,按照花色将扑克牌放入4个桶中,每个桶有13张扑克,然后再建立13个面值桶,再依次红黑梅方的顺序将每个花色桶的元素放入到面值桶中,于是得到了排列好的序列.总共次数为N=52*2=104次,若按照主位优先法则,先建立13个面值桶,则扫描52次,然后每个桶中花色进行排序,这时需要快排,每一个约为4*lon4=8次,于是最终N=52+8*13=156次.其实在大部分情况下LSD比MSD效果好。
对于单关键字,需要将关键字进行分解例如489,主关键字是4次关键字是8最次关键字是9,这是根据基数10分解的,对于一系列整数的排序,设数最长为D,采用B进制,于是需要建立B个桶,进行D趟排序,设数字个数为n,则T=O(D*(p+B)).对整数排序代码如下;
#define maxdigit 3
#define radix 10
class Node{
public:
int key[maxdigit];
Node*next;
};
typedef Node *List;
List RadixSort(List A){
List Bucket[radix]; //建立radix个桶
List rear[radix]; //记录每个桶表尾的元素
int i, j, digit;
for (i = maxdigit - 1; i >= 0; i--){ //次位优先排序
for (j = 0; j < radix; j++)
Bucket[j] = rear[j] = NULL; //初始化
while (A){ //开始排序
digit = A->key[i]; //取出当前关键字
if (!Bucket[digit]) //若当前为空
Bucket[digit]= A; //直接放入桶中
else
rear[digit]->next = A; //否则放入桶末尾
rear[digit] = A; //更新结尾
A = A->next; //更新A
}
for (j =radix-1; j>=0; j--){ //将桶中元素收集串联为A,一定从末尾桶开始
if (Bucket[j]){
rear[j]->next = A; //如果不空就威指针指向A
A = Bucket[j]; //A更新为头
}
/*A还是指向表头*/
}
}
return A;
}