【深度剖析】 快速排序为什么不稳定?!

文章目录

  • 零、前言
  • 一、快速排序的步骤原理
  • 二、什么是稳定性?
  • 三、不稳定的地方在哪里?
  • 四、怎么让快速排序变得稳定?
    • 1、采用双指针的快速排序
      • A 思路简述
      • B 参考代码 :
      • C 算法分析
    • 2、基于递归的快速排序
      • A 思路简述
      • B 参考代码
      • C 算法分析
    • 3、采用归并算法的快速排序
      • C 算法分析
    • 4、写在后面



零、前言


最近做面试题中,遇到一些平时学习中比较少注意到的问题,记录下来以便后来者学习讨论。

请添加图片描述



一、快速排序的步骤原理


要了解快速排序为什么不稳定,还需要从它的步骤原理入手。
那么,快速排序的步骤是什么呢?

1、选取基准数字。
2、开始遍历,如果数字比基准大,则位置不变,如果比基准小,则将该值与前面第一个比基准大的值交换位置,如果前面没有比基准大的数字就不用替换了。
3、完成一边遍历后,交换头部的基准数字和最后一个比基准数字小的数字的位置,可以保证基准数字前面的值都小,后面的都大。
4、重复1-3的步骤。

快速排序 动图如下:




二、什么是稳定性?


既然要讨论快速排序为什么不稳定,那么也应该明确什么是稳定性!

通常,在排序算法中,稳定性 是指如果两个元素在原始数组中的相对顺序保持不变,则在排序后它们的相对顺序也应该保持不变。
换句话说,如果有两个相等的元素,它们的位置在排序之前是 a 和 b,且 a 在 b 的前面,那么在排序后,a 仍然应该在 b 的前面。




三、不稳定的地方在哪里?


1、选取基准数字上,一般选取 头部(或尾部),此时,如果 相等元素 可能被分到 不同子数组 中,从而 改变 它们的 相对顺序

举个例子

数组为 [5, 3, 2, 5, 1],在基准数字选择为 第一个 元素 时,第一次分割后,数组变为 [3, 2, 1, 5, 5],其中左边的子数组 [3, 2, 1] 中的元素都小于基准 5,右边的子数组 [5, 5] 中的元素都 不小于 基准 5。然后,递归地对左右两部分进行排序,最后得到排序后的数组 [1, 2, 3, 5, 5]。
.
例子中,结果是正确的,但是可以看到,原数组中的两个相等的元素 5 的相对位置在排序后发生了变化。原本,第一个出现的 5 在第二个出现的 5 的前面,但在排序后,第一个出现的 5 在第二个出现的 5 的后面。而相等元素的相对顺序应该保持不变,显然这就 违背稳定性定义



2、既然选择头部和尾部都不稳定,那如果选取基准数字是 随机选取 ,或者数组随机化,会不会也导致出现排序的不稳定? 答案是肯定的。

        假设基准数字是随机选择,此时它们的相对顺序是无法确定的。
        在快速排序每一次迭代中,基准数字将数组分割为两个子数组,其中一个子数组中的元素都小于基准,另一个子数组中的元素都大于基准。如果有相等的元素,那么它们可能会被随机选择的基准分配到不同的子数组中。
        这样,在每一次迭代中,相等元素的相对顺序可能会发生变化,因为它们被随机分配到不同的子数组。
        这样看来,基准数字的 随机选取 似乎是稳定的。
        然而,由于基准数字是随机选择的,每次排序的结果可能都会不同。在某些情况下,随机选择的基准可能会导致相等元素的相对顺序发生变化,而在其他情况下,它们的相对顺序可能会保持不变。
        因此,快速排序在基准数字随机选择的情况下,虽然能够提高平均性能可能是稳定的,但也可能是不稳定的。



3、若是使用 交换,一旦中间元被交换,与中间元相同的值与中间元的相对位置可能会被改变,除非新开O(n)空间。
而如果是 插入 则该算法稳定,不过数组的插入本身效率低。




四、怎么让快速排序变得稳定?


从前面的分析,我们不难找到,快速排序之所以不稳定是因为,相等元素的相对顺序发生变化。
要使得快速排序变得稳定,变要从此处切入。

那么便有以下思路:

  • (1) 保留相等元素的相对顺序 :
           在进行元素比较和交换时,只有在相邻元素的值不相等时才进行交换。这样可以确保相等元素的相对顺序保持不变。但这个方法可能会导致排序的性能降低,因为需要额外的比较操作。
  • (2) 使用稳定的排序算法进行子数组的排序
           在快速排序的过程中,当遇到子数组的大小较小时,可以切换到使用稳定的排序算法,如归并排序或插入排序,对该子数组进行排序。这样可以保证子数组的稳定性,并最终保持整个排序的稳定性。

1、采用双指针的快速排序


A 思路简述

基于快速排序的分治思想。选择数组中的一个元素作为枢轴(一般是第一个元素),然后将数组划分为两个子数组,其中一个子数组的元素都小于等于枢轴元素,另一个子数组的元素都大于枢轴元素。然后对这两个子数组分别递归调用快速排序算法,继续进行划分和排序。递归的基本情况是子数组的长度为1或0,即已经有序或为空数组,不需要再进行排序。通过不断划分和排序,最终实现整体的排序。在划分过程中,通过比较元素与枢轴的大小关系,将元素放置到正确的位置上,并维护相等元素的相对顺序。通过递归和划分的方式,快速排序算法能够高效地对一个数组进行排序


B 参考代码 :

// 划分过程
int partition(vector<int>& a, int l, int r) 
{
    int p = a[l]; // 选择枢轴
    int i = l + 1 , j = r;

    while (i <= j) 
    {
        if (a[i] <= p) 
            i++;
		else if (a[j] > p)
            j--;
        else 
            swap(a[i], a[j]); 	// 保留相等元素的相对顺序
    }
    swap(a[l], a[j]);

    return j;
}

void quick_Sort_01(vector<int>& a, int l, int r) 
{
    if (l  < r) 
    {
        int p = partition(a, l , r);
        quick_Sort_01(a, l , p - 1);	// 递归排序左侧子数组
        quick_Sort_01(a, p + 1, r);		// 递归排序右侧子数组
    }
}

C 算法分析


  • (1) 划分过程:
    划分过程使用了双指针法,通过比较元素与枢轴的大小关系,将元素分为两个子数组。在相等元素时,使用交换操作保持相等元素的相对顺序。
  • (2) 递归过程:
    使用递归来对划分得到的子数组进行快速排序,分别递归调用quickSort函数。对于左子数组,递归调用quick_Sort_01(a, l, p-1),对于右子数组,递归调用quick_Sort_01(a, p+1, r)。
  • (3) 返回结果
    该算法是对原始数组进行原地排序,只是对原数组进行交换操作实现的排序,不需要返回额外的结果。



2、基于递归的快速排序


在这里插入图片描述
如果对递归定义不清晰,请点击👉👉👉 【算法思想】啊,递"龟” 还是 递归?


A 思路简述

        选择一个枢轴元素,将原始数组划分为两个子数组,其中一个子数组的元素都小于枢轴元素,另一个子数组的元素都大于等于枢轴元素。然后对两个子数组分别递归调用该算法进行排序,最后合并两个排序好的子数组,得到最终的排序结果。这个过程不断递归,直到子数组的长度为1或0,即达到了基本情况。通过分治和递归的思想,这个算法能够将一个大问题拆解为多个小问题,最终实现整体的排序。


B 参考代码


vector<int> quick_Sort_02(vector<int>& arr) 
{
    if (a.size() <= 1) 								//(1)
        return a;
    
    vector<int> l, r;
    for (int i = 1; i < a.size(); i++) 				//(2)
    {
        if (a[i] < a[0]) 							//(3)
            l.push_back(a[i]);
        else 										//(4)
            r.push_back(a[i]);
    }
    
    l = quick_Sort_02(l), r = quick_Sort_01(r);		//(5)
    l.push_back(a[0]);								//(6)
    l.insert(l.end(), r.begin(), r.end());			//(7)
    
    return l;
}
  • (1) 如果数组大小为1或更小,则已经排序完成
  • (2) 根据第一个元素(枢轴)对数组进行划分
  • (3) 将小于枢轴的元素放入左侧向量
  • (4) 将不小于枢轴的元素放入右侧向量
  • (5) 递归地对左右向量进行排序
  • (6) 将枢轴元素追加到左向量末尾
  • (7) 将排序后的右向量追加到左向量后面
  • (8) 返回合并后的排序向量

C 算法分析

  1. 枢轴选择:
            传统的快速排序算法通常选择第一个或最后一个元素作为枢轴,并根据枢轴将数组划分为两个子数组。而这个方法将第一个元素作为枢轴,然后遍历剩余元素,将小于枢轴的元素放入左侧向量,大于枢轴的元素放入右侧向量。因此,枢轴元素不会在位置交换时移动。

  2. 循环方式:
            传统的快速排序算法使用递归来划分和排序子数组。这个方法使用循环来遍历数组元素,并根据枢轴将它们划分为左右两个子数组,分别递归调用quick_Sort_02函数。对于左子数组,递归调用quick_Sort_02(l),对于右子数组,递归调用quick_Sort_02®。

  3. 子问题处理:
            在递归调用时,这个方法使用类似快速排序的思想对左右子数组进行排序。然而,不同于原地排序的传统快速排序算法,这个方法每次递归都创建一个新的排序向量。

  4. 过递归调用MySort函数对子数组进行排序,并将排序好的左右子数组进行合并后返回最终的排序结果。



3、采用归并算法的快速排序


int partition(vector<int>& a, int l, int r) 
{
    int p = a[l]; // 选择枢轴
    int i = l + 1 , j = r;

    while (i <= j) 
    {
        if (a[i] <= p) 
            i++;
		else if (a[j] > p)
            j--;
        else 
            swap(a[i], a[j]); 	// 保留相等元素的相对顺序
    }
    swap(a[l], a[j]);

    return j;
}
void merge(vector<int>& a, int l, int mid, int r)  	//(1)
{
	int n1 = mid - l + 1;
	int n2 = r - mid;

	vector<int> left(n1), right(n2);

	
	for (int i = 0; i < n1; i++) 					// (2) 
    	left[i] = a[l + i];	
	for (int j = 0; j < n2; j++)					// (3) 
    	right[j] = a[mid + 1 + j];

	int i = 0, j = 0, k = l;

	while (i < n1 && j < n2) 						// (4)
	{
    	if (left[i] <= right[j]) 
    	{
        	a[k] = left[i];
        	i++;
    	} 
    	else 
    	{
        	a[k] = right[j];
        	j++;
    	}
   	 	k++;
	}

	while (i < n1) 									//(5)
	{
    	a[k] = left[i];
    	i++;
    	k++;
	}

	while (j < n2) 
	{
    	a[k] = right[j];
    	j++;
    	k++;
	}
}


void mergeSort(vector<int>& a, int l, int r) 		//(6)
{
	if (l < r) 
	{
		int mid = l + ((r - l) >> 1);
		mergeSort(a, l, mid); 						// (7)
		mergeSort(a, mid + 1, r); 					// (8)
		merge(a, l, mid, r); 						// (9)
	}
}

