探讨排序算法的实现

排序算法是我们工作中使用最普遍的算法,常见的语言库中基本都会有排序算法的实现,比如c标准库的qsort,stl的sort函数等。本文首先介绍直接插入排序,归并排序,堆排序,快速排序和基数排序等比较排序算法,然后介绍计数排序,基数排序等具有线性时间的排序算法。本文主要讨论算法的实现方法,并不会过多介绍基本理论。

评价一个排序算法优劣适用与否,一般需要从三个方面来分析

  1. 时间复杂度。用比较操作和移动操作数的最高次项表示,由于在实际应用中最在乎的是运行时间的上限,所以一般取输入最坏情况的下的运行时间作为算法的时间复杂度,快速排序例外。

  2. 空间复杂度。指除了输入数据外,临时需要的内存空间。在内存紧张的系统中需要首先考虑这一因素。空间复杂度是O(1)的算法称为原地排序算法。

  3. 稳定性。如果一个算法可以保证两个键值相等元素的位置关系在排序前后一样,那么我们就说这个算法是稳定的。算法的稳定性有时非常重要,比如计数排序的稳定性保证了基数排序的正确性。

. 比较排序

比较排序算法由于受决策树的限制,时间复杂度具有下限O(nlgn)(即lgn!)。

1.
直接插入排序

直接插入排序是最简单的排序算法,基本操作是,从第2个元素开始,将前面的元素当作一个有序表,把当前元素插入到有序表的合适位置,形成新的长度加1的有序表,依此类推至最后一个元素。最直观的实现方法如下:

void InsertSort(int *array, int length){
for(int i=1; i<length; ++i){
if(array[i]>=array[i-1]) continue;
int j;
int val = array[i];
for(j=i-1; j>=0&&array[j]>val; --j){
array[j+1] = array[j];
}
array[j+1] = val;
}
}

为了减少判断次数,可以在array[0]增加一个哨兵,

void InsertSortGard(int *array, int length){
for(int i=2; i<length; ++i){
if(array[i]>=array[i-1]) continue;
array[0] = array[i];
int j;
for(j=i-1; array[j]>array[0]; --j){
array[j+1] = array[j];
}
array[j+1] = array[0];
}
}

这样虽然需要增加一个元素的空间,不过在第二层for循环中不用再判断 j是否越界,对于使用频繁的排序算法来说是非常值得的。直接插入排序的空间复杂度是O(1),时间复杂度O(n^2),属于稳定排序。不过由于插入排序的常数因子小,最坏情况下比较操作次数是n(n-1)/2,最好情况下只要比较n次,所以对于小规模的排序,直接插入排序常常有比较好的效率。

2.
归并排序

分治策略是程序设计中经常采用的方法,归并排序算法是分治策略的典型应用。基本操作包括:

  1. 分解:将n个元素分解成各含n/2个元素的子序列

  2. 解决:用归并排序递归处理各个子序列

  3. 合并:将两个排好序的子序列合并成一个有序序列。

很明显这是一个递归过程,算法实现如下:

void Merge(int *array, int s1, int e1, int s2, int e2){
int size = e1-s1+e2-s2+2;
int *temp = (int*)malloc(size);
int i=0, j=s1, k=s2;
while (i<size) {
if (j>e1 || k>e2) break;
if (array[j]<=array[k]) temp[i++] = array[j++];
else temp[i++] = array[k++];
}
while (j <= e1) temp[i++] = array[j++];
while (k <= e2) temp[i++] = array[k++];
for (i=0,j=s1; i<size;)
array[j++] = temp[i++];
free(temp);
}

void MergeSort(int *array, int s, int e){
if (s<e) {
int mid = (s+e)/2;
MergeSort(array, s, mid);
MergeSort(array, mid+1, e);
Merge(array, s, mid, mid+1, e);
}
}

