“快速排序:一种美丽的算法混沌”

news2024/11/18 22:38:48

 欢迎来到我的博客!在今天的文章中,我将采用一种独特且直观的方式来探讨我们的主题:我会使用一幅图像来贯穿整篇文章的讲解。这幅精心设计的图表不仅是我们讨论的核心,也是一个视觉辅助工具,帮助你更深入地理解和掌握本文的内容。

通过这种方式,我们可以一步步深入本文的主题,每个阶段都将图像作为参考。这样不仅可以增加信息的吸收和理解,还能让学习过程更加生动和有趣。无论你是刚入门的新手还是寻求更深层次理解的老手,这幅图都将是你理解本文内容的有力工具。

所以,让我们开始这一独特的旅程,一边阅读文章,一边参考这幅将贯穿全文的图表,深入探索我们今天的话题!

想象一下,你走进了一个充满了成千上万本书的巨大图书馆。这些书籍横七竖八地堆放着,没有任何顺序。你需要找到一本特定的书,但在这混乱中,这看似是一个不可能完成的任务。这时,一位图书管理员出现了。这位管理员不仅知道每本书的具体位置,而且还能以令人难以置信的速度将这些书籍分类和组织起来,让你轻松地找到所需的书籍。这正是快速排序算法在数据世界中的角色:一个高效、精准的“信息管理员”,能迅速将数据整理成有序的结构。

快速排序,这个由英国计算机科学家Tony Hoare在1960年发明的算法,至今仍是计算机科学中最受欢迎和广泛使用的排序算法之一。它之所以名为“快速”,是因为它比其他传统排序算法(如冒泡排序或插入排序)要快得多,尤其是在处理大量数据时。快速排序的基本思想是选择一个元素作为“基准”,然后将数组分为两部分,一边是小于基准的元素,另一边是大于基准的元素,接着递归地在这两部分进行同样的操作。

在现代计算的世界里,快速排序不仅是一个算法的范例,它也是处理大规模数据不可或缺的工具。从数据库的查询优化到大数据分析,再到我们日常使用的搜索引擎,快速排序的影响无处不在。它的效率和优雅使它成为了计算机科学中的一个经典,一个在不断变化和发展的数字时代依然屹立不倒的算法巨人。

快速排序有四种方法。他们分别是“hoare版本”  “挖坑法” 和 “前后指针版本” 和“非递归模拟实现”

1.hoare版本

一张图片带你秒懂过程! 

 

快速排序的基本原理

快速排序是一种高效的排序算法,使用分而治之的策略来排序数据。以下是它的基本步骤:

  1. 选择基准值(Pivot)

    从数组中选择一个元素作为基准值。这个选择可以是随机的,通常选择第一个或最后一个元素。
  2. 分区(Partitioning)

    重新排列数组,使得所有小于基准值的元素都排在基准之前,而所有大于基准值的元素都排在基准之后。这个步骤结束后,基准元素就处于其最终位置。
  3. 递归排序子数组

    对基准左侧和右侧的子数组分别递归地执行快速排序。
  4. 终止条件

    当子数组的大小减至1或0时,递归结束,因为每个元素都已经被排序。

// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
 if(right - left <= 1)
 return;
 
 // 按照基准值对array数组的 [left, right)区间中的元素进行划分
 int div = partion(array, left, right);
 
 // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
 // 递归排[left, div)
 QuickSort(array, left, div);
 
 // 递归排[div+1, right)
 QuickSort(array, div+1, right);
}

上述为快速排序递归实现的主框架。

分区(Partitioning)

我们首先选择基准值将其定义为key  一般为最左或最右,此时,对其进行遍历,L向右找比key大的值,R向左找比key小的值,找到了就相互交换, 直到第一次相遇,将key与相遇点进行交换

 

 此时可能会有人疑问,为什么相遇就一定要交换,如果我们相遇后这个值比key大的情况下,是否违背我们要排序的初衷呢?  这里先给出答案:相遇位置一定比key小

 原因:

 如果左边先走就无法保证一定比key小 

 接下来我们实现初步代码,如何让他先进行左右交换并让key挪动到相遇位置