void quickSort(vector<int>& a, int l, int r, int jud) 
{
	if (l < r) 
	{
		if (r - l + 1 < jud) 
			mergeSort(a, l, r); 					// (10)
		else 
		{
			int p = partition(a, l, r);
			quickSort(a, l, p - 1, jud);  // (11)
			quickSort(a, p + 1, r, jud);  // (12)
		}
}
  • (1) 采用归并排序,merge()函数 用于将两个有序子数组 合并 为一个有序数组。它接收一个数组 arr,以及左边界 l,中间位置 mid 和右边界 r,将位于左边界到中间位置的子数组和中间位置到右边界的子数组进行合并。
  • (2) 将元素拷贝到临时数组 left 中去。
  • (3) 将元素拷贝到临时数组 right 中去。
  • (4) 合并临时数组的元素到原数组
  • (5) 将剩余元素拷贝到数组
  • (6) mergeSort() 函数使用归并排序进行子数组排序,用于递归地将数组划分为子数组,然后调用 merge 函数进行合并。它接收一个数组 arr,以及左边界 low 和右边界 high,在数组范围内进行递归划分和排序。
  • (7) 递归排序左侧子数组
  • (8) 递归排序左侧子数组
  • (9) 合并两个有序子数组
  • (10) 判断待排序子数组的长度是否小于阈值 threshold,如果小于阈值,则调用稳定的归并排序算法 mergeSort 对该子数组进行排序。这样可以保证在处理较小范围的子数组时,算法仍能达到较高的效率,并且保持排序的稳定性。
    如果大于等于阈值,则继续进行快速排序的操作。
  • (11) 递归排序左侧子数组
  • (12) 递归排序右侧子数组

C 算法分析


结合了快速排序和归并排序的思想。

快速排序部分:

选择一个枢轴元素,将数组划分为两个子数组,其中一个子数组的元素都小于等于枢轴元素,另一个子数组的元素都大于枢轴元素。
-对这两个子数组分别递归调用快速排序,继续进行划分和排序。
归并排序部分:

将数组不断划分为更小的子数组,直到每个子数组只包含一个元素。
然后逐步合并相邻的子数组,保证合并后的数组仍然有序。
具体过程如下:

在快速排序的递归过程中,检查子数组的长度是否小于阈值(threshold)。
如果小于阈值,就使用稳定的排序算法(这里使用归并排序)对子数组进行排序。
如果大于等于阈值,就继续使用快速排序算法进行划分和排序。
归并排序通过merge函数将有序的子数组合并,确保整体数组有序。


4、写在后面


        这两种方法的实现可能需要对快速排序算法进行修改,增加一些额外的判断和操作。需要注意的是,这些修改可能会增加算法的复杂性和额外的性能开销。因此,通常情况下,快速排序被选择的主要原因是其在平均情况下具有较好的性能,而不是为了实现排序的稳定性。

        如果对排序算法的稳定性有严格要求,可以直接选择使用稳定排序算法,如归并排序或插入排序。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【K8S系列】深入解析K8S调度

序言 做一件事并不难&#xff0c;难的是在于坚持。坚持一下也不难&#xff0c;难的是坚持到底。 文章标记颜色说明&#xff1a; 黄色&#xff1a;重要标题红色&#xff1a;用来标记结论绿色&#xff1a;用来标记论点蓝色&#xff1a;用来标记论点 Kubernetes (k8s) 是一个容器编…

使用docker部署rancher并导入k8s集群

前言&#xff1a;鉴于我已经部署了k8s集群&#xff0c;那就在部署rancher一台用于管理k8s&#xff0c;这是一台单独的虚拟环境&#xff0c;之前在k8s的master节点上进行部署并未成功&#xff0c;有可能端口冲突了&#xff0c;这个问题我并没有深究&#xff0c;如果非要通过修改…

C#使用Chart进行统计,切换不同的图表类型

WindowsForm应用程序中Chart图表控件所属的命名空间&#xff1a; Chart 命名空间&#xff1a; System.Windows.Forms.DataVisualization.Charting 对应的dll路径&#xff1a; C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\Syst…

COT、COT-SC、TOT 大预言模型思考方式||底层逻辑:prompt设定

先讲一下具体缩写的意思 COT-chain of thoughts COT-SC (Self-consistency) Tree of thoughts:Deliberate problem solving with LLM 我理解其实不复杂 1. 最简单的是&#xff1a;直接大白话问一次 &#xff08;IO&#xff09; 2. 进阶一点是&#xff1a;思维链&#xff0c;…

PDF转CAD后尺寸如何保持一致?这几种方法可以尝试一下

CAD文件是可编辑的&#xff0c;可以进行修改、添加和删除&#xff0c;这使得在CAD软件中进行编辑更加容易和灵活。这意味着&#xff0c;如果需要对图纸进行修改或者添加新的元素&#xff0c;可以直接在CAD软件中进行操作&#xff0c;而不需要重新制作整个图纸。那么将PDF文件转…

Linux嵌入式项目-智能家居

一、资料下载 二、框架知识 三、MQTT通信协议 1、上位机APP主要工作 1.wait for msg / while(1)订阅等待消息 2.处理消息 客户端创建了两个线程&#xff0c;一个线程用于发布消息&#xff0c;一个线程用于监听订阅消息 &#xff08;那我的仿真系统也可以啊&#xff0c;一个…

DVDNET A FAST NETWORK FOR DEEP VIDEO DENOISING

DVDNET: A FAST NETWORK FOR DEEP VIDEO DENOISING https://ieeexplore.ieee.org/document/8803136 摘要 现有的最先进视频去噪算法是基于补丁的方法&#xff0c;以往的基于NN的算在其性能上无法与其媲美。但是本文提出NN的视频去噪算法性能要好&#xff1a; 其相比于基于补丁…

Oracle通过函数调用dblink同步表数据方案(全量/增量)

创建对应的包&#xff0c;以方便触发调用 /*包声明*/ CREATE OR REPLACE PACKAGE yjb.pkg_scene_job AS /*创建同步任务*/FUNCTION F_SYNC_DRUG_STOCK RETURN NUMBER;/*同步*/PROCEDURE PRC_SYNC_DRUG_STOCK(RUNJOB VARCHAR2) ; END pkg_scene_job; /*包体*/ CREATE OR REPL…

深入理解netfilter和iptables

目录 Netfilter的设计与实现 内核数据包处理流 netfilter钩子 钩子触发点 NF_HOOK宏与Netfilter裁定 回调函数与优先级 iptables 内核空间模块 xt_table的初始化 ipt_do_table() 复杂度与更新延时 用户态的表&#xff0c;链与规则 conntrack Netfilter(结合iptable…

100种思维模型之安全边际思维模型-92

安全边际&#xff0c; 简而言之即距离某一件糟糕的事件发生&#xff0c;还有多大的空间&#xff0c;安全边际越高&#xff0c;我们就越安全&#xff01; 安全边际思维模型一个 让生活变得更从容 的 思维模型。 01、何谓安全边际思维模型 一、安全边际思维 安全边际 源于…

ACL 2023 | 持续进化中的语言基础模型

尽管如今的 AI 模型已经具备了理解自然语言的能力&#xff0c;但科研人员并没有停止对模型的不断改善和理论探索。自然语言处理&#xff08;NLP&#xff09;领域的技术始终在快速变化和发展当中&#xff0c;酝酿着新的潮流和突破。 NLP 领域的顶级学术会议国际计算语言学年会 …

声网 Agora音视频uniapp插件跑通详解

一、前言 在使用声网SDK做音视频会议开发时, 通过声网官方论坛 了解到,声网是提供uniapp插件的,只是在官方文档中不是很容易找到。 插件地址如下: Agora音视频插件 Agora音视频插件(JS) 本文讲解如何跑通演示示例 二、跑通Demo 2.1 环境安装: 参考: 2. 通过vue-…

vue3+element+sortablejs实现table表格 行列动态拖拽

vue3elementsortablejs实现table动态拖拽 1.第一步我们要安装sortablejs依赖2.在我们需要的组件中引入3.完整代码4.效果 1.第一步我们要安装sortablejs依赖 去博客设置页面&#xff0c;选择一款你喜欢的代码片高亮样式&#xff0c;下面展示同样高亮的 代码片. npm install so…

巩固一下NodeJs

1、初始化(确保当前电脑有node环境) npm init 2、安装express npm i expressnpm i ws文件结构 3、编写相关代码启动node服务(server.js) //导入下列模块&#xff0c;express搭建服务器&#xff0c;fs用来操作文件、ws用来实现webscoket const express require("expr…

Android 使用webView打开网页可以实现自动播放音频

使用webview 自动播放音视频&#xff0c;场景如&#xff0c;流媒体自动部分&#xff0c;音视频通话等。会出现如下问题&#xff1a; 解决方案如下&#xff1a; 配置webview 如下&#xff0c;这样可以自动播放音频。 webView.getSettings().setMediaPlaybackRequiresUserGestur…

原生JS实现图片裁剪功能

功能介绍&#xff1a;图片通过原生input上传&#xff0c;使用canvas进行图片裁剪。 裁剪框限制不允许超出图片范围&#xff0c;图片限制了最大宽高&#xff08;自行修改要的尺寸&#xff09;&#xff0c;点击确认获取新的base64图片数据 注&#xff1a;fixed布局不适用该方案&…

在vue中点击弹框给弹框中的表格绑值

场景描述&#xff1a;如下图所示&#xff0c;我们需要点击 ‘账单生成’ 按钮&#xff0c;然后里边要展示一个下图这样的表格。 最主要的是如何展示表格中的内容&#xff0c;一起看看吧&#xff01; <template><!-- 水费 欠费--><el-dialog title"水费欠费…

短视频seo矩阵源码开发与实践分享

在短视频矩阵系统源码开发中&#xff0c;需要注意以下几个细节&#xff1a; 1. 确定系统的功能需求&#xff1a;在开发短视频矩阵系统源码时&#xff0c;必须先明确系统的功能需求&#xff0c;包括用户的基本操作、系统数据的生成和处理等。 2. 定义数据库结构&#xff1a;短…

零售数字化转型如何破局?这篇文章全说清了!

“数字化转型”&#xff0c;一个老生常谈的话题。自19世纪互联网崭露头角&#xff0c;亚马逊和eBay等电商平台崛起&#xff0c;引领电子商务的发展。传统零售业开始意识到在线渠道的重要性&#xff0c;并纷纷推出自己的电子商务网站&#xff0c;从自此进入数字化转型的赛道当中…

利用电价运行策略研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…