上面介绍了几种常用的简单排序算法,接下来介绍几种基于简单排序算法改进后的排序算法。
注:Swap方法具体实现见上篇
1.希尔排序(基于直接插入排序算法的改进)
思想:直接插入排序在待排序的记录较少或记录集合本身基本有序时,具有很大的优势,但是并不适合与待排序的数量较多或待排序集合无序甚至接近逆序的情况。该算法通过采用跳跃策略,即将相距某个增量的记录组成一个子序列,然后在子序列内分别进行直接插入操作得到的结果保证是基本有序的(所谓基本有序:就是小的记录基本在前面,大的基本在后面,不大不小的基本在中间)。该算法的增量序列是一个按某个公式递减的过程,循环截止的条件是增量小于等于1的时候。相对于直接插入排序算法更高效。
代码实现如下:(蓝色加深部分即为相对直接插入排序算法改进的地方)
void ShellSort(int *a,int length)
{
int i,j;
int increment = length; //length为待排序的记录个数
do
{
increment = increment / 3 + 1; //增量序列
for(i = increment + 1;i <= length;i++)
{
if(a[i] < a[i - increment])
{
a[0] = a[i]; //a[0]为哨兵
for(j = i - increment;j > 0 && a[0] < a[j];j -= increment)
{
a[j + increment] = a[j]; //记录每隔increment进行后移
}
a[j + increment] = a[0];
}
}
}while(increment > 1);
}
复杂度分析:
该算法“增量”的选取非常关键,究竟选取什么样的增量才是最好,迄今为止还没有人找到一种最好的增量序列。不过大量的研究表明,当增量序列为dlta[k] = 2^(t-k+1) - 1(0 <= k <= t <= log2(n+1)向下取整)时,可以获得不错的效果,其时间复杂度为O^(3/2),要好于直接插入排序O(n * n)。需要注意的是,增量序列的最后一个增量值必须等于1才行。
时间复杂度:
最好情况:时间复杂度O(n^1.3)
最坏情况:时间复杂度O(n^2)
平均情况:O(nlogn)-O(n^2)
空间复杂度:同直接插入排序算法,即也需要一个辅助空间,即为O(1)
稳定性:
不稳定
2.堆排序
思想:将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点,将根结点与最后一个叶子节点互换,得到的就是一个除根结点以外,其他结点都满足大顶堆定义的二叉树,将剩下的n-1个序列重新构建一个堆,这样就会得到n个元素中的次大元素,放在倒数第二个位置。如此反复执行,便能得到一个有序的序列了。
算法实现如下:
/* 已知a[s...m]中除了关键字a[s]外均满足堆的定义 */
/* 本函数调整a[s]的关键字,是a[s...m]成为一个大顶堆 */
void HeapAdjust(int *a,int s,int m)
{
int temp,j;
temp = a[s]; //用temp暂存a[s]
/* 沿关键字较大的孩子结点向下筛选,因为父节点如果比较大孩子结点小,
*则两者交换后,造成以较大孩子结点为父节点往下有可能不满足大顶堆的定义,
* 需要继续循环比较,而较小的孩子结点仍然满足大顶堆的定义
*/
for(j = s * 2;j <= m;j *= 2)
{
if(j < m && a[j] < a[j + 1])
{
/* 如果j不是最后一个结点,则从其和其右兄弟中选出一个较大的元素 */
++j;
}
if(temp >= a[j]) //如果当前结点的值大于或等于其孩子中的较大者,则满足大顶堆的定义,退出循环
{
break;
}
a[s] = a[j]; //将其孩子中的较大结点的值赋给当前结点
s = j;
}
a[s] = temp;
}
/* 堆排序算法 */
void HeapSort(int *a,int length)
{
int i;
for(i = length / 2;i > 0;i--)
{
HeapAdjust(a,i,length); //初始化,从最后一个非叶子结点开始构造大顶堆
}
for(i = length;i > 1;i--)
{
Swap(a,i,1); //将大顶堆的堆顶,即根结点与最后一个叶子结点互换
/*将剩下的n-1个结点重新构造大顶堆
*(从1开始构造是因为,除根结点以外,其他都已满足大顶堆的定义)
*/
HeapAdjust(a,1,i - 1);
}
}
复杂度分析:
时间复杂度:它的运行时间主要消耗在初始构建堆和重建堆时的反复筛选上。在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子结点进行比较和若有必要的交换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)。重建堆的时间复杂度为O(nlogn)。所以总体来说,堆排序的时间复杂度为O(nlogn).由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度都是O(nlogn)。
空间复杂度:只有一个用来交换的暂存单元(temp)。因此空间复杂度为O(1)。,
稳定性:不稳定。
3.归并排序
思想: