数据结构--快速排序(复习)

快速排序的核心思想是,对于一个数组,当我选定其中的一个数值(这里以数组当前的首元素为例),我通过交换等手段让数组中所有比该元素小的元素都跑到该元素的左边,所有比该元素大的元素都跑到该元素右边。完成后,再分别对该元素的左右两部分子数组进行同样的操作,依此类推,若干次运算之后,整个数组就可以排好序了。

对于快速排序而言,每次确定中心点并以此为依据交换元素时,对于整个数组而言(因为计算几次之后,整个数组会被分成若干个小数组),时间复杂度都为O(N),因此制约快速排序速度的因素就是排序时处理的次数,亦即多少次循环之后,对于每一个选定的中间值其左数组和右数组所含元素均不大于1(这种情况下,对于一个小部分来说,只要被选定元素的左面小于它,右面大于它,这个小部分就是有序的,亦即整个数组也是有序的)。由于快速排序的方法在直观上类似二叉树,可以想象,选取的中间值其本身的大小越接近待处理部分的数组中间元素的大小,整个数组被分割成单个元素的速度也就越快。反之,如果整个数组原本就很有序,那首元素的最终位置还是接近数组的开头部分,那数组被分割成单个元素需要的循环次数也就越多,所要花费的时间也就越长。这也就是为什么对于快速排序而言,数组越有序,排序花费的时间反而越多。

当我们排序一个数组时,我们只能决定我们排序的方式,没法决定排序的对象。而对于快速排序而言,数组内容会显著影响排序的速度,而选取的值在最终数组中的位置又是决定排序速度的关键。因此,在排序开始之前,应该额外增加一步,让我选择的初始值更接近数组最终排好序时的中间位置。这就需要用到三数取中法。

int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

确定了所取的初始值后,就可以开始正式排序了,这里介绍两种方法:

法1:

int Partion1(int* a, int left, int right)//Partion123函数的意义都是将数组排列成选定值的左边的数值都小于选定值,选定值右边的数值都大于选定值的效果并返回最终选定值在
//数组中所处位置的下标
{
	// 三数取中 -- 面对有序最坏情况,变成选中位数做key,变成最好情况
	int mini = GetMidIndex(a, left, right);
	Swap(&a[mini], &a[left]);//将选择出来的最中间的值放到数组的最前面

	int keyi = left;//将基准值的下标保存在整型变量keyi中
	while (left < right)
	{
		// 右边先走,找小
		while (left < right && a[right] >= a[keyi])
			--right;

		//左边再走,找大
		while (left < right && a[left] <= a[keyi])
			++left;

		Swap(&a[left], &a[right]);
	}

	Swap(&a[left], &a[keyi]);//运行到这一步时,一定是跳出了while循环,即此时left=right,对于二者相交的位置,在该位置左边的数一定小于基准值,在该位置右边的数一定
	//大于基准值

	return left;
}

法2:挖坑法

int Partion2(int* a, int left, int right)
{
	// 三数取中 -- 面对有序最坏情况,变成选中位数做key,变成最好情况
	int mini = GetMidIndex(a, left, right);
	Swap(&a[mini], &a[left]);
	int key = a[left];
	int pivot = left;
	while (left < right)
	{
		// 右边找小, 放到左边的坑里面
		while (left < right && a[right] >= key)
		{
			--right;
		}
		a[pivot] = a[right];//将挑选出的右侧的值赋给左边的“空”之后,该右侧的值处也就同样出现了一个空(即pivot=right)
		//注:这里“空”的意思并不是指这个位置真的没有元素,而是这个位置的元素的值已经被放到其它位置上去了,该元素原本位置上的本体可以不用多余考虑直接覆盖,
		//直观的看起来就像是一个空
		pivot = right;

		// 左边找大,放到右边的坑里面
		while (left < right && a[left] <= key)
		{
			++left;
		}
		a[pivot] = a[left];
		pivot = left;
	}
	a[pivot] = key;//当数组中的每一个元素都被遍历后,亦即当左右需要调换的值都调换完毕时,最后出现的“空”的位置处,一定该空的左侧所有值都小于基准值,右侧所有值都大于
	//基准值,那顺理成章的将基准值放在这个位置上即可
	return pivot;
}

以上的两种方法完成的任务是将其所处理的数组变成所选定元素的左边都比该元素小,右边都比该元素大。显而易见,这时递归法实现快速排序的其中一步。下面的代码是快速排序的递归实现:

void QuickSort(int* a, int left, int right)
{
	if (left >= right)//和上面同理,当然这个if是针对递归过程中的每一个小数组而言,略有不同之处在于当left>=left时说明再没有比较的必要了
		return;
	int keyi = Partion2(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi+1, right);
}

快速排序的非递归实现:递归方法如果递归过深,容易发生栈溢出,因此要掌握非递归的方法。核心思想是实现数据结构中的栈来代替计算机中栈的作用,由于人为实现的栈的空间是从堆中申请来的,而计算机中的堆空间远远大于栈空间,因此非递归方式也用到了栈,但却不容易发生栈溢出。

对于下面的函数而言,真正压入或从栈中取出的不是具体的元素,而是元素的下标。这些下标进进出出,本质上都是为我所实现的Partion函数服务的,以帮助该函数确定它要处理的小数组的下标范围。

void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	StackInit(&st);
	StackPush(&st, left);
	StackPush(&st, right);

	while (!StackEmpty(&st))//当栈不为空时,说明还有需要处理的部分
	{
		int end = StackTop(&st);
		StackPop(&st);

		int begin = StackTop(&st);
		StackPop(&st);//当前栈中的最后两个元素就是我需要处理的小数组下标,我将这两个下标储存在整型变量end和begin中,再用Partion函数处理,当然,
		//处理完之后,这两个下标的使命就完成了,需要将其出栈,方便后续的处理

		int keyi = Partion3(a, begin, end);
		// [begin, keyi-1] keyi [keyi+1, end]
		if (keyi + 1 < end)//当这两个if中的条件满足时,说明keyi两侧的小数组还有处理的必要,要将待处理的小数组的下标范围压入栈中
		{
			StackPush(&st, keyi+1);
			StackPush(&st, end);
		}

		if (begin < keyi-1)
		{
			StackPush(&st, begin);
			StackPush(&st, keyi-1);
		}
	}

	StackDestroy(&st);
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值