排序问题一直是计算机技术研究的重要问题,排序算法的好坏直接影响程序的执行速度和辅助存储空间的占有量,所以各大IT企业在笔试面试中也经常出现有关排序的题目。本节详细分析常见的各种排序算法,并从时间复杂度、空间复杂度、适用情况等多个方面对它们进行综合比较。
- 选择排序
- 插入排序
- 冒泡排序
- 双向冒泡排序
- 如何进行归并排序
- 快速排序
- 希尔排序
- 堆排序
- 各种排序算法有什么优劣
选择排序
选择排序是一种简单直观的排序算法,基本原理如下:对于给定的一组记录,经过第一轮比较后得到最小的记录,然后将该记录与第一个记录的位置进行交换;接着对不包括第一个记录以外的其他记录进行第二轮比较,得到最小的记录并与第二个记录进行位置交换;重复该过程,直到进行比较的记录只有一个时为止。
从简单选择排序的过程来看,它的特点就是交换移动数据次数相当少,这样也就节约了相应的时间。无论是最好情况,还是最差情况,其比较次数都是一样的,第i趟排序需要进行n-i次。而对于交换次数而言,最好的情况是有序,需要交换0次;最差的情况,即逆序时,交换次数为n-1次,基于最终的排序时间是比较与交换的次数总和,因此总的时间复杂度依然为O(n^2)。
插入排序
对于给定的一组记录,初始时假设第一个记录自成一个有序序列,其余的记录为无序序列;接着从第二个记录开始,按照记录的大小依次将当前处理的记录插入到其之前的有序序列中,直至最后一个记录插入到有序序列中为止。
冒泡排序
冒泡排序顾名思义就是整个过程像气泡一样往上升,单向冒泡排序的基本思想是(假设由小到大排序):对于给定的n个记录,从第一个记录开始依次对相邻的两个记录进行比较,当前面的记录大于后面的记录时,交换其位置,进行一轮比较和换位后,n个记录中的最大记录将位于第n位;然后对前(n-1)个记录进行第二轮比较;重复该过程直到进行比较的记录只剩下一个时为止。
双向冒泡排序
双向冒泡排序是冒泡排序的一种优化,基本思想是首先将第一个记录的关键字和第二个记录的关键字进行比较,若为‘逆序’(即L.r[1].key > L.r[2].key),则将两个记录交换,然后比较第二个记录和第三个记录的关键字。依次类推,直至第n-1个记录的关键字和第n个记录的关键字比较过为止。这是第一趟冒泡排序,其结果是使得关键字最大的记录被安置到最后一个记录的位置上。
第一趟排序之后进行第二趟冒泡排序,将第n-2个记录的关键字和第n-1个记录的关键字进行比较,若为“逆序”(即 L.r[n-1].key
#include<iostream>
using namespace std;
void Swap( int &a, int &b )
{
int temp = a;
a = b;
b = temp;
}
void Bubble2Sort( int arr[], int length )
{
int left = 1;
int right = length - 1;
int t;
do
{
// forward
for(int i=right; i>=left; i-- )
{
if( arr[i]<arr[i-1] )
{
Swap( arr[i],arr[i-1] );
t = i;
}
}
left = t + 1;
//...
for( int i=left; i<right+1; i++ )
{
if(arr[i]<arr[i-1])
{
Swap( arr[i], arr[i-1]);
t = i;
}
}
right = t-1;
}while( left<=right );
}
int main()
{
int i=0;
int a[] = { 5,4,9,8,7,6,0,1,3,2 };
int len = sizeof(a) / sizeof(a[0]);
Bubble2Sort( a,len );
for( i=0; i<len; i++ )
cout << a[i] << " ";
cout << endl;
return 0;
}
如何进行归并排序
归并排序是利用递归与分治技术将数据序列划分成为越来越小的半子表,再对半子表排序,最后再用递归步骤将排好序的半子表合并成为越来越大的有序序列。其中“归”代表的是递归的意思,即递归地将数组折半地分离为单个数组。例如,数组 [ 2,6,1,0 ] 会先折半,分为[ 2,6] 和【 1,0 】两个子数组,然后再折半将数组分离,分为【2】【6】和【1】【0】。“并”就是将分开的数据按照从小到大或者从大到小的顺序在放到一个数组中。如上面的【2】、【6】合并到一个数组中是【2,6】,【1】【0】合并到一个数组中是【0,1】,然后再将【2,6】和【0,1】合并到一个数组中即为【0,1,2,6】。
具体而言,归并排序算法的原理如下:对于给定的一组记录(假设共有n个记录),首先将每两个相邻的长度为1的子序列进行归并,得到 n/2(向上取整)个长度为2或1的有序子序列,再将其两两归并,反复执行此过程,直到得到一个有序序列为止。
所以,归并排序的关键就是两步:第一步,划分子表;第二步,合并半子表。
以数组{ 49,38,65,97,76,13,27 } 为例,排序过程如下:
#include<iostream>
using namespace std;
void Merge( int arr[], int p,int q, int r )
{
int i,j,k,n1,n2;
n1 = q-p+1;
n2 = r - q ;
int *L = new int[n1];
int *R = new int[n2];
for( i=0, k=p; i<n1; i++,k++ )
L[i] = arr[k];
for( i=0,k=q+1; i<n2; i++,k++ )
R[i] = arr[k];
for( k=p,i=0,j=0; i<n1 && j<n2; k++ )
{
if(L[i] >R[j])
{
arr[k] = L[i];
i++;
}
else
{
arr[k] = R[j];
j++;
}
}
if( i<n1 )
{
for( j=i; j<n1; j++,k++ )
arr[k] = L[j];
}
if( j<n2 )
{
for(i=j; i<n2; i++,k++ )
arr[k] = R[i];
}
}
void MergeSort( int arr[], int p, int r )
{
if( p<r )
{
int q = ( p+r )/2;
MergeSort( arr, p, q );
MergeSort( arr, q+1,r );
Merge( arr, p,q,r );
}
}
int main()
{
int a[] = { 5,4,9,8,7,6,0,1,3,2 };
int len = sizeof( a )/sizeof( a[0] );
MergeSort( a,0,len-1 );
for( int i=0; i<len; i++ )
cout << a[i] << " ";
cout << endl;
return 0;
}
二路归并排序的过程需要进行logn趟。每一趟归并排序的操作,就是将两个有序子序列进行归并,而每一对有序子序列归并时,记录的比较次数均小于等于记录的移动次数,记录移动的次数均等于文件中记录的个数n,即每一趟归并的时间复杂度为O(n)。因此,二路归并排序的时间复杂度为O(nlogn)。
快速排序
快速排序是一种非常高效的排序算法,它采用“分而治之”的思想,把大的拆分为小的,小的再拆分为更小的。其原理是:对于一组给定的记录,通过一趟排序后,将原序列分为两部分,其中前部分的所有记录均比后部分的所有记录小,然后再依次对前后两部分的记录进行快速排序,递归该过程,直到序列中的所有记录均有序为止。
具体算法步骤如下。
(1)分解:将输入序列arr【m,…,n】划分为两个非空子序列arr【m,…,k】和arr【k+1,…,n】,使arr【m,…,k】中任一元素的值不大于arr【k+1,…,n】中任一元素的值。
(2)递归求解:通过递归调用快速排序算法分别对划分的两个子序列进行排序。
(3)合并:由于对分解出的两个子序列的排序是就地进行的,所以在两个子序列都排序好后,不需要执行任何计算就已排好序。
以数组 { 49,38,65,97,76,13,27,49 } 为例
当初始的序列整体或局部有序时,快速排序额性能将会下降,此时快速排序将退化为冒泡排序。
快速排序的相关特点如下:
(1)最坏时间复杂度
最坏情况是指每次区间划分的结果都是基准关键字的左边(或右边)序列为空,而另一边的区间中的记录项仅比排序前少了一项,即选择的基准关键字是待排序的所有记录中最小或者最大的。例如,若选取第一个记录为基准关键字,当初始序列按递增顺序排序时,每次选择的基准关键字都是所有记录中的最小值,这时记录与基准关键字的比较次数会增多。因此,在这种情况下,需要进行(n-1)次区间划分。对于第k(0<k<n
)次区间划分,划分前的序列长度为(n-k+1),需要进行(n-k)次记录的比较。当k从1~(n-1)时,进行的比较次数总共为 n(n-1)/2,所以在最坏情况下快速排序的时间复杂度为O(n^2)
(2)最好时间复杂度
最好情况是指每次区间划分的结果都是基准关键字左右两边的序列长度相等或者相差为1,即选择的基准关键字为待排序的记录中的中间值。此时,进行的比较次数总共为nlogn,所以在最好情况下快速排序的时间复杂度为O(nlogn)
(3)平均时间复杂度
快速排序的平均时间复杂度为O(nlogn)。虽然快速排序在最坏情况下的时间复杂度为O(n^2),但是在所有平均时间复杂度为O(nlogn)的算法中,快速排序的平均性能是最好的。
(4)空间复杂度
快速排序的过程中需要一个栈空间来实现递归。当每次对区间的划分都比较均匀时(即最好情况),递归树的最大深度为logn(向上取整)+1;当每次区间划分都使得有一边的序列长度为0时(最好情况),递归树的最大深度为n。在每轮排序结束后比较基准关键字左右的记录个数,对记录多的一边先进行排序,此时,栈的最大深度可降为logn。因此,快速排序的平均空间复杂度为O(logn)
(5)基准关键字的选取
基准关键字的选择是决定快速排序算法性能的关键。常用的基准关键字的选择有以下几种方式
- 三者取中
三者取中是指在当前序列中,将其首尾和中间位置上的记录进行比较,选择三者的中值作为基准关键字,在划分开始前交换序列中的第一个记录与基准关键字的位置。
- 取随机数
取left和right之间的一个随机数m,m作为基准关键字的位置
希尔排序
希尔排序也称为“缩小增量排序”。基本原理是:首先将待排序的元素分成多个子序列,使得每个子序列的元素个数相对较少,对各个子序列分别进行直接插入排序,待整个待排序列“基本有序”,再对所有元素进行一次直接插入排序。
希尔排序的关键并不是随便地分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列,实现跳跃式的移动,使得排序的效率提高。
堆排序
堆是一种特殊的树形数据结构,其每个节点都有一个值,通常提到的堆都是指一棵完全二叉树,根节点的值小于(或大于)两个子节点的值,同时根节点的两个子树也分别是一个堆
堆排序是一树形选择排序,在排序过程中,将R[ 1,…,N ]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲节点和孩子节点之间的内在关系来选择最小的元素。
堆一般分为大顶堆和小顶堆两种不同的类型。对于给定的n个记录的序列(r(1),r(2),…,r(n)),当且仅当满足条件 (r(i)>=r(2i), i=1,2,…,n)时称之为大顶堆,此时堆顶元素比为最大值。 反之则为小顶堆,此时堆顶元素为最小值.