八大排序算法之插入排序
排序分为内部排序和外部排序,内部排序时数据记录在内存中进行排序,适用于数据量比较少的情况;而外部排序因数据量太大,内存一次不能容纳所有的数据,此时需要借助文件来进行排序。
本文这里介绍的八大排序为内部排序,然后通过内部排序来实现一种外部排序。
下面先对排序进行分类如下:
在具体实现排序之前,先介绍三个衡量算法性能的标准—时间复杂度、空间复杂度和稳定性。
时间复杂度:一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
空间复杂度:空间复杂度是指算法在计算机内执行时所需存储空间的度量
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
这里先介绍插入排序中的直接插入排序和希尔排序,以下例子以升序为例。
一、直接插入排序(Insert Sort)
1.基本思想
直接插入排序的思想和生活中打扑克理牌的过程。打扑克牌时要将每张牌按顺序整理好,比如说刚开始摸了张7,
只有一张牌不用排序;又摸了张5,从后往前找第一张比5打的牌,将5插入该牌之前;然后摸了张3,同理将3插在已有的牌中第一张比3小的牌之前,同理后面出现的牌根据点数依次找到插入的位置即可。
直接插入排序的过程和上面描述的整理扑克牌的过程很相似。总结一下分为三步:(1)找位置;(2)移动数组;(3)将待排序的书插入。
2.排序过程
以数组{7,5,3,9,6}为例进行说明,排序过程如下:
3.代码实现与分析
(1)代码实现
void Insert_Sort(int *pArray, int iLen)
{
int iCount;
int iIndex;
int iTemp;
//n个数最多排(n-1)趟
for (iCount = 1; iCount < iLen; ++iCount)
{
iTemp = pArray[iCount]; //先将待排序元素存入临时变量中
//找位置,从前往后找第一个比待排序元素大的元素,记录其位置
for (iIndex = 0; iIndex < iCount; ++iIndex)
{
if (iTemp < pArray[iIndex])
{
break;
}
}
//找到第一个比待排序元素大的元素以后,整体后移
for (int i = iCount; i > iIndex; --i)
{
pArray[i] = pArray[i - 1];
}
//整体后移以后插入待排序元素
pArray[iIndex] = iTemp;
}
}
(2)分析
一般分析一个算法都是从时间复杂度、空间复杂度和稳定性来进行分析。
a)时间复杂度
从程序中可以看出,嵌套了两层循环。第一层循环用于控制循环的次数,N个数训话N-1次;第二层循环用于找元素插入的位置和移动元素,这两个循环是并列关系,所以在计算时间复杂度时可以看成一层循环。所以时间复杂度为O(N^2).
b)空间复杂度
从上面代码中可以看出,不管有多少个元素进行排序,都只使用了一个额外的临时变量,所以空间复杂度为O(1)。
c)稳定性
本算法是稳定的,读者可以根据程序自行证明一下。
4.程序改进
上面的程序在最好的情况下(即原始数组有序)的时间复杂度也为O(N^2),这就让我们觉得不太合理,应该有一种方法在数组有序的情况下加速排序过程。仔细思考上面的代码,发现之所以最好的情况下时间复杂度也为O(N^2),是因为以下这个循环始终都会进行:
for (iIndex = 0;iIndex < iCount; ++iIndex)
{
if (iTemp < pArray[iIndex])
{
break;
}
}
因为每次都是从前往后找,所以即使原始数据有序循环还是全部执行。
为了改善上面的问题,我们换一种思路。上面的程序是从前往后找,找到插入的位置以后再整体移动;我们可以从后往前找,找第一个比待排序元素小的元素,假设现在序列为{5, 7, 3}
待排序元素为3,此时3先和7比较,发现7比3大,此时7往后移一个位置(这里就是和上面程序不同之处,将比较和移位放在一个循环中进行);然后3和5比较,发现5也比3大,然后5也后移一个位置,此时再将3插入到5之前即可。
代码实现如下:
void Insert_Sort_(int *pArray, int iLen)
{
int iCount;
int iIndex;
int iTemp;
//n个数最多排(n-1)趟
for (iCount = 1; iCount < iLen; ++iCount)
{
iTemp = pArray[iCount]; //先将待排序元素存入临时变量中
//找位置,从后往前找第一个比待排序元素小的元素。改进之处:边找边移位
for (iIndex = iCount - 1; iIndex >= 0; --iIndex) // 1 2 3 4 5
{
if (iTemp >= pArray[iIndex])
{
break;
}
else
{
pArray[iIndex + 1] = pArray[iIndex];
}
}
//插入待排序元素
pArray[iIndex + 1] = iTemp;
}
}
分析:
此时假设元素数组元素有序{1,2,3,4,5},里面的循环每次都只比较一次就break了,所以此时的时间复杂度在最好的情况下降到了O(n)。
小结:直接插入排序越有序越快。
二、希尔排序
1.基本思想
希尔排序和直接插入排序一样都是插入排序的一种,而且希尔排序是在直接插入排序的基础上发展而来,因为每一趟希尔排序的过程用的都是直接插入排序。借助直接插入排序越有序越快这一特点,我们先对原始数据进行间隔性分组,先使组内有序,然后再缩小间隔,再使组内有序;直到间隔为1的时候,再进行排序即可。
总结一下步骤:(1)先定义分组间隔;(2)根据间隔进行分组,使用直接插入排序使组内有序;(3)最后一趟排序过程的间隔必须为1,此时再使用直接插入排序即可。
2.排序过程
假设原始待排序列为{12, 4,6, 9, 0, 34, 56, 7, 8, 10, 23, 45, 21, 11, 2},间隔序列为{5, 3, 1}。刚开始以5为间隔进行分组,则分组情况如下:
同理,可得到间隔为3和间隔为1时的排序序列。
(1).代码分析与实现
void ShellSort(int *pArray, int iLen)
{
int iDlt_a[] = {3, 1 };
for (int i = 0; i < (sizeof(iDlt_a) / sizeof(*iDlt_a)); ++i)
{
Shell(pArray, iLen, iDlt_a[i]);
}
}
//一趟希尔排序其实就是一趟直接插入排序
void Shell(int *pArray, int iLen, int dlt)
{
int iCount;
int iIndex;
int iTemp;
for (iCount = dlt; iCount < iLen; iCount++) //需要iLen-dlt趟
{
iTemp = pArray[iCount]; //临时变量暂存待排序的值
for (iIndex = iCount - dlt; iIndex >= 0; iIndex -= dlt)
{
if (pArray[iIndex] < iTemp) //满足条件说明找到第一个比待排序元素小的元素,iIndex指向其位置
{
break;
}
else
{
pArray[iIndex + dlt] = pArray[iIndex];
}
}
pArray[iIndex + dlt] = iTemp;//iIndex指向的是第一个比待排序元素小的元素的位置,我们应该把待排序元素插在iIndex之前,即iIndex+dlt
}
}
(2)分析
希尔排序的分析是一个复杂的问题,因为它的时间是所取“增量”序列的函数,这涉及到数学上一些尚未解决的难题。下面的分析摘自《数据结构(严蔚敏)》,仅供参看。
a)时间复杂度
时间复杂度为O(n^1.3) ~ O(n^1.5).
b)空间复杂度
从上面的程序中可以看出,用到的额外空间主要是一个增量序列和一个临时变量,这两个在程序运行前都是确定好的,是不会随着待排序规模的增改而改变的,所以希尔排序的时间复杂度为O(1)。
c)稳定性
不稳定。跳跃式的交换数据都不稳定。