1 快速排序(QuickSort)
快速排序是一个就地排序,分而治之,大规模递归的算法。从本质上来说,它是归并排序的就地版本。快速排序可以由下面四步组成。
(1) 如果不多于1个数据,直接返回。
(2) 一般选择序列最左边的值作为支点数据。
(3) 将序列分成2部分,一部分都大于支点数据,另外一部分都小于支点数据。
(4) 对两边利用递归排序数列。
快速排序比大部分排序算法都要快。尽管我们可以在某些特殊的情况下写出比快速排序快的算法,但是就通常情况而言,没有比它更快的了。快速排序是递归的,对于内存非常有限的机器来说,它不是一个好的选择。
int _Partition(int array[], int start, int end)
{
int spliter=array[end];
int pbigger=start, pless=start-1;
for(; pbigger<end; pbigger++)
{
if(array[pbigger]<spliter)
{
swap(&array[pbigger], &array[pless+1]);
pless++;
}
}
swap(&array[pless+1], &array[end]);
return pless+1;
}
void _QuickSort(int array[], int start, int end)
{
if(start<0 || end<0 || start>=end)
return;
int q=_Partition(array, start, end);
_QuickSort(array, start, q-1);
_QuickSort(array, q+1, end);
}
void QuickSort(int array[], int n)
{
_QuickSort(array, 0, n-1);
}
2 归并排序(MergeSort)
归并排序先分解要排序的序列,从1分成2,2分成4,依次分解,当分解到只有1个一组的时候,就可以排序这些分组,然后依次合并回原来的序列中,这样就可以排序所有数据。合并排序比堆排序稍微快一点,但是需要比堆排序多一倍的内存空间,因为它需要一个额外的数组。
void _Merge(int array[], int start, int mid, int end)
{
if(start <= mid && mid < end)
{
int* pleft, *pright;
pleft = new int[mid-start+1];
pright = new int[end-mid];
memcpy(pleft, array+start, (mid-start+1)*sizeof(int));
memcpy(pright, array+mid+1, (end-mid)*sizeof(int));
int i=0;
int j=0;
for(int pos=start; pos<=end; pos++)
{
if(j>= end-mid && i<mid-start+1)
array[pos]=pleft[i++];
else if(i>= mid-start+1 && j<end-mid)
array[pos]=pright[j++];
else if(pleft[i]<=pright[j])
array[pos]=pleft[i++];
else if(pleft[i]>pright[j])
array[pos]=pright[j++];
}
delete[] pleft;
delete[] pright;
}
}
void MergeSort(int array[], int start, int end)
{
if(start < end)
{
int mid=start+(end-start)/2;
MergeSort(array, start, mid);
MergeSort(array, mid+1, end);
_Merge(array, start, mid, end);
}
}
3 堆排序(HeapSort)
堆排序适合于数据量非常大的场合(百万数据)。
堆排序不需要大量的递归或者多维的暂存数组。这对于数据量非常巨大的序列是合适的。比如超过数百万条记录,因为快速排序,归并排序都使用递归来设计算法,在数据量非常大的时候,可能会发生堆栈溢出错误。
堆排序会将所有的数据建成一个堆,最大的数据在堆顶,然后将堆顶数据和序列的最后一个数据交换。接下来再次重建堆,交换数据,依次下去,就可以排序所有的数据。
void _MaxHeapify(int array[], int n, int r)
{
int left = (r<<1)+1;
int right = (r<<1)+2;
int largest=r;
if(left<n && array[left]>array[largest])
largest=left;
if(right<n && array[right]>array[largest])
largest=right;
if(largest!=r)
{
swap(&array[largest], &array[r]);
_MaxHeapify(array, n, largest);
}
}
void _BuildMaxHeap(int array[], int n)
{
for(int i=(n-1)/2; i>=0; i--)
_MaxHeapify(array, n, i);
}
void HeapSort(int array[], int n)
{
_BuildMaxHeap(array, n);
for(int i=n-1; i>=1; i--)
{
swap(&array[0], &array[i]);
n--;
_MaxHeapify(array, n, 0);
}
}
4 Shell排序(ShellSort)
Shell排序通过将数据分成不同的组,先对每一组进行排序,然后再对所有的元素进行一次插入排序,以减少数据交换和移动的次数。平均效率是O(nlogn)。其中分组的合理性会对算法产生重要的影响。现在多用D.E.Knuth的分组方法。
Shell排序比冒泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相对比较简单,它适合于数据量在5000以下并且速度并不是特别重要的场合。它对于数据量较小的数列重复排序是非常好的。
void _Shell(int array[], int n, int d)
{
for(int k=0; k<d; k++)
{
for(int i=d+k; i<n; i+=d)
{
int j=i-d;
int tmp=array[i];
while(array[j]>tmp && j>=0)
{
array[j+d]=array[j];
j-=d;
}
array[j+d]=tmp;
}
}
}
void ShellSort(int array[], int n)
{
int inc[5]={1,3,7,11,17};
for(int i=4; i>=0; i--)
{
_Shell(array, n, inc[i]);
}
}
5 插入排序(InsertSort)
插入排序通过把序列中的值插入一个已经排序好的序列中,直到该序列的结束。插入排序是对冒泡排序的改进。它比冒泡排序快2倍。一般不用在数据大于1000的场合下使用插入排序,或者重复排序超过200数据项的序列。
void InsertionSort(int array[], int len)
{
for(int j=1; j<len; j++)
{
int tmp=array[j];
int i=j-1;
while(array[i]>tmp && i>=0)
{
array[i+1]=array[i];
i--;
}
array[i+1]=tmp;
}
}
6 冒泡排序(BubbleSort)
冒泡排序是最慢的排序算法。在实际运用中它是效率最低的算法。它通过一趟又一趟地比较数组中的每一个元素,使较大的数据下沉,较小的数据上升。它是O(n^2)的算法。
void bubbleSort(int array[], int len)
{
for(int i=len-1; i>=0; i--)
{
for(int j=0; j<=i-1; j++)
{
if(array[j] > array[j+1])
swap(&array[j], &array[j+1]);
}
}
}
7 交换排序(ExchangeSort)和选择排序(SelectSort)
这两种排序方法都是交换方法的排序算法,效率都是 O(n2)。在实际应用中处于和冒泡排序基本相同的地位。它们只是排序算法发展的初级阶段,在实际中使用较少。
void SelectionSort(int array[], int n)
{
for(int i=0; i<n; i++)
{
int smallest=i;
for(int j=i+1; j<n; j++)
{
if(array[j]<array[smallest])
smallest=j;
}
if(smallest!=i)
swap(&array[i], &array[smallest]);
}
}
8 基数排序(RadixSort)
基数排序和通常的排序算法并不走同样的路线。它是一种比较新颖的算法,但是它只能用于整数的排序,如果我们要把同样的办法运用到浮点数上,我们必须了解浮点数的存储格式,并通过特殊的方式将浮点数映射到整数上,然后再映射回去,这是非常麻烦的事情,因此,它的使用同样也不多。而且,最重要的是,这样算法也需要较多的存储空间。
void RadixSort(int array[], int n)
{
int max=0;
for(int i=0; i<n; i++)
if(array[i]>max)
max=array[i];
int digits=log10(max)+1;
for(int d=1; d<=digits; d++)
{
int blen[10];
int* bucket[10];
for(int i=0; i<10; i++)
{
bucket[i]=new int[n];
blen[i]=0;
}
for(int j=0; j<n; j++)
{
int b=(array[j]%m)/(m/10);
bucket[b][blen[b]++]=array[j];
}
int pos=0;
for(int i=0; i<10; i++)
{
for(int j=0; j<blen[i]; j++)
{
array[pos++]=bucket[i][j];
}
}
}
}
9 总结
下面是一个总的表格,大致总结了我们常见的所有的排序算法的特点。
排序法 | 平均时间 | 最差情形 | 稳定度 | 额外空间 | 备注 |
冒泡 | O(n2) | O(n2) | 稳定 | O(1) | n小时较好 |
交换 | O(n2) | O(n2) | 不稳定 | O(1) | n小时较好 |
选择 | O(n2) | O(n2) | 不稳定 | O(1) | n小时较好 |
插入 | O(n2) | O(n2) | 稳定 | O(1) | 大部分已排序时较好 |
基数 | O(logrd) | O(logrd) | 稳定 | O(n) | d是关键字项数(0-9), r是基数(个十百) |
Shell | O(nlogn) | O(ns) 1<s<2 | 不稳定 | O(1) | s是所选分组 |
快速 | O(nlogn) | O(n2) | 不稳定 | O(nlogn) | n大时较好 |
归并 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大时较好 |
堆 | O(nlogn) | O(nlogn) | 不稳定 | O(1) | n大时较好 |
下面是一组10万随机数排序的实测数据,Degree of disorder表示数组的无序度。
Degree of disorder: 2497452562
Size: 100000
BubbleSort 36723ms
InsertionSort 7379ms
SelectionSort 12340ms
ShellSort 530ms
MergeSort 62ms
QuickSort 16ms
HeapSort 31ms
RadixSort 16ms