【数据结构】第十三站:排序(中)快速排序

news2024/11/27 8:26:55

本文目录

  • 一、快速排序递归法
    • 1.快速排序思想
    • 2.hoare版本实现快速排序
    • 3.hoare版本的优化
      • 1>使用随机值rand()函数
      • 2>三数取中
      • 3>三路划分
    • 4.证明hoare版本的key在左边,必须让右边先走
    • 5.挖坑法实现快速排序
    • 6.将前面快排的一趟排序给提取出来
    • 7.双指针法实现快速排序
    • 8.快速排序小区间优化
  • 二、快速排序非递归

一、快速排序递归法

1.快速排序思想

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

2.hoare版本实现快速排序

hoare版本的快速排序的基本思路是,先选出最左边或者最右边的值为key,如果选左边,那就让右边先走,如果选右边,那就让左边先走。后面会说为什么必须这样走。我们假定选左边为key,那么当右边碰到比key小的时候,停下来。然后让左边走,当左边的找到一步比key大的时候,左边停下来,然后交换这两个值。
之后,我们将循环结束的位置称之为keyi,这样,我们交换key和keyi位置的数据。
然后这样我们会发现,我们已经将一个数据给放到他应该在的位置上了。
这样我们可以使用递归的思想,将左半区间和右半区间分别递归。
如此一来。也就成功实现了排序
hoare

//快速排序
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int keyi = left;
	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[keyi], &a[left]);
	QuickSort(a, begin, left - 1);
	QuickSort(a, left + 1, end);

}

如上代码所示,但是这种方法我们需要有几点注意事项
1:递归结束条件,不难发现,我们这样一直递归下去,总有一个区间只有一个元素,或者不存在元素。这样的情况就得返回了。由于我们是知道left和right的,一旦上一层递归中只有两个或者三个元素的时候,下一次递归传递的left和right一定是left>=right的。这就可以作为递归结束条件了。
2.右边找小和左边找大的时候,假如不添加left<right进行限定。那么一旦整个数组的其他值都大于key,或者小于key,有可能造成直接越界了。
3.我们所说的找小和找大是严格的小于,不能是等于,因为等于可以是在key的左边也可以是在key的右边,如果我们特殊处理等于的情况,有可能造成无法预知的后果。
4.如果选左边为key,那么必须从右边开始走,反之亦然。只有这样才能保证相遇位置一定是小于key或者大于key的一个数。后面会详细证明

时间复杂度分析
对于上面的快排,我们可以简单的分析一下时间复杂度。不难得知,他的递归方式类似于一颗二叉树。
不难发现,他最好情况一共有logN层,第一层有一个key,每一侧的key都是上一层的两倍。但是当数据量很大时候,key的数量对N的影响不大。所以每一层的数据量仍然是O(N)级别,而每一层我们其实都会遍历一遍的为了选出同key数量相等的数。所以每一层的时间复杂度为O(N)。所以总的时间复杂度为O(N*logN),而空间复杂度为O(logN),这是因为总共会开logN层栈帧。
快排时间复杂度分析但是上面的其实是最好的情况,对于快排而言,最坏的情况是数据量全部顺序,或者全部逆序的情况。在这种情况下,快排由于每次只能搞定一个元素,而这个元素恰好就是最边缘的元素,这样就需要开N层栈帧,每层都需要N-i次,类似于等差数列。时间复杂度变为了O(N2),空间复杂度变为了O(N)

3.hoare版本的优化

我们可以看到hoare版本的快速排序其实还是存在一些问题的,当数据顺序或逆序的时候,效率很低。反而乱序的时候效率很高。所以我们可以采用一些特殊方法来处理一下。

1>使用随机值rand()函数

我们发现,快速排序的问题就在于一旦出现顺序或者逆序就会导致效率很低,但是我们可以使用一种随机选key来处理一下。使得每一次选出的key都是随机值,具体做法为使用rand函数,随机生成一个下标,将这个下标与最左边的交换。这样就能确保每一次都是一个随机的下标。因为在大量数据面前,每次随机选数,恰好为顺序或逆序的概率为0。根据这种思想,代码为

