【数据结构】排序(二)

23 篇文章 1 订阅
11 篇文章 0 订阅

在认识了插入排序和选择排序后,我们再来了解一下交换排序,其中主要结束快速排序,包括其递归实现以及非递归实现。
在这里插入图片描述

三.交换排序

1.冒泡排序

概念及分析

冒泡排序,顾名思义,每趟将待排序数组中的最大值冒到序列的最后一个位置,当进行n - 1趟后,序列便成了升序。
在这里插入图片描述

算法分析

冒泡排序总共需要冒n - 1趟,每趟比较n减去已经比较好的数的个数再减1,即n - 1 - i次比较,若相邻两数前者比后者大,则二者交换,因此冒泡排序的代码为:

void BubbleSort(int* a, int n)
{
	int i = 0;
	for (i = 0; i < n - 1; i++)//冒泡排序总共进行n - 1趟
	{
		int j = 0;
		int flag = 1;//假设序列已经有序
		for (j = 0; j < n - 1 - i; j++)//每趟排序比较n - 1 - i次
		{
			if (a[j] > a[j + 1])//若前者比后者大,二者交换
			{
				Swap(&a[j], &a[j + 1]);
				flag = 0;
			}
		}
		if (flag == 1)//flag仍为1说明冒泡过程中未进行交换,即数组已经有序,则跳出循环
		{
			break;
		}
	}
}

需要注意的一点是:这里设置了一个变量flag来记录序列是否有序的状态,首先假设序列是有序的,即设flag为1,若序列并非有序那么会进行交换,这个过程中将flag置为0,反之若序列有序则flag仍为1,这时无需进行后续比较即可跳出循环,使代码更加优化。

小结

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)

2.快速排序

概念

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

算法分析与实现

void QuickSort(int* a, int left, int right)
{

	if (left >= right)
		return;
	//按照基准值对数组区间[left, right]进行划分
	//使基准值左边的值均小于基准值,基准值右边的值均大于基准值
	int keyi = PartSort1(a, left, right);
	//划分成功后以keyi处的元素为边界形成了[left, keyi - 1] keyi [kei + 1, right]三个区域
	//递归对[left, keyi - 1]排序
	QuickSort(a, left, keyi - 1);
	//递归对[keyi + 1, right]排序
	QuickSort(a, keyi + 1, right);
}

上述为快速排序递归实现的主框架,而将区间按照基准值划分为左右两部分的常见方法有:

1.hoare版本

hoare版本是快排最初创建时使用的划分方法,其思想是:选定一个数作为key(比如left处的值),然后让right和left分别从两边开始遍历,其中right先往左走,当right遇到比key小的值时停下,让left往右走,当left遇到比key大的值时让left和right处的值交换,接着重复上述步骤直至left与right相遇,这时将key与相遇处的值交换,这便是hoare版本的划分思路。
在这里插入图片描述
hoare版本的代码为:

int PartSort1(int* a, int left, int right)//hoare版本
{
	int keyi = left;
	int key = a[left];
	while (left < right)
	{
		//right从右往左找比key小的值
		while (left < right && a[right] >= key)
		{
			right--;
		}
		//left从左往右找比key大的值
		while (left < right && a[left] <= key)
		{
			left++;
		}
		//交换left和right处的值
		Swap(&a[left], &a[right]);
	}
	//left和right相遇,将keyi处的值与相遇处的值交换
	Swap(&a[right], &a[keyi]);
	keyi = left;
	return keyi;
}
2.挖坑法

挖坑法在思想上与hoare版本的并无二异,即将第一个值赋给临时变量key,从而形成一个坑位hole,然后让right从右往左找比key小的值,将其放在坑位hole中,同时right处形成坑位(即将right赋值给hole),接着让left从左往右找比key大的值,将其放在坑位hole中,同时left处形成坑位(将left赋值给hole),重复上述操作,直到left和right相遇,再将key值填入坑中,即完成了一次划分。
在这里插入图片描述
因此,挖坑法的代码即为:

