插入排序和希尔排序(shell sort)

对于排序算法,我们一开始都是学习的最简单暴力的算法,即冒泡排序和选择排序,这两个差不多,都是思想简单,实现起来也很简单,但就是时间复杂度非常高,无论是最优效率还是最差效率,复杂度都为O(n2)。

这里我将详细介绍一下两种比较高级的排序算法——插入排序和希尔排序(shell sort)

注:这里的排序算法默认都是非降序排列

插入排序(insertion sort)

插入排序是一种减治法的思想,并且是减治法中的减一技术

有关减治法,可以参考我的另一篇博文:《减治法》

插入排序的主要思想就是假设前n-1个元素都已经是有序队列了,然后将第n个元素插入到有序队列中的对应位置中。

实现思路

从插入排序的主要思想中,我们可以发现这是基于递归思想实现的,但是很明显使用迭代的效率会更高一些。

那么我们应该可以很容易地想到从后面开始依次进行插入,即第n个元素首先和第n-1个元素进行比较。

那么可以从前面开始判断吗

其实最好还是不要,因为我们可以想一想,如果从前面开始判断,那么假设知道第n个元素应该被放在第i个位置,则剩下n-i个元素仍然需要遍历,即无论第n个元素应该被放在第1个位置(最差情况)还是第n个位置(最优情况),一次插入都需要遍历n个元素,这和冒泡排序、选择排序有什么区别呢?因此,要想时间复杂度小于O(n2),则必须从后面开始比较。

先上伪代码

因为伪代码没有其他语言的语法限制,能够有助于我们更好地将注意力放在算法上,所以这里我们先用伪代码来讲述一下主要思想:

algorithm insertionSort(A[0..n-1])
//用插入排序对给定数组排序
//输入:n个可排序元素构成的一个数组A[0..n-1]
//输出:非降序排列的数组A[0..n-1]
for i <- 1 to n-1 do
	v <- A[i]
	j <- i-1
	while j >= 0 and A[j] > v do
		A[j+1] <- A[j]
		j <- j-1
	A[j+1] <- v  //这里注意,因为第j个元素不大于第i个元素,所以应该放在第j+1个位置

那么我们再来看看图解(其中,竖线将输入的有序部分和剩下的元素分开,正在插入的元素用粗体表示,即为第i个元素):

在这里插入图片描述

代码实现

这里我用C++来实现。

首先定义一个数组:

int A[10] = {23, 5, 7, 4, 89, 76, 2, 6, 44, 33};

然后便是插入排序的关键代码:

void insertionSort(int* A) {
	for (int i = 1; i < 10; i++) {
		int temp = A[i];
		int j = i - 1;
		while (j >= 0 && A[j] > temp) {
			A[j + 1] = A[j];
			j--;
		}
		A[j + 1] = temp;
	}
}

在main函数中启动:

int main() {
	cout << "排序前的数组:" << endl;
	for (int i = 0; i < 10; i++) {
		cout << A[i] << " ";
	}cout << endl;

	insertionSort(A);

	cout << "排序后的数组:" << endl;
	for (int i = 0; i < 10; i++) {
		cout << A[i] << " ";
	}cout << endl;
	return 0;
}

运行结果如下:

在这里插入图片描述

接下来,我们再来看看插入排序的时间复杂度:

时间复杂度

我们可以将该算法的基本操作规定为A[j] > v,那么,

最优情况,即为该数组已经是非降序排列的:
C b e s t = ∑ i = 1 n − 1 1 = n − 1 ∈ O ( n ) C_{best}=\sum_{i=1}^{n-1}1=n-1\in O(n) Cbest=i=1n11=n1O(n)
最差情况,即为该数组是非升序排列,刚好为逆序数组:
C w o r s t = ∑ i = 1 n − 1 ∑ j = 0 i − 1 1 = ∑ i = 1 n − 1 i = ( n − 1 ) n 2 ∈ O ( n 2 ) C_{worst}=\sum_{i=1}^{n-1}\sum_{j=0}^{i-1}1=\sum_{i=1}^{n-1}i=\frac{(n-1)n}{2}\in O(n^2) Cworst=i=1n1j=0i11=i=1n1i=2(n1)nO(n2)
由此,我们可以很容易地发现一个问题,这最优情况与最差情况的差距太大了!而且,最差情况与冒泡排序、选择排序差不多。。。