//快速排序
void QuickSort1(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int keyi = left;
	int randi = left + rand() % (right - left);
	Swap(&a[randi], &a[left]);
	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[keyi], &a[left]);
	QuickSort1(a, begin, left - 1);
	QuickSort1(a, left + 1, end);

}

2>三数取中

三数取中的基本思想是这样的:将最左边,最右边,以及中间三个位置的元素的数据中,取出中间的数据,然后让数值为中间的数据的下标与最左边进行交换,而取中的逻辑可如下
三数取中>由于三数取中的优化,可以使得每一次的左值一定不是最大或者最小的。从而优化掉顺序或者逆序的情况

int GetMidNumi(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 right;
		}
		else
		{
			return left;
		}
	}
	else
	{
		if (a[left] < a[right])
		{
			return left;
		}
		else if (a[mid] < a[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
}
//快速排序:三数取中
void QuickSort2(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int keyi = left;
	
	int midi = GetMidNumi(a, left, right);
	Swap(&a[midi], &a[left]);
	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[keyi], &a[left]);
	QuickSort2(a, begin, left - 1);
	QuickSort2(a, left + 1, end);
}

3>三路划分

如果遇到大量相同的元素的话,那么前面的方法都不好使了。这时就引出了三路划分。三路划分后面具体详细讲解

4.证明hoare版本的key在左边,必须让右边先走

左边做key,必须右边先走,可以保证相遇位置一定小于key或者相遇位置就是key,从而使得key与相遇位置交换后,使得key落到他应该存在的位置。

1.R找到小,L找大没有找到,L遇到R,相遇位置一定是比key小的
在这里插入图片描述

2.R找小,没有找到,直接遇到L,要么就是一个比key小的,要么就是直接就是key在这里插入图片描述

3.类似道理,右边作key,左边就得先走

5.挖坑法实现快速排序

首先是挖坑法的基本思想,先将最左边的数保存在一共临时变量里面,然后让这个左下标设置为坑,假定坑内没有有效数据,然后右边先走,当遇到一个比临时变量小的数的时候,将这个值甩给坑,然后将坑重新设置为当前的right。然后左边走,找到一个比临时变量大的数据的时候,将这个数据甩给右边的坑。如此循环下去。当两个相遇的时候,我们在将临时变量的值赋给坑。就将一个数据排好了
挖坑法

//快速排序:挖坑法
void QuickSort3(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int hole = left;

	int midi = GetMidNumi(a, left, right);
	Swap(&a[midi], &a[left]);
	int key = a[left];
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;
		//左边找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	QuickSort3(a, begin, hole - 1);
	QuickSort3(a, hole + 1, end);
}

6.将前面快排的一趟排序给提取出来

这是为了方便我们后面写快排的非递归形式.

int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	int midi = GetMidNumi(a, left, right);
	Swap(&a[midi], &a[left]);
	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[keyi], &a[left]);
	return left;
}
int PartSort2(int* a, int left, int right)
{
	int hole = left;
	int midi = GetMidNumi(a, left, right);
	Swap(&a[midi], &a[left]);
	int key = a[left];
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;
		//左边找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	return hole;
}
//快速排序
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int keyi = PartSort2(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

7.双指针法实现快速排序

双指针法的基本思路如下:
1.仍然让最左边设置为key,然后让prev指针指向第一个元素,让cur指针指向第二个元素。
2.当cur所指向的小于key的时候,我们先让prev++,然后让prev所指向的与cur所指向的交换位置。然后让cur++。
3.当cur所指向的大于key的时候,我们直接让cur++即可。
4.最终当cur大于right的时候结束。此时prev恰好指向最后一个小于key的数,prev后面的全部数都是大于key的。
5.我们直接让key和prev进行交换。这样就将一趟给排好了
在这里插入图片描述

int PartSort3(int* a, int left, int right)
{
	int midi = GetMidNumi(a, left, right);
	Swap(&a[midi], &a[left]);
	int prev = left;
	int cur = left + 1;
	int key = a[left];
	while (cur <= right)
	{
		if (a[cur] > key)
		{
			cur++;
		}
		else
		{
			prev++;
			Swap(&a[prev], &a[cur]);
			cur++;
		}
	}
	Swap(&a[prev], &a[left]);
	return prev;
}
//快速排序
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

事实上,上面的代码还有一种更加优雅的写法

int PartSort3(int* a, int left, int right)
{
	int midi = GetMidNumi(a, left, right);
	Swap(&a[midi], &a[left]);
	int prev = left;
	int cur = left + 1;
	int key = a[left];
	while (cur <= right)
	{
		if (a[cur] < key && ++prev != cur)
			Swap(&a[cur], &a[prev]);
		cur++;
	}
	Swap(&a[prev], &a[left]);
	return prev;
}
//快速排序
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

8.快速排序小区间优化

由于我们的快速排序是使用递归的思想。类似于二叉树。我们可以发现,在最底层的递归中。每一个都是小区间,但是每一个小区间我们都需要使用很多次递归。而最后的几层占据了绝大多数递归。比如最后三层,占据了50%+25%+12.5%=87.5%的递归。这样的消耗确实挺大。我们可以在最后的小区间,让他不要使用递归了,直接使用插入排序来进行优化。因为小区间以及很接近有序了。使用插入排序最佳。这样可以极大的节约消耗。当然区间不可以太大,因为我们要考虑小区间直接插入的效率高于递归的效率,否则得不偿失

//快速排序
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	if ((right - left + 1) > 10)
	{
		int keyi = PartSort3(a, left, right);
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
	else
	{
		InsertSort(a + left, right - left + 1);
	}
}

二、快速排序非递归

上面我们已经详细分析了快速排序的递归版本。但是只要是递归就一定会存在某些问题:
1.效率
2.深度太深,栈溢出
这时候就需要我们将递归形式改为非递归形式。

递归改成非递归有两种情况
1:直接改递归,这比较适合一些简单的递归
2:间接改递归,使用栈辅助改递归

对于这个快排,我们直接改递归是比较困难的,我们可以使用间接的方式
递归其实就是不断建立栈帧的过程,而栈是可以模拟一个递归的过程的
我们先分析在递归下的快排,其实快排递归本质就是区间的变化
快排的本质
我们可以将他的区间下标给入栈,为了更好的模拟,我们需要使用右边的变量先入栈,然后再让左边的变量入栈。这样可以保证最终先出的是左边的,然后是右边的变量。我们每次入区间的时候,也是先让右区间入,再让左区间入,这样可以保证出的时候先出左区间。
然后就是我们不断取出区间,进行每一趟快排了,这也就是之前我们需要将一趟快排给提取出来的原因,为了方便我们改为非递归。后面就是不断入区间出区间的过程了

//快速排序非递归
void QuickSortNonR(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	Stack st;
	StackInit(&st);
	StackPush(&st, right);
	StackPush(&st, left);

	while (!StackEmpty(&st))
	{
		int begin = StackTop(&st);
		StackPop(&st);
		int end = StackTop(&st);
		StackPop(&st);
		int keyi = PartSort3(a, begin, end);

		
		// [begin,keyi-1] keyi [keyi+1,end]
		if (keyi + 1 < end)
		{
			StackPush(&st, end);
			StackPush(&st, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, begin);
		}
	}
	StackDestroy(&st);
}

———————————————————————————————————————
好了本期内容就到这里了
如果对你有帮助的话,不要忘记点赞加收藏哦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/471298.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java小技能:使用FeignClient

文章目录 引言I FeignClient1.1 定义FeignClient1.2 启用FeignClient1.3 使用FeignClient引言 一款不错的SpringCloud 脚手架项目:注册中心(nacos)+RPC(feign) https://blog.csdn.net/z929118967/article/details/127966912 RPC(feign):使用Feign的方式,进行微服务调…

AlgoC++第九课:手写AutoGrad

目录 手写AutoGrad前言1. 基本介绍1.1 计算图1.2 智能指针的引出 2. 示例代码2.1 Scale2.2 Multiply2.3 Pow 总结 手写AutoGrad 前言 手写AI推出的全新面向AI算法的C课程 Algo C&#xff0c;链接。记录下个人学习笔记&#xff0c;仅供自己参考。 本次课程主要是手写 AutoGrad …

逆向动态调试工具简介

常用逆向工具简介&#xff1a; 二进制尽管属于底层知识&#xff0c;但是还是离不开一些相应工具的使用&#xff0c;今天简单介绍一下常用的逆向工具OD以及他的替代品x96dbg&#xff0c;这种工具网上很多&#xff0c;也可以加群找老满&#xff08;184979281&#xff09;&#x…

java实现乘法的方法

我们都知道&#xff0c;乘法运算的核心思想就是两个数相乘&#xff0c;如果能将乘法运算转化成一个加数的运算&#xff0c;那么这个问题就很容易解决。比如我们要实现23的乘法&#xff0c;首先需要定义两个变量&#xff1a;2和3。我们将这两个变量定义为一个变量&#xff1a;2x…

如何利用Mybatis-Plus自动生成代码(超详细注解)

1、简介 MyBatis-Plus (opens new window)&#xff08;简称 MP&#xff09;是一个 MyBatis (opens new window)的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;为简化开发、提高效率而生。 特性 无侵入&#xff1a;只做增强不做改变&#xff0c;引入…

一例感染型病毒样本的分析

这个样本是会释放两个dll和一个驱动模块&#xff0c;通过感染USB设备中exe文件传播&#xff0c;会向C&C下载PE执行&#xff0c;通过rookit关闭常用的杀软&#xff0c;是一例典型的感染型病毒&#xff0c;有一定的学习价值。 原始样本 样本的基本信息 Verified: Unsigned …

免费无需魔法会语音聊天的ChatGPT

今天发现了一个很好的ChatGPT&#xff0c;可以语音聊天&#xff0c;而且免费无需魔法 角色目前包括夏洛克、雷电影等等&#xff0c;对话的声调完全模拟了原角色&#xff01; 目前只有英文和日语两种对话&#xff0c;我们可以文字输入或者语音输入&#xff0c;中文即可&#xff…

泰克Tektronix DPO5204B混合信号示波器

特征 带宽&#xff1a;2 GHz输入通道&#xff1a;4采样率&#xff1a;1 或 2 个通道上为 5 GS/s、10 GS/s记录长度&#xff1a;所有 4 个通道 25M&#xff0c;50M&#xff1a;1 或 2 个通道上升时间&#xff1a;175 皮秒MultiView zoom™ 记录长度高达 250 兆点>250,000 wf…

M序列测量幅频特性

M序列 M 序列是一种伪随机序列&#xff0c;具有很好的伪噪声特性&#xff0c;常用于信道噪声测试和保密通信。不过 M 序列还有一个用途&#xff0c;也就是本文所介绍的——通过 M 序列测量频率响应。在讨论这个问题之前&#xff0c;我们先介绍 M 序列的特征与生成方法。 M 序列…

活力二八:CRM助力销售管理再现“浓缩”新活力

活力28、沙市日化&#xff01; 央视段子手朱广权再次喊出这句口号时&#xff0c;迅速激活了人们心中对于曾经“日化一哥”的记忆。 作为市场占率曾超 70% 的家清品牌&#xff0c;活力二八业务始于1950年&#xff0c;前身为沙市油脂化工厂&#xff0c;伴随中国改革开放大潮&…

第十一章_SpringBoot集成Redis

总体概述 redisTemplate-jedis-lettuce-redission之间的的联系 1、redisTemplate是基于某个具体实现的再封装&#xff0c;比如说springBoot1.x时&#xff0c;具体实现是jedis&#xff1b;而到了springBoot2.x时&#xff0c;具体实现变成了lettuce。封装的好处就是隐藏了具体的…

大家都在用的视频音频提取器,免费用!

随着互联网的日益普及&#xff0c;人们可以通过多种方式获取和分享媒体内容&#xff0c;例如通过社交媒体、视频共享网站等。但是&#xff0c;在处理媒体文件时&#xff0c;提取其中的音频或视频仍然是一个挑战。这就是为什么越来越多的人都在使用免费的视频音频提取器。 这些…

Node框架 【Koa】介绍、安装以及使用

文章目录 &#x1f31f;前言&#x1f31f;介绍&#x1f31f;koa优势&#x1f31f;洋葱模型&#x1f31f;安装&#x1f31f;具体步骤&#xff1a;&#x1f31f;创建项目目录&#x1f31f;初始化项目&#x1f31f;进入目录,安装koa &#x1f31f;使用&#x1f31f;案例&#x1f3…

C++STL详解(十一)-- 位图(bitset)

文章目录 位图的介绍位图的引入位图的概念位图的应用 位图的使用位图的定义位图的成员函数位图运算符的使用 位图的模拟实现成员函数构造函数set reset testflip,size,countnone,any,all 位图应用题扩展位图模拟实现代码 位图的介绍 位图的引入 有一道面试题: 给40亿个不重复…

QFIELD-GIS工具 定位功能使用方法

一、 简介 定位是一款GIS APP重要功能&#xff0c;可以帮助我们快速在地图上找到现在所处的位置。结合地图我们就可以快速了解我们所处环境的情况。同时可以利用APP的信息标注采集功能采集当前位置的信息到数据库中。 下面我们来介绍【QFIELD-GIS】如何进行GPS定位、如何…

平衡二叉树的实现(包含旋转)

平衡二叉树是子啊二叉排序树的基础上建立的&#xff0c;他的概念就是这棵树中的任意节点的平衡因子都必须要大于1或是小于-1。平衡因子就是这个节点的左子树高度减右子树高度所得到的差。那么&#xff0c;它有什么优点呢&#xff1f;为什要在二叉排序树的基础上来建立这个平衡二…

语音芯片排行榜,为何唯创知音WT588F语音芯片如此受欢迎

随着智能家居、智能玩具、智能机器人等领域的快速发展&#xff0c;语音芯片逐渐成为智能硬件的重要组成部分。在众多语音芯片中&#xff0c;唯创知音WT588F语音芯片备受关注&#xff0c;成为市场上备受欢迎的产品。那么&#xff0c;WT588F语音芯片具备哪些功能&#xff0c;为何…

您的云,您做主:Google Distributed Cloud Hosted 全面可用

近日&#xff0c;谷歌宣布Google 分布式云(GDC) 托管的全面可用性来扩展该产品组合&#xff0c;它支持具有最严格要求的客户的需求&#xff0c;包括机密、受限和绝密数据。 GDC Hosted 包括部署、操作、扩展和保护完整托管云所需的硬件、软件、本地控制平面和操作工具。此外&…

【高危】MySQL Server拒绝服务漏洞(CVE-2023-21912)

漏洞描述 MySQL是Oracle开源的关系型数据库管理系统。 MySQL Server 受影响版本存在拒绝服务漏洞&#xff0c;攻击者者无需身份验证可发送连接数据包导致MySQL Server 崩溃拒绝服务。官方未公布相关细节&#xff0c;可能由于对客户端设置字符集的处理不当&#xff0c;当客户端…

关于倾斜摄影超大场景的三维模型轻量化中的数据质量优化方法浅析

关于倾斜摄影超大场景的三维模型轻量化中的数据质量优化方法浅析 倾斜摄影超大场景的三维模型轻量化处理需要兼顾数据大小和渲染性能&#xff0c;同时保证模型的准确性和真实感。为了提高轻量化质量&#xff0c;可以从以下方面入手&#xff1a; 1、选择合适的轻量化算法和参数…