1、排序算法性能汇总
是否比较排序 | 方法 | 平均时间 | 最坏情况 | 最好情况 | 附加空间 | 稳定性 | 备注 |
比较排序 时间复杂度下限:O(nlogn) | 直接插入 | O(n2) | O(n2) | O(n) | O(1) | 稳定 | 大部分已排序时较好 |
希尔排序 | O(nlogn) | O(nlogn) | 与步长相关 | O(1) | 不稳定 | n小时较好 | |
冒泡排序 | O(n2) | O(n2) | O(n) | O(1) | 稳定 | n小时较好 | |
快速排序 | O(nlogn) | O(n2) | O(nlogn) | O(logn) 主要是递归调用占用空间 | 不稳定 | n大时较好,基本有序时反而不好 | |
直接选择 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 | n小时较好 | |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 | n小时较好(建堆顶为n/2) | |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n)主要是为了合并有序数列占用空间 | 稳定 | n小时较好 | |
非比较排序 | 基数排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(n+r) | 稳定 | d为位数,r为基数 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 | 优于比较排序算法 | |
桶排序 | O(n+c) | O(nlogn) | O(n) | O(n+m) | 稳定 |
2、测试代码
Sort.h中主要定义头部信息
#ifndef _SORT_H_
#define _SORT_H_
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<math.h>
#include<time.h>
#define MAXSIZE 10000
int *randData ;
void getRandData(){
int a[] = {0,4,1,3,2,16,9,10,14,8,7};
int i=0;
randData=(int*)malloc(MAXSIZE*sizeof(int));
/*for (i =0;i<MAXSIZE;i++){
randData[i] = a[i];
}*/
srand((unsigned)time(NULL)); //设定随机数种子
for ( i = 0; i < MAXSIZE; i++)
randData[i]=1 + rand()%MAXSIZE;
}
void printRandData(){
int i=0;
for ( i = 0; i < MAXSIZE; i++){
if(i%15==0)
printf("\n");
printf("%5d",randData[i]);
}
printf("\n");
}
void InsertSort(int* list,int len);
void ShellSort(int* list,int len, int *dk, int dkLen);
void BubbleSort(int *list,int len);
void BubbleSort2(int *list,int len);
void QuickSort(int *list, int low, int high);
void SelectionSort(int *list, int len);
void HeapSort(int *list,int len);
void HeapSort2(int *list,int len);
void MergeSort(int *list, int n);
void LSDRadixSort(int arr[],int len,int d);
#endif
Main.cpp主要用于定义测试代码
#include "Sort.h"
void main(){
int sd[6] = {1,3,5,7,9,11};
clock_t start,finish;
double duration;
getRandData();
start = clock();
//InsertSort(randData,MAXSIZE);
//ShellSort(randData,MAXSIZE,sd,6);
//BubbleSort(randData,MAXSIZE);
//BubbleSort2(randData,MAXSIZE);
QuickSort(randData,0,MAXSIZE-1);
//SelectionSort(randData,MAXSIZE);
//HeapSort(randData,MAXSIZE);
//HeapSort2(randData,MAXSIZE);
//MergeSort(randData,MAXSIZE);
//LSDRadixSort(randData,MAXSIZE,5);
finish = clock();
printRandData();
duration = (double)(finish - start) / CLOCKS_PER_SEC;
printf( "\nSort Used time:%f seconds\n", duration );
}
3、常见排序方法代码汇总
3.1、直接插入
/************************************************************************/
/*直接插入排序
将整个数组分为已经排好序的部分,和未排好序的部分,未排好序的部分想已经排好序的部分插入数据
整个算法的精妙之处在于,哨兵的设置。
1、将要插入的值拿出来,当成哨兵。因为设置了哨兵,因此向后移动过程中,不用担心数值覆盖。
2、向排好序的部分插入时,比较过程和移动过程一起进行(这是数组的特点)。与哨兵进行比较。
3、在得到目的地址之后,直接插入即可。
*/
/************************************************************************/
void InsertSort(int* list,int len){
int temp = 0,i = 0,j = 0;
for (i = 0;i<len;i++){
//哨兵
temp = list[i];
//判断和移动同时进行
for (j = i;j>= 1;j--){
if (list[j-1] > temp){
list[j] = list[j-1];
} else {
break;
}
}
list[j] = temp;
}
}
3.2、希尔排序
#include <stdio.h>
/************************************************************************/
/* 希尔排序,缩小增量排序。
其是在直接插入排序基础之上,利用
1、在排序记录基本为正序时期复杂度降为O(n)的情况。
2、直接插入排序简单,在n值很小时效率也很高。
希尔排序的时间复杂度不好估量,gap的选取也没有定论,gap=[gap/2]的程序较好写。
*/
/************************************************************************/
void ShellSort(int* list,int len, int *dk, int dkLen){
//list为待排序的数组
//len为待排序数组的长度
//dk为每次排序步长
//deLen为步长数组的长度
int i = 0,j = 0,k = 0,temp = 0,step = 0;
//对于每个步长
for (i = dkLen-1;i >= 0;i--){
step = dk[i];
//一下是加入步长参数的希尔排序
//对于数组中的每个数都采用
for (j = 0;j<len;j++){
//哨兵,带插入点
temp = list[j];
//
for (k = j;k-step>=0;k=k-step){
//与哨兵进行比较
if (list[k-step] > temp){
list[k] = list[k-step];
} else {
break;
}
}
list[k] = temp;
}
}
}
3.3、冒泡排序
//普通冒泡排序
void BubbleSort(int *list,int len){
int i , j , k, temp;
i = j = k =temp= 0;
//外层循环的意思是,对每个元素都要进行排序
for (i = 0;i<len;i++){
//内层循环的意思是,对没个元素都要与其他未排的元素进行比较
//内存排序可以理解为找最大的元素出来。但是找的过程中,
//因为len-i这些元素已经排序过了,因此,没有必要再次比较。
//而len-i-1的原因是,需要进行a[j+1]的比较。
for (j = 0;j<len-i-1;j++){
if (list[j]>list[j+1]){
temp = list[j+1];
list[j+1] = list[j];
list[j] = temp;
}
}
}
}
//增加了判断条件的冒泡排序
void BubbleSort2(int *list,int len){
int i , j , k ,flag,temp;
i = j = k = flag = temp = 0;
for (i = 0;i<len;i++){
if (flag){
break;
}
flag = 1;
for (j = 0;j<len-i-1;j++){
if (list[j]>list[j+1]){
//已经不存在差序的情况
flag = 0;
temp = list[j+1];
list[j+1] = list[j];
list[j] = temp;
}
}
}
}
3.4、快速排序
//快速排序
/************************************************************************/
/* 是对冒泡排序的一种改进,通过一趟排序将待排序的记录分成独立的两部分,
其中一部分比另一部分记录的关键字小。然后对这两部分分记录进行快速排序。 递归的栈空间为O(logn)
*/
/************************************************************************/
void exchange(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
//划分前后块,因为,快速排序本身是不稳定的排序,因此交换方法自然也就多样化,只要能实现前后大小规范即可。
int Partition(int *list, int low ,int high){
int i, j;
int value = list[high];
for(i = low - 1, j = low; j <= high - 1; j++) {
//本段代码有这样一个特点,
//小的换到后面去了,同时,i在走动过程中,i+1始终指向比value大的元素
//这就造成了,每次遇到比value大的元素,j继续向后走,走到比j小的时候,
//与这个比i+1个元素进行交换。
if(list[j] <= value) {
i++;
exchange(list + i, list + j);
}
}
//最后,将最后一个元素交换到中间来。
exchange(list + i + 1, list + high);
return i + 1;
}
int Partition2(int *list, int low ,int high){
int key = list[low];
while(low < high){
//注意,一定是>=或者<=
while (low < high && list[high] >= key){
high--;
}
exchange(list+low,list+high);
while(low< high && list[low] <= key){
low ++;
}
exchange(list+low,list+high);
}
return low;
}
void QuickSort(int *list, int low, int high){
if (low < high){
int mid = Partition(list,low,high);
QuickSort(list,low,mid-1);
QuickSort(list,mid+1,high);
}
}
3.5、直接选择
//简单选择排序
void SelectionSort(int *list, int len){
int i = 0,j = 0,k = 0,temp = 0;
//对于列表中的每个元素
for (i = 0;i<len;i++){
temp = list[i];
k = i;
//从i+1到len中选取最小的元素与i所对应的元素比较,交换。
for (j = i+1;j<len;j++){
if (temp > list[j]){
temp = list[j];
k = j;
}
}
if (k == i){
continue;
}
temp = list[i];
list[i] = list[k];
list[k] = temp;
}
}
3.6、堆排序
#include <stdio.h>
//堆排序
/************************************************************************/
/* 对于堆排序,我们所要掌握的就是,大/小头堆的建立和删除头结点之后的调整。
如果用一课完全二叉树来表示堆的话,就是说该树中的非叶子结点的值均不大于(或不小于)其左右两个分支结点的值。
堆排序有两个关键步骤:
(1)构造堆,将一个无序序列初始化为一个堆;
(2)调整堆,在输出了堆的根节点之后,调整剩余元素成为一个新的堆.
输出的元素是非递减的(递增的),需要建立大根堆,因为,根节点是跟数组的后面的结点交换了,而不是直接输出了。
*/
/************************************************************************/
//recursion begin
/******************************堆排序的递归实现开始*******************************/
/************************************************************************/
/* 我们假定以某个节点i的左儿子节点和右儿子节点为根的子树都是大根堆,但是A[i]可能小于其子节点的值,这样就违背了大根堆的性质。
首先找出i节点和其左右子节点共3个节点中值最大的节点,如果不是i,则将i与值最大的节点互换。这样确保了根i处的值是最大的。
然后调整以刚才与i互换的子节点为根的子树,递归调用算法MaxHeapify。
注意:下标均以1开始,而不是0,因为完全二叉树 中i是父节点,2i和2i+1是子节点的性质的前提是 其是从1开始的,而非0。
二叉堆通常用数组来实现,它舍弃下标0,从下标1开始置数,则很容易满足,对于数组中任意位置i上的元素,
其左儿子的位置在2i上,右儿子的位置在2i+1上,双亲的位置则在i/2上。
*/
/************************************************************************/
//得到父节点索引
int getParent(int i){
//右移1位相当于i/2
return i>>1;
}
//得到左子树索引
int getLeftSon(int i){
//左移1位相当于i*2
return i<<1;
}
//得到右子树索引
int getRightSon(int i){
return ((i<<1) + 1);
}
//调整以某个节点i为根节点的子树为大根堆
//时间复杂度:T(n) = O(lgn)。或者是O(h),h为树的高度。
void MaxHeapify(int *list,int i,int heapSize) {
int left = getLeftSon(i);
int right = getRightSon(i);
int largest = i;//记录值最大的元素的索引
if (left <= heapSize && list[left] > list[i]){
largest = left;
}
if (right <= heapSize && list[right] > list[largest]){
largest = right;
}
//i结点不是最大结点
if (largest != i){
//进行交换
int temp = list[i];
//将i位置设置为最大值
list[i] = list[largest];
list[largest] = temp;
//对与i进行调整的结点再次进行调整
MaxHeapify(list,largest,heapSize);//递归调用,继续调整子树
}
}
//建堆 建堆的时间是O(n)
void buildMaxHeap(int *list,int heapSize) {
//heapSize / 2设置为i点(堆顶,最大值对应位置),即二叉树的根节点。
//为什么要设置heapSize / 2设置为i点呢?这是因为,根据二叉树的特点,最后一层的结点数量
//占整个二叉树结点数量的一般,而将最后一层的结点也作为交换的结点是无意义的。
//自底向上地调整堆
for (int i = heapSize / 2;i > 0;--i) {
//随着头结点与后面结点的交换,堆的大小逐渐减小
MaxHeapify(list,i,heapSize);
}
}
//堆排序 堆排序的时间复杂度是O(nlgn)
void HeapSort(int *list,int heapSize) {
//首先建立堆
buildMaxHeap(list,heapSize);
//n-1次调整堆,每次时间代价是O(logn)
for (int i = heapSize-1;i > 0;--i) {
//因为大根堆,根节点在list[1]位置。
//从1开始。
int temp = list[1];
list[1] = list[i];
list[i] = temp;
MaxHeapify(list,1,i);
}
}
/******************************堆排序的递归实现结束*******************************/
/******************************堆排序的非递归实现开始*******************************/
/*调整堆*/
/*---------------------------------------------------------
/ 假设在输出根节点之后,以最后一个元素代替它。此时根节
/ 点的左右子树均为堆, 则只需要自上而下进行调整即可。
/ 首先将它和左右子树相比较,如果根节点比它两个子结点
/ 的值都小,那么就是堆了不用调整;否则,让根结点和其中
/ 较小的结点值互换,先让根节点满足堆的定义。可能因为交换,
/ 使得交换以后的结点的子树不满足堆的性质,则继续向下调整。
/---------------------------------------------------------*/
void HeapAdjust(int a[],int s,int m) {
int temp=a[s];
int j = 0;
//2*s的原因是,父节点跟子节点比较
for(j=2*s;j<=m;j*=2) {
if(j<m && a[j]<a[j+1]) {
j++; //j指向两个子结点中较大的一个
}
//这里,调整父节点和子节点。
if(temp >= a[j]){
break; //如果as[s]都比最大的大,因此可以退出。
}
//因为a[s]的值已经存放到temp中
//将最大的放到头结点。
a[s]=a[j];
s=j; //下降一个结点,越往下,结点越小。
}
//将原先最大值交换到最小结点处。
a[s]=temp;
}
/*构造堆*/
/*-----------------------------------------------------------------
/ 为构造堆,可以在已经是堆的两个自序列上加上它们的根节点,并进行
/ 必要的调整使之成为更大的堆。加上根节点以后,可能不满足堆的定义
/ 则可以用前面的调整堆的算法进行调整,使之称为堆。所以从一个无序
/ 序列构造堆的过程就是反复调整的过程。将n个待排序的关键字序列看为
/ 一棵完全二叉树,则最后一个非叶子结点为第(int)[n/2]个元素。首先,
/ 将n个叶子结点看成是n个堆,然后从第int[n/2]个结点开始,依次将第
/ [n/2],[n/2]-1,....,第1个结点按照堆的定义逐一加到它们的子结点上。
/ 直到最后建成一个完全的堆。
/-----------------------------------------------------------------*/
void HeapSort2(int *a,int n) {
int i = 0;
for( i=n/2;i>0;i--) //构造堆
HeapAdjust(a,i,n-1);
for(i=n-1;i>1;i--) //进行堆排序,将最大值放在序列的最后
{
int temp=a[1];
a[1]=a[i];
a[i]=temp;
HeapAdjust(a,1,i-1);
}
}
/******************************堆排序的非递归实现结束*******************************/
3.7、归并排序
#include <stdlib.h>
/************************************************************************/
/*
归并排序的效率是比较高的,设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,
时间复杂度可以记为O(N),故一共为O(N*logN)。因为归并排序每次都是在相邻的数据中进行操作,
所以归并排序在O(N*logN)的几种排序方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。并且其是一稳定的排序方法
归并排序是利用递归和分而治之的技术将数据序列划分成为越来越小的半子表,再对半子表排序,
最后再用递归步骤将排好序的半子表合并成为越来越大的有序序列,归并排序包 括两个步骤,分别为:
1)划分子表
2)合并半子表
(1)稳定性
归并排序是一种稳定的排序。
(2)存储结构要求
可用顺序存储结构。也易于在链表上实现。
(3)时间复杂度
对长度为n的文件,需进行 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。
(4)空间复杂度
需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。
注意:若用单链表做存储结构,很容易给出就地的归并排序。
*/
/************************************************************************/
//将有二个有序数列a[first...mid]和a[mid...last]合并。
void mergeArray(int a[], int first, int mid, int last, int temp[]){
int i = first, j = mid + 1;
int m = mid, n = last;
int k = 0;
while (i <= m && j <= n){
//主要是在这里进行了比较操作。
if (a[i] <= a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}
while (i <= m)
temp[k++] = a[i++];
while (j <= n)
temp[k++] = a[j++];
//将归并好的有序数组再次放到原数组中
for (i = 0; i < k; i++)
a[first + i] = temp[i];
}
void mSort(int a[], int first, int last, int temp[]){
if (first < last){
int mid = (first + last) / 2;
mSort(a, first, mid, temp); //左边有序
mSort(a, mid + 1, last, temp); //右边有序
mergeArray(a, first, mid, last, temp); //再将二个有序数列合并
}
}
void MergeSort(int *list, int n){
int *p = (int *)malloc(sizeof(int)*n);
mSort(list, 0, n - 1, p);
free(p);
}
3.8、基数排序
/************************************************************************/
/* 基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序的方式可以采用LSD(Least significant digital)最低位优先法首先依据最低位关键码Kd对所有对象进行一趟排序
或MSD(Most significant digital),最高位优先法通常是一个递归的过程
LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
基数排序的时间复杂度是 O(k·n),其中n是排序元素个数,k是数字位数。注意这不是说这个时间复杂度一定优于O(n·log(n)),因为k的大小一般会受到 n 的影响。
LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好,MSD的方式恰与LSD相反,是由高位数为基底开始进行分配,其他的演算方式则都相同。
*/
/************************************************************************/
#include<stdlib.h>
//参考:http://www.cnblogs.com/Braveliu/archive/2013/01/21/2870201.html
int getdigit(int x,int d) {
int a[] = {1, 1, 10,100,1000,10000,100000}; //因为待排数据最大数据也只是两位数,所以在此只需要到十位就满足
return ((x / a[d]) % 10); //确定桶号
}
//MSD法实现
void msdradix_sort(int arr[],int begin,int end,int d) {
const int radix = 10;
int count[radix], i, j;
//置空
for(i = 0; i < radix; ++i) {
count[i] = 0;
}
//分配桶存储空间
int *bucket = (int *) malloc((end-begin+1) * sizeof(int));
//统计各桶需要装的元素的个数
for(i = begin;i <= end; ++i){
count[getdigit(arr[i], d)]++;
}
//求出桶的边界索引,count[i]值为第i个桶的右边界索引+1
for(i = 1; i < radix; ++i){
count[i] = count[i] + count[i-1];
}
//这里要从右向左扫描,保证排序稳定性
for(i = end;i >= begin; --i){
j = getdigit(arr[i], d); //求出关键码的第d位的数字, 例如:576的第3位是5
bucket[count[j]-1] = arr[i]; //放入对应的桶中,count[j]-1是第j个桶的右边界索引
--count[j]; //第j个桶放下一个元素的位置(右边界索引+1)
}
//注意:此时count[i]为第i个桶左边界
//从各个桶中收集数据
for(i = begin, j = 0;i <= end; ++i, ++j){
arr[i] = bucket[j];
}
//释放存储空间
free(bucket);
//对各桶中数据进行再排序
for(i = 0;i < radix; i++){
int p1 = begin + count[i]; //第i个桶的左边界
int p2 = begin + count[i+1]-1; //第i个桶的右边界
if(p1 < p2 && d > 1) {
msdradix_sort(arr, p1, p2, d-1); //对第i个桶递归调用,进行基数排序,数位降 1
}
}
}
//LSD法实现
void LSDRadixSort(int arr[],int len,int d){
//d为多少位
const int radix = 10;
int count[radix], i, j;
int *bucket = (int*)malloc((len)*sizeof(int)); //所有桶的空间开辟
//按照分配标准依次进行排序过程
for(int k = 1; k <= d; ++k){
//置空
for(i = 0; i < radix; i++){
count[i] = 0;
}
//统计各个桶中所盛数据个数
for(i = 0; i <= len-1; i++){
count[getdigit(arr[i], k)]++;
}
//count[i]表示第i个桶的右边界索引
for(i = 1; i < radix; i++){
count[i] = count[i] + count[i-1];
}
//把数据依次装入桶(注意装入时候的分配技巧)
for(i = len-1;i >= 0; --i){ //这里要从右向左扫描,保证排序稳定性
j = getdigit(arr[i], k); //求出关键码的第k位的数字, 例如:576的第3位是5
//确定桶
bucket[count[j]-1] = arr[i]; //放入对应的桶中,count[j]-1是第j个桶的右边界索引
//因为是从桶的最大位置到最小位置的倒序放入桶中
--count[j]; //对应桶的装入数据索引减一
}
//注意:此时count[i]为第i个桶左边界
//从各个桶中收集数据
for(i = 0,j = 0; i <= len-1; ++i, ++j){
arr[i] = bucket[j];
}
}
free(bucket);
}
4、排序算法使用情况分析及选择
冒泡排序:在最优情况下只需要经过n-1次比较即可得出结果,(这个最优情况那就是序列己是正序,从100K的正序结果可以看出结果正是如此),但在最坏情况下,即倒序(或一个较小值在最后),下沉算法将需要n(n-1)/2次比较。所以一般情况下,特别是在逆序时,它很不理想。它是对数据有序性非常敏感的排序算法。
冒泡排序2:它是冒泡排序的改良(一次下沉再一次上浮),最优情况和最坏情况与冒泡排序差不多,但是一般情况下它要好过冒泡排序,它一次下沉,再一次上浮,这样避免了因一个数的逆序,而造成巨大的比较。如(2,3,4,…,n-1,n,1),用冒泡排序需要n(n-1)/2次比较,而此排序只要3轮,共比较(n-1)+(n-2)+(n-3)次,第一轮1将上移一位,第二轮1将移到首位,第三轮将发现无数据交换,序列有序而结束。但它同样是一个对数据有序性非常敏感的排序算法,只适合于数据基本有序的排序。
快速排序:它同样是冒泡排序的改进,它通过一次交换能消除多个逆序,这样可以减少逆序时所消耗的扫描和数据交换次数。在最优情况下,它的排序时间复杂度为O(nlog2n)。即每次划分序列时,能均匀分成两个子串。但最差情况下它的时间复杂度将是O(n^2)。即每次划分子串时,一串为空,另一串为m-1(程序中的100K正序和逆序就正是这样,如果程序中采用每次取序列中部数据作为划分点,那将在正序和逆时达到最优)。从100K中正序的结果上看“快速排序”会比“冒泡排序”更慢,这主要是“冒泡排序”中采用了提前结束排序的方法。有的书上这解释“快速排序”,在理论上讲,如果每次能均匀划分序列,它将是最快的排序算法,因此称它作快速排序。虽然很难均匀划分序列,但就平均性能而言,它仍是基于关键字比较的内部排序算法中速度最快者。
直接选择排序:简单的选择排序,它的比较次数一定:n(n-1)/2。也因此无论在序列何种情况下,它都不会有优秀的表现(从上100K的正序和反序数据可以发现它耗时相差不多,相差的只是数据移动时间),可见对数据的有序性不敏感。它虽然比较次数多,但它的数据交换量却很少。所以我们将发现它在一般情况下将快于冒泡排序。
堆排序:由于它在直接选择排序的基础上利用了比较结果形成。效率提高很大。它完成排序的总比较次数为O(nlog2n)。它是对数据的有序性不敏感的一种算法。但堆排序将需要做两个步骤:-是建堆,二是排序(调整堆)。所以一般在小规模的序列中不合适,但对于较大的序列,将表现出优越的性能。
直接插入排序:简单的插入排序,每次比较后最多移掉一个逆序,因此与冒泡排序的效率相同。但它在速度上还是要高点,这是因为在冒泡排序下是进行值交换,而在插入排序下是值移动,所以直接插入排序将要优于冒泡排序。直接插入法也是一种对数据的有序性非常敏感的一种算法。在有序情况下只需要经过n-1次比较,在最坏情况下,将需要n(n-1)/2次比较。
希尔排序:增量的选择将影响希尔排序的效率。但是无论怎样选择增量,最后一定要使增量为1,进行一次直接插入排序。但它相对于直接插入排序,由于在子表中每进行一次比较,就可能移去整个经性表中的多个逆序,从而改善了整个排序性能。希尔排序算是一种基于插入排序的算法,所以对数据有序敏感。
归并排序:归并排序是一种非就地排序,将需要与待排序序列一样多的辅助空间。在使用它对两个己有序的序列归并,将有无比的优势。其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlog2n)。对数据的有序性不敏感。若数据节点数据量大,那将不适合。但可改造成索引操作,效果将非常出色。
基数排序:在程序中采用的是以数值的十进制位分解,然后对空间采用一次性分配,因此它需要较多的辅助空间(10*n+10), (但我们可以进行其它分解,如以一个字节分解,空间采用链表将只需辅助空间n+256)。基数排序的时间是线性的(即O(n))。由此可见,基数排序非常吸引人,但它也不是就地排序,若节点数据量大时宜改为索引排序。但基数排序有个前提,要关键字能象整型、字符串这样能分解,若是浮点型那就不行了。
不同的排序方法适应不同的应用环境和要求,所以选择合适的排序方法很重要
(1)若n较小,可采用直接插入或直接选择排序。 当记录规模较小时,直接插入排序较好,它会比选择更少的比较次数; 但当记录规模较大时,因为直接选择移动的记录数少于直接插人,所以宜用选直接选择排序。 这两种都是稳定排序算法。
(2)若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜(这里的随机是指基准取值的随机,原因见上的快速排序分析);这里快速排序算法将不稳定。
(3)若n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
- 快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
- 堆排序虽不会出现快速排序可能出现的最坏情况。但它需要建堆的过程。这两种排序都是不稳定的。
- 归并排序是稳定的排序算法,但它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。
(4)特殊的箱排序、基数排序 ,它们都是一种稳定的排序算法,但有一定的局限性:
- 关键字可分解。
- 记录的关键字位数较少,如果密集更好
- 如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序
在初始序列已基本有序(除去n个元素中的某k个元素后即呈有序,k<<n)的情况下,排序效率最高的算法是: 插入排序
排序的平均时间复杂度为O(n•logn)的算法是:归并排序、快速排序、堆排序
排序的平均时间复杂度为O(n•n)的算法是:冒泡排序、插入排序、选择排序
排序过程中的比较次数与排序方法无关的是:选择排序、归并排序
如果只想得到1000个元素组成的序列中第5个最小元素之前的部分排序的序列,最快的算法是:堆排序
在文件"局部有序"或文件长度较小的情况下,最佳内部排序的方法:插入排序