Merge中需要分配临时数组,所以空间复杂度是O(n),应用中由于malloc往往非常消耗时间,可以考虑使用全局数组,分配一次,重复使用。在最好最坏的输入情况下时间复杂度都是O(nlgn),也属于稳定的排序算法。

不过递归算法频繁调用函数既消耗栈空间又消耗时间,可以进一步优化为非递归版本。递归归并算法其实可以看作是一个先自顶向下后自底向上的过程,通过直接构造自底向上的过程可以解除递归。

void MergeSortNonRecur(int *array, int length){
int step = 1;
while (step < length) {
for(int i=0; i<length; i+=step<<1){
if (i+2*step-1<length)
Merge(array, i, i+step-1, i+step, i+2*step-1);
else if (i+step < length)
Merge(array, i, i+step-1, i+step, length-1);
}
step = step<<1;
}
}

下面是在我的电脑上测试的数据(us),环境是ubuntu12.04.3 LTS+gcc4.6.3。各个算法在50个元素和1000个元素的输入集的运行时间如下(单位微秒):





















直接插入

直接插入(哨兵)

递归归并

非递归归并

50

12

10

20

18

1000

2900

2700

500

470

可以发现带哨兵的直接插入排序比不带哨兵的要高,非递归归并排序比递归排序效率要高,同时,在规模较小时,由于直接插入排序的常数因子小,所以效率要比归并排序好,但是规模大了以后,归并排序要远远优于直接插入排序。

3.
堆排序

堆排序将数组看做一棵完全二叉树,这使得它和归并排序一样具有O(nlgn)的复杂度,不同的是,堆排序完全在数组内部,空间复杂度是O(1),属于原地排序(in place)算法。

堆排序的第一步是建堆,从最后一个非叶子节点开始,将大的元素上移,保证每个子树都是符合最大或最小堆

void MaxHeap(int *array, int i, int length) {
int l = (i<<1)+1;
int r = (i<<1)+2;
int largest = i;
if (l<=length && array[i]<array[l])
largest = l;
if(r<=length && array[largest]<array[r])
largest = r;
if(largest != i) {
swap(&array[i],&array[largest]);
MaxHeap(array, largest, length);
}
}

void BuildMaxHeap(int *array, int length){
for(int i=(length>>1)-1; i>=0; --i)
MaxHeap(array, i, length);
}

第二部排序,从最后一个元素开始,依次和第一个即最大的元素交换,然后重新建堆。

void HeapSort(int *array, int length) {
BuildMaxHeap(array, length);
for(int i=length-1; i>0; --i){
swap(&array[i], &array[0]);
length--;
MaxHeap(array, 0, length-1);
}
}

可以看出堆排序的元素交换是跳跃式的,这导致两个问题,一,堆排序是不稳定的排序方式
;二,实践中不能充分利用cache。不过堆排序非常适合用来实现优先级队列,解决topk问题。

4. 快速排序

快速排序是非常经典的算法,最坏情况下时间复杂度是O(n^2),最佳情况下是O(nlgn),但是由于其平均情况下与最佳情况下时间复杂度非常接近,而且与堆排序相比较,能更有效地利用硬件缓存,并且属于原地排序,所以实践中用的比较多。

经典的实现方法如下

int partition1(int *array, int low, int high) {
int val = array[low];
while(low < high) {
while(low<high && array[high]>=val) --high;
swap(&array[low], &array[high]);
while(low<high && array[low]<=val) ++low;
swap(&array[low], &array[high]);
}
return low;
}

void Quicksort1(int *array, int b, int e) {
if (b >= e) return;
int p = partition1(array, b, e);
Quicksort1(array, b, p-1);
Quicksort1(array, p+1, e);
}

快速排序的核心在于partion方法,我更喜欢下面这种方式,

int partition2(int *array, int low, int high) {
int i,j;
for(i=low,j=low; j<high;){
if(array[j]<array[high]) {
if(i!=j)
swap(&array[i], &array[j]);
++i;++j;
} else {
++j;
}
}
return i;
}

上一篇文章中,我还介绍了一种非递归的方法

void QuicksortNonRecur(int *array, int b, int e) {
if (b >= e) return;
std::stack< std::pair<int, int> > stk;
stk.push(std::make_pair(b, e));
while(!stk.empty()) {
std::pair<int, int> pair = stk.top();
stk.pop();
if(pair.first >= pair.second) continue;
int p = partition1(array, pair.first, pair.second);
if(p < pair.second) stk.push(std::make_pair(p+1, e));
if(p > pair.first) stk.push(std::make_pair(b, p-1));
}
}


线性时间排序

下面介绍的算法不使用比较操作,不依赖决策树模型,所以时间复杂度也就没有下界O(nlgn)。

1.
计数排序

计数排序有一个前提条件,待排序的元素必须都在一个固定范围内,比如n个输入元素,每个元素都在0到k之间,那么就可以用一个k大小的数组记录每个元素的出现频率,从而确定每个元素在所有输入元素中的位置。

void CountingSort(int *array, int length){
for(int i=0; i<length; ++i)
array_count[array[i]]++;
for(int j=1; j<MAXNUM; ++j)
array_count[j] += array_count[j-1];
for(int i=0; i<length; ++i) {
array_sort[array_count[array[i]]] = array[i];
array_count[array[i]]--;
}
}

时空复杂度都是O(n+k),当k=O(n)时,复杂度就是O(n)。

2.
基数排序

对于给定的n个输入数列,一般的比较排序算法是从最高位开始比较,然后对于每个子集递归排序,而基数排序恰恰相反,从低位开始比较,每次都是在全集中排序,这么做的好处是不用多余的空间记录每个子集的位置,不用递归排序子集。基数排序在每一位都要用到计数排序,计数排序是稳定的,这可以保证基数排序的正确性,但是基数排序本身却不是稳定的。

下面程序利用基数排序可以在在O(n)时间内对0到n^2-1之间的n个数排序。

#define NUM 10
#define NUM2 (NUM*NUM)

int array[NUM];
int array_tmp[NUM];
int array_count[NUM];

void ArrayInit(int *array){
srand((unsigned)time(0));
for(int i=0; i<NUM; ++i){
array[i] = rand()%NUM2;
}
}

void ArrayReset(int *array) {
for(int i=0; i<NUM; ++i)
array[i] = 0;
}
void ArrayCopy(int *src, int *des) {
for (int i=0; i<NUM; ++i)
des[i] = src[i];
}
int RadixSort(int *array) {
for(int i=0; i<2; ++i) {
for(int j=0; j<NUM; ++j){
int r = i, index = array[j];
while (r-->0) index /= NUM;
array_count[index%NUM]++;
}
for(int k=1; k<NUM; ++k)
array_count[k] += array_count[k-1];
for(int m=NUM-1; m>=0; --m){
int r = i, index = array[m];
while (r-->0) index /= NUM;
array_tmp[array_count[index%NUM]-1] = array[m];
array_count[index%NUM]--;
}
ArrayCopy(array_tmp, array);
ArrayReset(array_count);
}
}

int main(){
ArrayInit(array);
RadixSort(array);
}

code:

https://github.com/coderkian/algorithm/tree/master/sort

探讨排序算法的实现,布布扣,bubuko.com

时间: 2024-10-11 14:53:31

探讨排序算法的实现的相关文章

Python 排序算法的实现