void QuickSort(int* a, int begin, int end)
{
	int keyi = begin;
	int right = end;
	int left = begin ;

	while (left < right)
	{
		//左边找大
		while(left < right && a[left] < a[keyi])//加入&&的原因是如果到相遇点时,left还是小于kei,他不会在相遇点停下,并且再次交换就颠倒顺序了
		{
			++left;
		}
		//右边找小
		while (left < right && a[right] < a[keyi])
		{
			--right;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	keyi = left;
	
}

 此时我们已经完成初步交换

 那我们是不是可以得出,如果接下来让左边变得有序,右边也有序。整体就有序了

 递归排序子数组

接下来我们对左右子数组进行递归处理,就可以得出一个有序数组

接下来我们先对左子树进行递归

 

 进行处理后,3就是key就是相遇处,接着再对左子数组进行处理递归处理直到只剩一个数组时返回

 接着再对右子树进行处理,以此类推,这里不多做赘述

终止条件

这里涉及到递归子分治问题,所以我们进行修改代码,将第一步的key也就是图中的6保留,keyi-1就是左边的数组个数,keyi+1就是右边的数组个数,我们进行递归,返回条件就是只有一个节点时返回。

void QuickSort(int* a, int begin, int end)
{

	if (begin >= end)
		return;
	int keyi = begin;
	int right = end;
	int left = begin+1 ;

	while (left < right)
	{
		//左边找大
		while(left < right && a[left] < a[keyi])//加入&&的原因是如果到相遇点时,left还是小于kei,他不会在相遇点停下,并且再次交换就颠倒顺序了
		{
			++left;
		}
		//右边找小
		while (left < right && a[right] < a[keyi])
		{
			--right;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	keyi = left;
	
	//左半  [begin , keyi-1]     keyi     [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

快速排序的优化

对以上代码进行测试,我们会发现在处理有序数组时,快速排序反而是最慢的排序

为什么有序的情况下快排会很吃亏呢? 因为我们采取了固定选Key 

理想状态下,快排时间复杂度是O(N*logN)

所以我们采取三数取中法 可以避开所有最坏的情况,越接近二分,就越快

三数取中法是快速排序算法中一种改进的基准(pivot)选择方法。它的目的是减少快速排序在最坏情况下的时间复杂度,这通常发生在选择的基准值是最小或最大元素时。通过选择一个更接近中间值的基准,可以提高分区的均匀性,从而使算法更加高效。

在传统的快速排序中,基准通常被选择为数组的第一个元素、最后一个元素或随机一个元素,这样在面对某些特定的数据集时(比如已经排序或接近排序的数组)可能会导致算法效率低下。

三数取中法是通过考虑数组的第一个元素、中间元素和最后一个元素,然后选择这三个元素的中位数作为基准。这种方法在实践中通常能给出一个较好的基准,从而使得分区更加平衡。

void QuickSort(int* a, int begin, int end)
{

	if (begin >= end)
		return;

	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);

	int keyi = begin;
	int right = end;
	int left = begin ;

	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}
		//左边找大
		while(left < right && a[left] <= a[keyi])//加入&&的原因是如果到相遇点时,left还是小于kei,他不会在相遇点停下,并且再次交换就颠倒顺序了
		{
			++left;
		}
	
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	keyi = left;
	
	//左半  [begin , keyi-1]     keyi     [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}
  1. begin 和 end: 这两个变量通常表示正在处理的数组或子数组的开始和结束索引。在数组的第一次排序时,begin 通常是 0(数组的第一个元素的索引),而 end 是数组的最后一个元素的索引。

  2. 计算中点: (begin + end) / 2 这个表达式的目的是找到 beginend 之间的中间索引。这是通过将 beginend 的值相加,然后除以 2 来实现的。由于这里使用的是整数除法,所以即使 begin + end 是奇数,结果也会被自动向下取整到最接近的整数。

  3. 数组索引: 在 C/C++ 中,数组的索引是从 0 开始的。因此,计算出的 midi 一定是一个有效的数组索引,只要 beginend 是有效的。这意味着 a[midi] 是数组中的一个实际元素。

总结来说,int midi = (begin + end) / 2; 确保 midi 是位于 beginend 之间的一个有效索引,而 a[midi] 则是数组在这个索引位置的元素。这是一种常见的找到数组中间元素索引的方法。

结论:Hoare版本快速排序的效率与优势

Hoare版本的快速排序算法是计算机科学中的一个经典例子,它展示了高效算法设计的重要性。通过其独特的分区策略,该算法能够快速、高效地处理各种大小的数据集。Hoare版本的快速排序在最佳情况下的时间复杂度为 O(n log n),并且即使在平均情况下也能维持这一高效表现。虽然它在最坏情况下的时间复杂度为 O(n^2),但通过智能选择枢轴点,如使用三数取中法或随机选取,可以大幅减少这一风险。

此外,Hoare版本相较于其他变体如Lomuto分区方法,通常需要更少的数据交换操作,这使得它在处理大数据集时更为高效。然而,它的实现相比一些其他版本更为复杂,可能需要更细致的理解和调试。

总的来说,Hoare版本的快速排序因其高效性和对大数据集友好的特性,在算法领域和实际应用中仍然占有一席之地。它不仅是排序算法的一个优秀代表,也是计算机算法设计和分析的一个经典案例。

2.“挖坑法”

 

什么是挖坑法?

挖坑法是快速排序的一种实现方式,它的核心思想是将数组分为两部分,使得一部分的所有元素都比另一部分的元素小。这个过程通过“挖坑”和“填坑”来实现,即在数组中创建一个“坑”,并用合适的元素填补它。

int PartSort2(int* a, int begin, int end)
{
	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);

	int key = a[begin];
	int hole = begin;

	while (begin < end)
	{
		//右边找小,找左边的坑
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[hole] = a[end];
		hole = end;
		// 左边找大,填到右边的坑
		while (begin < end && a[begin] <= key)
		{
			++begin;
		}

		a[hole] = a[begin];
		hole = begin;
	}
	a[hole] = key;
	return hole;
}void QuickSort2(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	if (end - begin + 1 <= 10)   //小区间优化
	{
		InsertSort(a + begin, end - begin + 1);  //a+begin  防止都是从左数组开始
	}
	int keyi = PartSort2(a, begin, end);
	QuickSort2(a, begin, keyi - 1);
	QuickSort2(a, keyi + 1, end);
}
挖坑法的优点
  • 效率高:通过选取合适的基准值,挖坑法能够有效减少不必要的数据交换,从而提高排序效率。
  • 易于理解:这种方法的逻辑清晰,易于理解和实现。
  • 适用范围广:适用于大多数需要排序的场景,尤其是数据量较大的情况。
结语

快速排序的挖坑法不仅效率高,且易于理解和实现。通过上述代码示例,我们可以清晰地看到其具体实现过程和方法。无论是学习算法还是解决实际问题,掌握这种排序方法都是非常有价值的。

 3.前后指针法

什么是前后指针法?

前后指针法是快速排序的一种实现技巧,它使用两个指针在数组中从前往后遍历,以此来重新排列元素,达到排序的目的。该方法通过将数组分成两部分,其中一部分的所有元素都比另一部分的元素小来实现排序。

int PartSort3(int* a, int begin, int end)
{

	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);


	int keyi = begin;
	int prev = begin;
     int cur = prev + 1;
	while (cur<=end)
	{
		if(a[cur] < a[keyi] && ++prev != cur)
		Swap(&a[prev], &a[cur]);
		
		++cur;
	}
	Swap(&a[keyi], &a[prev]);
	keyi = prev;
	return keyi;
}
void QuickSort3(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	if (end - begin + 1 <= 10)   //小区间优化
	{
		InsertSort(a + begin, end - begin + 1);  //a+begin  防止都是从左数组开始
	}
	int keyi = PartSort3(a, begin, end);
	QuickSort3(a, begin, keyi - 1);
	QuickSort3(a, keyi + 1, end);
}
前后指针法的优点
  • 高效:前后指针法减少了不必要的数据交换,提高了排序效率。
  • 易理解:该方法的逻辑清晰,易于理解和编写。
  • 广泛适用:适用于多种排序场景,特别是数据量较大的情况。
结语

快速排序的前后指针法提供了一个高效且易于理解的排序策略。通过上述的代码示例,我们可以清楚地看到其具体的实现方法。无论是在学术学习还是实际编程中,掌握这种排序技巧都将非常有益。

4非递归实现快速排序:栈的应用

快速排序是一种高效的排序算法,通常以递归的形式实现。然而,在某些情况下,递归可能导致栈溢出或额外的性能开销。为了解决这些问题,可以使用非递归的方式来实现快速排序,本文将通过一个具体的栈结构实现的示例来详细介绍这种方法。

为什么使用非递归快速排序?

非递归快速排序的主要优势在于它避免了递归带来的栈溢出风险,特别是在处理大数据集时。此外,非递归实现在某些情况下可能更加高效,因为它减少了函数调用的开销。

void QuickSortNonR(int* a, int begin, int end)
{
	ST s;
	STInit(&s);

	STPush(&s, end);
	STPush(&s, begin);

	while (!STEmpty(&s))
	{
		int left = STTop(&s);
		STPop(&s);
		int right = STTop(&s);
		STPop(&s);
		int keyi = PartSort3(a, left, right);
		//  [left, keyi-1] keyi  [keyi+1,right]
		if (left < keyi - 1)
		{
			STPush(&s, keyi - 1);
			STPush(&s, left);
		}
		if (keyi + 1 < right)
		{
			STPush(&s, right);
			STPush(&s, keyi+1);
		}
	}

	STDestory(&s);
}
非递归快速排序的优势
  • 避免栈溢出:在处理大数据集时,递归可能会导致栈溢出。非递归实现避免了这个问题。
  • 性能提升:在某些情况下,非递归实现由于减少了函数调用,可能会有更好的性能表现。
结语

非递归快速排序提供了一种有效的替代方案,特别是在处理大规模数据时。通过使用栈结构来模拟递归过程,我们不仅可以避免潜在的栈溢出问题,还可能获得性能上的提升。掌握这种方法,对于深入理解快速排序算法和处理大数据集具有重要意义。

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

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

相关文章

【idea】idea尾部自动删除空格,idea2023.1.2关闭自动去除行尾空格的功能

这个功能是由于git或者svn上的代码有许多空格的时候&#xff0c;会自动把空格去掉&#xff0c;就会导致出现许多更改的地方&#xff0c;会自动删空格。 尾部刚打好空格准备写代码&#xff0c;自动就删掉空格&#xff0c;又得重打空格后继续编码&#xff0c;非常不爽。 设置如…

linux sed批量修改替换文件中的内容/sed特殊字符

sed系列文章 linux常用命令(9)&#xff1a;sed命令(编辑/替换/删除文本)linux sed命令删除一行/多行_sed删除第一行/linux删除文件某一行linux sed批量修改替换文件中的内容/sed特殊字符 文章目录 sed系列文章一、sed替换文本语法1.1、基础语法1.2、高阶语法 二、实战/实例2.1…

群晖7.2使用Docker安装容器魔方结合内网穿透实现远程访问

最近&#xff0c;我发现了一个超级强大的人工智能学习网站。它以通俗易懂的方式呈现复杂的概念&#xff0c;而且内容风趣幽默。我觉得它对大家可能会有所帮助&#xff0c;所以我在此分享。点击这里跳转到网站。 文章目录 1. 拉取容器魔方镜像2. 运行容器魔方3. 本地访问容器魔…

SpringTask

SpringTask是一种用于定时任务调度的框架周期性任务、定时任务需要SpringTask框架 比较出名的框架有三种&#xff1a; &#xff08;1&#xff09;SpringTask(没有很大的并发量需求量&#xff0c;用SpringTask足够) &#xff08;2&#xff09;Quartz(老牌的定时任务&#xff0c…

在IDEA 2023.3中Jrebel debug 模式无法启动

目录 版本说明问题描述解决方式 版本说明 IDEA&#xff1a;IntelliJ IDEA 2023.3 (Ultimate Edition) Jrebel&#xff1a; JRebel Agent 2023.4.2 (202312041035) 问题描述 在IDEA中启动debug模式时无法正常启动 解决方式 在 IDEA 2023.3 版本修改了默认的配置&#xff…

仿淘宝、京东首页icons横向滑动效果

一、效果展示 淘宝&#xff1a; 京东&#xff1a; 二、话不多说&#xff0c;直接上demo 案例效果 代码 <template><div class"demo-page"><h1>滚动效果</h1><div classicons-slide-wrapper><div class"icons-container&quo…

k8s节点not ready

开发小伙伴反应&#xff0c;发布应用失败。检查后发现有个虚拟机挂掉了 启动后先重启服务&#xff1a;&#xff08;一般是自启动&#xff0c;自动拉起pod服务&#xff09; service docker restart docker ps |grep kube-apiserver|grep -v pause|awk ‘{print $1}’|xargs -i …

【SpringBoot】配置文件

配置文件官网 1. 配置方式 application.propertiesapplication.yml / application.yaml 2. 自定义配置信息 将实体类中的本应该写死的信息写在属性配置文件中。 可以使用 Value("${键名}") 获取&#xff0c;也可以使用 ConfigurationProperties(prefix"前…

C++_类的定义和使用

目录 1、类的引用 1.1 类的成员函数 1.2 类成员函数的声明和定义 2、类的定义 2.1 类的访问限定&#xff08;封装&#xff09; 3、类重名问题 4、类的实例化 4.1 类的大小 5、隐含的this指针 5.1 空指针问题 结语&#xff1a; 前言&#xff1a; C的类跟c语言中的结…

【Vue】日期格式化(全局)

系列文章 【Vue】vue增加导航标签 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/134965353 【Vue】Element开发笔记 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/133947977 【Vue】vue&#xff0c;在Windows IIS平台…

Tomcat的结构分析和请求处理原理解析

目录 Tomcat服务器&#xff1f;Tomcat结构处理请求流程Tomcat作用其他的web服务器 Tomcat服务器&#xff1f; 我们经常开口闭口“服务器”、“服务器”的&#xff0c;其实“服务器”是个很容易引发歧义的概念 其实&#xff0c;Tomcat服务器 Web服务器 Servlet/JSP容器&#…

java简易制作-王者荣耀游戏

一.准备工作 首先创建一个新的Java项目命名为“王者荣耀”&#xff0c;并在src下创建两个包分别命名为“com.sxt"、”com.stx.beast",在相应的包中创建所需的类。 创建一个名为“img”的文件夹来储存所需的图片素材。 二.代码呈现 package com.sxt; import javax…

人工智能联盟的首件神兵利器——“Purple Llama” 项目,旨为保护工智能模型安全性

Meta公司&#xff08;Meta Platform Inc&#xff09;&#xff0c;原名Facebook&#xff0c;创立于2004年2月4日&#xff0c;市值5321.71亿美元。总部位于美国加利福尼亚州门洛帕克。 Meta 公司推出了名为“Purple Llama”的项目&#xff0c;旨在保护和加固其开源人工智能模型。…

【MODBUS】Modbus的3种传输方式

概述 1979年&#xff0c;Modicon 首先推出了串行Modbus标准&#xff0c;后来由于网络的普及&#xff0c;需要更高的传输速度&#xff0c;1997年制定了基于TCP网络的Modbus标准。 所以总的可分为两个传输模式:基于串行链路的和基于以太网TCP/IP的。但是我个人还是习惯分为3种传…

【MySQL 索引】InooDB 索引实现

1 索引定义 维基百科对数据库索引的定义: 数据库索引是数据库管理系统&#xff08;DBMS&#xff09;中的一个排序数据结构, 以协助快速查询和更新数据库表中的数据。 MongoDB对索引的定义: 索引是一种特殊的数据结构, 以有序和便于遍历的形式存储数据集合中特定字段或一组字段…

蓝桥杯专题-真题版含答案-【制作表格】【5位黑洞数】【泊松分酒】【亲密数】

Unity3D特效百例案例项目实战源码Android-Unity实战问题汇总游戏脚本-辅助自动化Android控件全解手册再战Android系列Scratch编程案例软考全系列Unity3D学习专栏蓝桥系列ChatGPT和AIGC &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分…

UG NX二次开发(C++)-库缺少需要的入口点的原因与解决方案

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 1、前言2、“库缺少需要的入口点”错误展示3、可能出现的原因与解决方案3.1 对于采用CTRL+U方式调用3.2 对于menu菜单下调用1、前言 在UG NX二次开发过程中,有时会遇到形形色色的bug,比如有个读…

可视化监控云平台/智能监控EasyCVR如何使用脚本创建ramdisk挂载并在ramdisk中临时运行

视频云存储/安防监控EasyCVR视频汇聚平台基于云边端智能协同&#xff0c;支持海量视频的轻量化接入与汇聚、转码与处理、全网智能分发、视频集中存储等。安防管理视频平台EasyCVR拓展性强&#xff0c;视频能力丰富&#xff0c;具体可实现视频监控直播、视频轮播、视频录像、云存…

【Spring Boot】内网穿透实现远程调用调试

文章目录 1. 本地环境搭建1.1 环境参数1.2 搭建springboot服务项目 2. 内网穿透2.1 安装配置cpolar内网穿透2.1.1 windows系统2.1.2 linux系统 2.2 创建隧道映射本地端口2.3 测试公网地址 3. 固定公网地址3.1 保留一个二级子域名3.2 配置二级子域名3.2 测试使用固定公网地址 4.…

管理类联考——英语二——考点+记忆篇——大作文——表格

大作文Remarkablechangesconcerning主题词havebeenwitnessedinTheabovechartthoroughlyrevealedthefascinatingissue.Onecanseethat年份1,事物1wasonly数字1.ButwithinashortdurationofXyears,事物1bumpedto数字2in年份2.Apartfromthat,事物2hasenjoyedaslightdeclineduringthe…