经典排序算法系列之二:选择排序

1.序言

 

        这是经典排序算法系列的第二篇,讲的是选择排序的各种排序算法。选择排序主要包括:直接选择排序和堆排序。与上一篇一样,我阐述会这两种排序算法的基本思想,拓展两种算法的思路,分析他们的运行效率。我会由直接选择排序算法过渡慢慢深入到堆排序,让你轻松理解选择排序算法的内涵。

 

2.直接选择排序

 

2.1基本思想

        第i趟排序开始时,当前有序区和无序区分别为R[0..i-1]和R[i..n-1],该趟排序则是从当前无序区中选出关键字最小的记录R[k],将它与无序区的第一个记录R[i]交换,使得R[0..i] 和R[i+1..n-1]分别变为新的有序区和无序区,经过i-1趟排序后,整个表递增有序。

 

2.2算法思路

首先我们要做的就是从无序区中选出最小值下标,看看下面的代码:

int i;
int temp;
//存储最小值下标
int key = 0;
//遍历数组查找最小值下标
for(i = 0; i < length; i++)
{
	if(a[i]<a[key])
	{
		key = i;
	}
}


        通过上面的代码我们可以很轻松的找出无序区A[i..n]中最小值的下标,接下来就是将得到的最小值换到无序区的头部,即下标为i的位置,这样有序区A[0..i-1]的长度增加一个变为A[0..i],而无序区的长度则减少一个变为A[i+1..n],以此类推,当无序区的元素个数减少到1个时,这时的数组已经完成排序。为了让大家更好理解,在这里列举一个实例,假设用直接选择排序算法对数组a={7,4,3,1,5,2,6}进行排序,排序过程如下:

第一趟:无序区最小值下标为3,则a[0]和a[3]交换位置,数组为a={1,4,3,7,5,2,6},有序区为a[0],无序区为a[1..6];

第二趟:无序区最小值下标为5,则a[1]和a[5]交换位置,数组为a={1,2,3,7,5,4,6},有序区为a[0..1],无序区为a[2..6];

第三趟:无序区最小值下标为2,则a[2]不需要移动位置,数组为a={1,2,3,7,5,4,6},有序区为a[0..2],无序区为a[3..6];

第四趟:无序区最小值下标为5,则a[3]和a[5]交换位置,数组为a={1,2,3,4,5,7,6},有序区为a[0..3],无序区为a[4..6];

第五趟:无序区最小值下标为4,则a[4]不需要移动位置,数组为a={1,2,3,4,5,7,6},有序区为a[0..4],无序区为a[5..6];

第六趟:无序区最小值下标为6,则a[5]和a[6]交换位置,数组为a={1,2,3,4,5,6,7},有序区为a[0..5],无序区为a[6];

上面六步完成了对数组a的排序,现在我们可以按照上面的步骤写出直接选择排序算法的核心代码:

//直接选择排序算法
void straightSelectSort(int *a, int length)
{
	int i, j;
	int temp;
	//存储最小值下标
	int key;
	for(i = 0; i < length-1; i++)
	{
		key = i;
		//遍历数组查找最小值下标
		for(j = i+1; i < length; i++)
		{
			if(a[i]>a[j])
			{
				key = j;
			}
		}
		//如果无序区最小值不在无序区头部则交换位置到无序区头部
		if(key != i)
		{
			temp = a[i];
			a[i] = a[key];
			a[key] = temp;
		}
	}
}



2.3代码效率

        在直接选择排序中,共需要进行n-1次选择和交换,每次选择需要进行n-i 次比较 (1<=i<=n-1),而每次交换最多需要3次移动,因此,总的比较次数C=1/2(n*n - n),,总的移动次数3(n-1)。由此可知,直接选择排序的时间复杂度为O(n^2) (n的平方),所以当记录占用字节数较多时,通常比直接插入排序的执行速度快些。由于在直接选择排序中存在着不相邻元素之间的互换,因此,直接选择排序是一种不稳定的排序方法。

 

3.堆排序

 