冒泡排序: 1 def bubble(l): 2 length = len(l) 3 for i in range(length): 4 for j in range(i+1, length): 5 if l[i] > l[j]: 6 l[i], l[j] = l[j], l[i] 7 print l 选择排序: 1 def select(l): 2 length = len(l) 3 for i in range(length): 4 minn = i 5 for j in range(i+1

Python学习(三) 八大排序算法的实现(下)

本文Python实现了插入排序.基数排序.希尔排序.冒泡排序.高速排序.直接选择排序.堆排序.归并排序的后面四种. 上篇:Python学习(三) 八大排序算法的实现(上) 1.高速排序 描写叙述 通过一趟排序将要排序的数据切割成独立的两部分,当中一部分的全部数据都比另外一部分的全部数据都要小,然后再按此方法对这两部分数据分别进行高速排序,整个排序过程能够递归进行,以此达到整个数据变成有序序列. 1.先从数列中取出一个数作为基准数. 2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全

七种排序算法的实现和总结

最近把七种排序算法集中在一起写了一遍. 注释里有比较详细的说明. 1 /*排序算法大集合**/ 2 #include <stdio.h> 3 #include <string.h> 4 #include <stdlib.h> 5 6 //------------------快速排序------------------// 7 /* 8 核心: 9 如果你知道多少人该站你前面,多少人站你后面,你一定知道你该站哪个位置. 10 算法: 11 1.选取分界数,参考这个分界数,

软考笔记第六天之各排序算法的实现

对于前面的排序算法,用c#来实现 直接插入排序: 每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序.第一趟比较前两个数,然后把第二个数按大小插入到有序表中: 第二趟把第三个数据与前两个数从前向后扫描,把第三个数按大小插入到有序表中:依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程.直接插入排序属于稳定的排序,最坏时间复杂性为O(n^2),空间复杂度为O(1).直接插入排序是由两层嵌套循环组成的.外层循环标识并决定待比较的数值.内层循环为待比较数值确定其最终位

常见排序算法的实现(归并排序、快速排序、堆排序、选择排序、插入排序、希尔排序)

这篇博客主要实现一些常见的排序算法.例如: //冒泡排序 //选择排序 //简单插入排序 //折半插入排序 //希尔排序 //归并排序 //双向的快速排序 //单向的快速排序 //堆排序 对于各个算法的实现原理,这里不再多说了,代码中注释较多,结合注释应该都能理解算法的原理,读者也可自己google一下.另外,注释中有很多点,比如边界条件.应用场景等已经用 * 标记,* 越多,越应该多注意. 下面是实现: //冒泡排序 void BubbleSort(int *arr, int n) { if(

排序算法的实现(归并,快排,堆排,希尔排序 O(N*log(N)))

今天跟着左老师的视频,理解了四种复杂度为 O(N*log(N))的排序算法,以前也理解过过程,今天根据实际的代码,感觉基本的算法还是很简单的,只是自己写的时候可能一些边界条件,循环控制条件把握不好. //对于一个int数组,请编写一个选择冒泡算法,对数组元素排序. //给定一个int数组A及数组的大小n,请返回排序后的数组. //测试样例: //[1, 2, 3, 5, 2, 3], 6 //[1, 2, 2, 3, 3, 5] #include <iostream> using namesp

数据结构实验4(排序算法的实现及性能分析)

实现了选择排序, 插入排序, 冒泡排序, 快速排序, 改进后的快速排序, 以及两路合并排序. 通过随机函数随机生成100个数, 进行各种排序, 记录排序开始时间以及结束时间, 计算消耗的时间来比较算法的优略. 实现代码: #include "iostream" #include "cstdio" #include "cstring" #include "algorithm" #include "queue"

各种排序算法的实现代码

#include"stdio.h" #include"malloc.h" #include"stdlib.h" typedef int KeyType; #define MAXSIZE 20 typedef struct { KeyType key; }RedType; typedef struct { RedType r[MAXSIZE+1]; int length; }SqList,* SQLIST; void play_choose(voi

排序算法的实现--理解方法实现

以前也看过很多排序算法的原理,每次都想自己实现一下,一直都再拖,现在着牛课网学习算法课程,希望自己能够坚持练习. //对于一个int数组,请编写一个选择冒泡算法,对数组元素排序. //给定一个int数组A及数组的大小n,请返回排序后的数组. //测试样例: //[1, 2, 3, 5, 2, 3], 6 //[1, 2, 2, 3, 3, 5] #include <iostream> using namespace std; #include<string> void printR