int PartSort2(int* a, int left, int right)//挖坑法
{
	int hole = left;
	int key = a[left];
	while (left < right)
	{
		//right从右往左找比key小的值
		while (left < right && a[right] >= key)
		{
			--right;
		}
		//找到后将该值填入坑中,同时形成新的坑位
		a[hole] = a[right];
		hole = right;
		//left从左往右找比key大的值
		while (left < right && a[left] <= key)
		{
			++left;
		}
		//找到后将该值填入坑中,并形成新的坑位
		a[hole] = a[left];
		hole = left;
	}
	//最后将key值填入坑中
	a[hole] = key;
	return hole;
}
3.前后指针版本

前后指针的思想是前面的指针cur从左往右找比key小的值,每次找到比key小的值后,让prev向后走一步,同时交换cur和prev处的值;当cur遇到比key大的值时,保持prev不动,让cur向后走一步即可,这样当cur往后走到数组末尾时结束,同时让prev处的值和key值交换,这样就完成了一次划分。
在这里插入图片描述
由此即可写出前后指针版本的代码:

int PartSort3(int* a, int left, int right)//前后指针版本
{
	int keyi = left;
	int key = a[left];
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		//cur每次均往后走一步,在遇到比key小的值时,且prev向后走一步与cur不重合
		//则交换prev和cur处的值
		if (a[cur] <= key && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	//cur走到数组末尾时交换keyi处的值与prev处的值
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}

快速排序代码的优化

注意到如果left处的值取的是最大或最小值时,一次划分之后的数组并未发生变化,那么这样将会使时间花销大大增加,更为甚者,若所要排序的数组已经有序,那么这种最坏情况的时间花销为O(N^2),这便是快速排序的致命缺陷,那么为了应对这种情况,我们就需要对代码进行优化,即优化对key值的取法,关于解决方法,有人提出取随机值的解决方法,但是这种方法过于随机,如果极端一点,每次取的都是最大或最小值,那么实际优化的效果并不佳。因此在经过多次的总结和实验后,三数取中法被认为是最佳的优化方法,即在left,right和mid中取中间值,然后让a[left] 与a[mid] 交换,这样就可以避免所取的key值为最大或最小值,三数取中具体代码如下:

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

在划分的代码部分前加上以下代码即可:

	int mid = GetMid(a, left, right);
	Swap(&a[left], &a[mid]);

快速排序的非递归实现

由于递归调用栈,而实际再内存中栈的空间是很小的,因此当递归深度很大时,非常容易造成栈溢出,所以了解快速排序的非递归实现是有必要的,快速排序的非递归实现是通过借助数据结构中的栈结构和循环来实现的,数据结构中的栈是占用内存中的堆空间的,而堆空间的大小相对于栈空间是大很多的,因此不会出现类似栈溢出的情况。
那么如何借助栈和循环来实现快速排序的非递归呢?
首先我们已经知道快速排序的递归思想是以基准值划分区间,再以两个区间进行递归,因此我们可以从区间的边界即left和right这两个变量来进行突破。
对此,我们首先将left和right入栈,注意先入right,再入left,这样栈顶元素将为left,方便出栈,当栈非空时,每次从栈中取出两个元素,即一个区间的左右边界,然后对该区间进行划分,直到区间中只剩下一个数。每次划分后,将划分的左右区间的边界入栈,这样循环下去就实现了快速排序。

//非递归版本
void QuickSort(int* a, int left, int right)
{
	Stack s;
	StackInit(&s);
	//先入right,这样先出的就是left
	StackPush(&s, right);
	StackPush(&s, left);
	while (!StackEmpty(&s))
	{
		int begin = StackTop(&s);
		StackPop(&s);
		int end = StackTop(&s);
		StackPop(&s);
		if (begin >= end)//只有一个数时无需划分
			continue;
		int keyi = PartSort1(a, begin, end);
		//此时数组被分为[begin, keyi - 1] keyi 以及[keyi + 1, end]三个部分
		//对于分出的左右两个区间
		//先入右区间的右边界end,再入右区间的左边界keyi + 1
		StackPush(&s, end);
		StackPush(&s, keyi + 1);
		//然后入左区间的右边界keyi - 1,再入左区间的左边界begin
		StackPush(&s, keyi - 1);
		StackPush(&s, begin);
	}
}

实际上快速排序的非递归实现和递归实现的本质思想都是一样的,即将区间划分,基准值左边的数都比基准值小,基准值右边的数都比基准值大,然后再对划分出的左右区间划分,这样将一个问题逐步划分成更小的问题,这便是分治思想。

小结

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值