总结

虽然这种算法对于一个完全无序的数组进行排序的效率不高,但是如果是处理基本有序的数组,那么使用这种算法的效率就会很明显,因此,这种算法常用于对一个有序的数组插入一个无序的数组。

希尔排序(shell sort)

希尔排序其实是插入排序的一种扩展,因为考虑到插入排序每插入一个元素,都需要与前n-1个元素依次进行比较,直到找到一个不大于该元素的元素,这其中就会花费大量的时间进行比较,而如果能够将比较的次数缩短,那么算法的效率就能够得到极大的改进了。因此,这才有了希尔排序。

主要思想:首先设置一个增量序列,该序列应该由大到小排列,且最后一个元素必须为1(步长为1的情况就是前面讲的插入排序),然后根据增量序列中的步长,将待排序的数组分成若干组,以组的形式进行插入排序。由于大多数情况下的数组都是无序的,那么如果一个很小的元素排在比较靠后的位置,步长为1的时候就需要进行多次的比较操作,因此希尔排序就是希望开始的时候“步子迈大一点”,这样比较靠后的小元素就能够很快的排到前面去,从而减少比较的次数。最后一个步长必须为1,是为了进行最终的比对,只有步长为1的情况经历了一遍,我们才能肯定数组已经是有序的了,否则可能在局部还是无序的。

因此希尔排序的关键就是增量序列的选取!

先上伪代码

老规矩,先来看看伪代码:

algorithm shellSort(A[0..n-1])
//用希尔排序对给定数组排序
//输入:n个可排序元素构成的一个数组A[0..n-1]
//输出:非降序排列的数组A[0..n-1]
for t <- 0 to k-1 do  //步长数组D[0..k-1],并且D[k-1] = 1
	d <- D[t]
	for i <- d to n-1 do
		for j <- i-d to 0 step -d do  //每次j-d
			if(A[i] < A[j]) swap(A[i], A[j])

例如:对于序列(256,301,751,129,937,863,742,694,76,438),用希尔排序(增量序列为d=5,3,1):

第一趟(d=5):25630169476438863742751129937

第二趟(d=3):76301129256438694742751863937

第三趟(d=1):76,129,256,301,438,694,742,751,863,937

其中,颜色相同的表示在一组中。

代码实现

以之前的例子为例。

数据如下:

int A[10] = {23, 5, 7, 4, 89, 76, 2, 6, 44, 33};

然后希尔排序函数:

int step[3] = {5, 3, 1};  //增量序列
void shellSort(int* A) {
	for (int i = 0; i < 3; i++) {
		int d = step[i];
		for (int j = d; j < 10; j++) {
			for (int k = j - d; k >= 0; k -= d) {
				if (A[j] < A[k]) swap(A[j], A[k]);
			}
		}
	}
}

然后在main函数中启动:

int main() {
	cout << "排序前的数组:" << endl;
	for (int i = 0; i < 10; i++) {
		cout << A[i] << " ";
	}cout << endl;

	shellSort(A);

	cout << "排序后的数组:" << endl;
	for (int i = 0; i < 10; i++) {
		cout << A[i] << " ";
	}cout << endl;
	return 0;
}

运行结果如下:

在这里插入图片描述

时间复杂度

注意:希尔排序的时间复杂度依赖于增量序列的选取!

研究表明:

当选取的增量序列为n/2k(n为数组元素个数,k=1,2,3…),如n/2,n/4,n/8,…,1,则最差的时间复杂度为n2

据说,增量序列为1,4,13,40,121的效率是最高的(书上说的,我不负责,hhh)

因此增量序列的选取也是一门学问,一个好的增量序列能够大大提高希尔排序的效率。

总结

由于希尔排序一开始是按照分组进行排序的,因此相同元素的相对位置可能在中途会被打乱顺序,因此希尔排序其实是不稳定的。

其中算法的稳定性指的是,一个数组中的元素相对位置在进行排序之后不会发生改变,如1,2,1这个数组,第一个1和第二个1虽然数值是一样的,但是相对位置是不一样的,如果将这两个1对换位置了,则说明该算法不稳定,否则就是稳定的。

参考资料

《算法设计与分析基础》第三版以及老师的课件

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花无凋零之时

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值