3.1基本思想

        想要了解堆排序的基本思想,我们首先要了解大根堆和小根堆的含义。大根堆就是任何父亲结点值大于左右孩子结点值的完全二叉树,而小根堆就是任何父亲结点值小于左右孩子结点值的完全二叉树。堆排序就是先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区,再将关键字最大的记录R[0](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1]<=R[n],由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2]<=R[n-1..n],同样要将R[1..n-2]调整为堆。

 

3.2算法思路

        首先解释一下完全二叉树的概念:若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的节点都连续集中在最左边,这就是完全二叉树。下面我给大家展示大根堆和小根堆,如下图所示:

        根据上面对大根堆和小根堆概念的阐述,上面两个堆符合要求,我们从中可以看出,在大根堆中根结点值最大(小根堆中根结点值最小)。在堆排序算法中正式利用大根堆(或者小根堆)这一个特征来进行排序。所以在排序之前,我们必须使用输入数组建立好大根堆(或者小根堆)。

        当我们得到输入数组的时候,我们可以把它看成一个无序的堆。假设数组a的长度为n,数组元素从头到尾分别对应1,2,3,...,n,则由二叉树的特点,我们知道每一个结点i的左右孩子结点分别对应2*i和2*i+1,这样我们就可以建立相应的堆的模型,建立了堆的模型之后,我们还要将这个堆调整为大根堆(或者小根堆),为了让大家对这个过程有一个更直观的印象,我以数组a={2,9,8,5,4,1}为例来建立堆模型并调整为大根堆,如下图所示:

        由上图步骤可知,该方法是将孩子结点中的最大值往上浮,而下沉的结点可能会破坏子树的大根堆性质,所以继续对子树进行调整,最终得到一个最大堆。根据图示的步骤我们可以很轻松的实现其核心代码:

//实现最大堆
void maxHeapify(int *a, int i)
{
	int temp;
	//存储较大结点下标
	int max = i;
	//比较左右孩子结点大小
	if(a[2*i]>a[2*i+1])
	{
		max = 2*i;
	}
	else
	{
		max = 2*i+1;
	}
	//父亲结点不是较大值则与较大孩子结点交换
	if(max != i)
	{
		temp = a[i];
		a[i] = a[max];
		a[max] = temp;
	}
	//当孩子结点与父亲结点交换位置后,子树可能不是最大堆,对交换的孩子结点调用本函数,让子树成为最大堆
	maxHeapify(a,max);
}

 

        我们已经为建立大根堆完成核心工作,剩下的工作就不难完成了,要建立大根堆,我们就必须对完全二叉树从下至上的每一个非叶子结点调用一下maxHeapify即可,实现建立大根堆的核心代码如下:

//建立大根堆
void buildMaxHeap(int *a, int length)
{
	int i;
	//从下至上对每一个非叶子结点调用maxHeapify
	for(i = length/2; i > 0; i--)
	{
		maxHeapify(a,i-1);
	}
}


           在建立大根堆之后,整个树的最大结点一定是根结点,我们要做的就是将大根堆的根结点与a[n]交换位置,于是我们再以数组a[0..n-1]建立大根堆,将根结点与a[n-1]交换位置,以此类推,当大根堆只剩下一个元素时,整个数组已经排好序。实现排序的核心代码如下:

//堆排序算法
void heapSort(int *a, int length)
{
	int i;
	int temp;
	//建立大根堆
	buildMaxHeap(a,length);
	//将大根堆根结点与尾节点交换位置
	for(i = length; i > 1; i--)
	{
		temp = a[0];
		a[0] = a[i-1];
		a[i-1] = temp;
		length = length-1;
		maxHeapify(a,0);
	}
}


3.3代码效率

      堆[排序的时间,主要由建立初始]堆和反复重建堆这两部分的时间开销构成,它们均是通过调用Heapify实现的。堆排序的最坏时间复杂度为O(nlogn)。堆序的平均性能较接近于最坏性能。由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。堆排序是就地排序,辅助空间为O(1),它是不稳定的排序方法。

 

4.参考资料

 

《算法导论》机械工业出版社 Thomas H.Cormen   Charles E.Leiserson   Ronlad L.Rivest   Clifford Stein

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值