本编博客介绍了两种三种排序方法,包括 简单选择排序、直接插入排序和希尔排序,这三种排序方法中,最后一个希尔排序的思路相对复杂一些。下面逐一介绍:
1. 简单选择排序
基本思想:每一趟在 n-i-1(i=1,2,3,……,n-1)个记录中选取关键字最小的记录作为有序序列的第 i 个记录
通俗理解:设置一个 自增变量 i,比如 i = 1时,通过min标记在 i>=2 至 N 的数据中的最小元素的序号,如果i != min 然后将 r[i] 与 r[min] 交换, 确保交换过的第 i 个元素是 i>=1至 N 中最小的。然后 i 递增,逐步确保第 2、3、4……N 个元素都是比较过程中最小的。
/* 最简单的选择排序 */
void SelectSort0( SqList *L )
{
int i, j, min;
for( i = 1; i < L->length; i++)
{
min = i;
for( j = i + 1; j < L->length; j++)
{
/* 找到其余节点中最小的,共比较n-i-1次 */
if( L->r[min] > L->r[j] )
min = j;
}
if( min != i)
/* 如果 r[i] 不是最小,则交换 */
swap( L, min, i );
}
}
- 无论最好最差的情况,其比较次数都是一样的多,第 i 趟 排序需要进行 n-i 次关键字的比较,此时需要比较 ∑(n-i) = n-1 + n-2 + …… +1 = n(n-1) / 2。对于交换次数而言,最好情况交换 0 次,最差的时候,交换次数为 n - 1 次,基于最终的排序次数和交换次数的总和, 因此,总的时间复杂度依然为 O(n^2)
- 应该说,尽管与冒泡排序的时间复杂度相同,但是简单选择排序的性能上还是要略优于冒泡排序。
2. 直接插入排序
引子:
扑克牌的插牌方法,我们平时玩扑克牌时候,每抓到一张牌,我们都自动的把它放到合适的位置(或递增、或递减),这就是来源于我们生活中的排序模型。
思路:将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增 1 的有序表。
通俗理解:顺序表r[0]不存储元素,作为辅助存储空间。 设置自增变量 i ,i= 1 时候,有序表为一个元素即r[1];然后 i 递增,i = 2,将r[2]插入到有序表中(有序表即一个元素r[1]),如果r[2] < r[1],则需要将 r[2]赋值为r[1],将r[1]赋值为r[2]。继续,i=3,将r[3]插入到有序表中(有序表有两个元素 r[1], r[2]),如果r[3] < r[2], 则需要先将r[3]赋值为r[2],继续比较r[3]与r[1]的大小,如果r[3] > r[1], 则r[1]不移动,将r[2]赋值为r[3],。否则,将r[2]为赋值为r[1],最后将r[1]赋值为r[3]。继续……
/* 对顺序表 L 作直接插入排序 */
void InsertSort( SqList *L )
{
int i, j;
for( i = 2; i < L->length; i++)
{
/* 从第二个记录开始后移,向前面的有序数列插入至合适的位置 */
if( L->r[i] < L->r[i-1])
{
L->r[0] = L->r[i];
for( j = i-1; L->r[j] > L->r[0]; j--)
{
/* 将比L->r[0]大的记录全部后移 */
L->r[j+1] = L->r[j];
}
/* 将记录插入到合适位置 */
L->r[j+1] = L->r[0];
}
}
}
复杂度分析
- 从空间上来看,它只需要一个记录的辅助空间,因此关键来看它的时间复杂度。
- 最好的情况,表本身有序,比较了n-1次,没有记录移动,时间复杂度为O( n )。
- 当最坏的情况,排序表逆序,比如{ 6, 5, 4, 3, 2 },此时需要比较 ∑i = (n+2)(n-1) / 2 次,记录的移动次数也达到最大值 ∑(i+1) = (n+4)(n-1) / 2 次。
- 如果排序记录是随机的,那么根据概率相同的原则,平均比较和移动次数约为 n^2/4,所以 O(n^2)。从这里也可以看出,同样的复杂度,直接插入排序比冒泡和简单选择排序的性能要略好一些。
3.希尔排序
历史渊源:希尔排序 是D.L.Shell 于1959 年提出来的一种排序算法, 在这之前排序算法的时间复杂度基本都是 O(n^2)的,希尔排序算法是突破这个时间复杂度的第一批算法之一。
基本有序:就是小的关键字基本在前面,大的基本在后面,不大不小的基本在中间,像{2, 1,3,6,4,7,5,8,9}这样就可以成为基本有序了,但像{1,5,9,3,7,8,2,4,6}这样的9在第三位,2在倒数第三位就谈不上基本有序了。
我们需要采取跳跃分割的策略:
将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行插入排序后得到的结构是基本有序而不是局部有序。
通俗理解:希尔排序是变相的插入排序,比较次数并没有减少,但是在增量increment 选择比较好的情况,移动数据的次数明显减少。increment按照 increment/3 + 1 的规律在变化,从一个比较大的数值,最后变到1,当increment = 1时候,可以查看下面的代码,和上述“简单插入排序”是完全相同的。从 do 开始 假设 Length = 10,则increment = 4,i 从 5 开始增至 N,i=5,将r[1],r[5]进行插入排序。 i++, i=6,将r[2],r[6]进行插入排序,继续i=7,8……,N。现在的线性表基本有序,然后 increment = increment/3 + 1 = 2,则i从3开始增至 N。i=3,将r[1],r[3]进行插入排序;i=4,将r[2],r[4]进行插入排序,继续i=5,将r[1],r[3],r[5]进行插入排序,继续i=6,将r[2],r[4],r[6]进行插入排序……
/* 对顺序表 L 作希尔排序 */
/* 和插入排序非常像,只是增量变了 */
void ShellSort( SqList *L )
{
int i, j;
int increment = L->length;
do
{
increment = increment/3 + 1; /* 增量序列 */
for( i = increment + 1; i < L->length; i++)
{
if( L->r[i] < L->r[i-increment] )
{
/* 需将L->r[i] 插入到有序增量表中(…… r[i-increment-increment],r[i-increment],r[i]) */
L->r[0] = L->r[i]; /* 暂存在L->r[0] */
for( j = i - increment; j > 0 && L->r[0] < L->r[j]; j -= increment )
L->r[j+increment] = L->r[j]; /* 记录后移,查找插入位置 */
L->r[j+increment] = L->r[0]; /* 将L->r[i]插入到合适位置 */
}
}
}while( increment > 1);
}
通过这段代码的分析,相信大家有些明白,希尔排序的关键并不是随便分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列,实现跳跃式的移动,使得排序的效率提高。所以,增量的选取很关键,上述函数中,用increment = increment /3 + 1的方式选取增量的,可究竟应该选取什么样的增量才是最好,目前还是一个数学难题,迄今为止还没有人找到一种最好的增量序列。不过大量研究表明,效率最高时候,时间复杂度为 O( n ^ 3/2) ,要好于 直接排序的 O(n^2)。需要注意的是,
增量序列的最后一个增量必须等于 1 才行
。由于记录是跳跃式的移动,希尔排序并不是一种稳定的排序算法。