【数据结构入门】排序算法之三路划分与非比较排序

news2024/12/29 10:05:58

文章目录

前言

一、三路划分优化

1.1. 基本思想

1.2. 实现步骤

1.3. 优点

1.4 代码实现

二、非比较排序

2.1 计数排序

2.1.1基本思想

2.1.2具体步骤

2.1.3算法特性

2.1.4算法实现

2.2 基数排序

2.2.1基本思想

2.2.2具体步骤

2.2.3 基数排序的方法

2.2.4算法特性

2.2.5算法实现

 2.2.6 应用场景

三、算法复杂度及稳定性分析

总结


前言

前文(【数据结构入门】排序算法之交换排序与归并排序)提到了,快速排序在遇到数据大量重复的时候算法效率会极大的降低,这是由于分治思想不能很好的发挥他的作用,那么本篇博客会根据这种情况,介绍三路划分法优化快速排序,解决这个问题。

非比较排序是一类不依赖于元素间直接比较大小的排序算法。非比较排序算法主要包括计数排序、桶排序和基数排序等。下面将会介绍计数排序和基数排序。

一、三路划分优化

三路划分是一种在排序算法中常用的技术,特别是在处理包含大量重复数据的数组时,它可以显著提高排序效率。这种技术通过选择一个关键字(或称为基准值),然后将数组中的数据分为三部分:小于关键字的、等于关键字的和大于关键字的。

1.1. 基本思想

在快速排序的基础上,三路划分将数组分为三个部分,即小于基准值的部分、等于基准值的部分和大于基准值的部分。这样做的目的是减少递归排序的次数,特别是当数组中存在大量重复数据时,可以显著提高排序效率。

1.2. 实现步骤

  1. 选择基准值:通常选择数组的第一个元素作为基准值,但也可以使用其他策略,如三数取中或随机数选取,以避免在某些特殊情况下(如数组已经有序)导致的性能退化。
  2. 设置指针:设置三个指针,分别指向数组的起始位置(left)、当前处理位置(cur,初始值为left+1)和结束位置(right)。
  3. 遍历数组:从cur开始遍历数组,根据当前元素与基准值的大小关系,执行以下操作:
    • 如果当前元素小于基准值,将其与left指向的元素交换,并将left和cur都向右移动一位。
    • 如果当前元素大于基准值,将其与right指向的元素交换,但只将right向左移动一位(因为交换过来的元素还需要与基准值比较)。
    • 如果当前元素等于基准值,只需将cur向右移动一位。
  4. 递归排序:当cur大于right时,遍历结束。此时,数组被划分为三个部分,分别对小于基准值的部分和大于基准值的部分进行递归排序。注意,等于基准值的部分已经就位,无需再次排序。

伪代码整理:   

1.a[cur] < key , 交换left 和 cur 数据, left++, cur++;

2.a[cur] == key, cur++;

3.a[cur] > key, 交换cur 和 right数据, right++;

4.cur > right 结束

 

1.3. 优点

  • 提高排序效率:特别是当数组中存在大量重复数据时,三路划分可以显著减少递归排序的次数,从而提高排序效率。
  • 减少内存交换:通过减少不必要的元素交换,可以降低排序过程中的内存开销。

1.4 代码实现

void QuickSortPlus(int* a, int left, int right)
{
	//递归结束条件
	if (left >= right)
	{
		return;
	}
	//三数取中
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
	{
		Swap(&a[left], &a[midi]);
	}

	//记录begin, end 位置
	int begin = left;
	int cur = left + 1;
	int end = right;

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

	while (cur <= right)
	{
		if (a[cur] < key)
		{
			Swap(&a[left], &a[cur]);
			left++;
			cur++;
		}
		else if (a[cur] > key)
		{
			Swap(&a[cur], &a[right]);
			right--;
		}
		else //a[cur] == key
		{
			cur++;
		}
	}

	//小区间优化 -- 小区间使用插入排序
	//这个数字不能太大,否则没有意义
	if ((end - begin + 1) > 10)
	{
		//[begin, left-1] [left, right] [right+1, end]

		QuickSort(a, begin, left - 1);
		QuickSort(a, right+1, end);
	}
	else
	{
		InsertSort(a + left, right - left + 1);
	}
}

二、非比较排序

这类算法通过利用数据的某些特性或构建特定的数据结构来实现排序,通常具有较高的时间效率,在某些特定情况下甚至可以达到线性时间复杂度。

2.1 计数排序

计数排序(Counting Sort)是一种非比较型整数排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

2.1.1基本思想

对于每一个输入的元素x,确定小于x的元素个数,这样即可把x直接放到它在最终排序数组中的位置。

2.1.2具体步骤

  1. 统计:统计数组中每个值为i的元素出现的次数,存入数组C的第i项。
  2. 累加:累加数组C中的值,C[i]现在包含实际位置为i的元素的个数。

最后,反向填充目标数组:将每个元素i放在新数组的第C[i]项,每放一个元素就将C[i]减去1。

2.1.3算法特性

  • 1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  • 2. 时间复杂度:O(MAX(N,范围))
  • 3. 空间复杂度:O(范围)
  • 4. 稳定性:稳定
  • 5. 

2.1.4算法实现

void CountSort(int* a, int n)
{
	//找范围
	int min = a[0], max = a[0];

	for (int i = 0; i < n; ++i)
	{
		if (a[i] < min)
		{
			min = a[i];
		}
		if (a[i] > max)
		{
			max = a[i];
		}
	}

	int range = max - min + 1;
	int* countA = (int*)calloc(range, sizeof(int));//初始化为0
	if (countA == NULL)
	{
		perror("malloc fail");
		return;
	}

	//计数
	for (int i = 0; i < n; i++)
	{
		countA[a[i]-min]++;
	}
	// 排序
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (countA[i]--)
		{
			a[j++] = min + i;
		}
	}
	free(countA);
	return;
}

2.2 基数排序

基数排序(Radix Sort)是一种非比较型整数排序算法,它的工作原理是按照数字的每一位来分配和收集元素。这种排序方式通常用于排序数字(尽管它也可以用于排序其他类型的数据,比如字符串)。

2.2.1基本思想

将所有待比较的数字统一为相同的位数长度,位数较短的数字前面补零。然后,从最低位开始,依次进行一次分配和收集,直至排序完成。

2.2.2具体步骤

2.2.3 基数排序的方法

基数排序可以采用两种方法进行排序:

  1. 最低位优先(Least Significant Digit first,简称LSD)法:先从最低位开始排序,再对更高位进行排序,依次重复,直到最高位排序完成。
  2. 最高位优先(Most Significant Digit first,简称MSD)法:先按最高位排序分组,同一组中记录,关键码相等,再对各组按次高位排序分成子组,之后,对后面的位数继续这样的排序分组,直到最低位。再将各组连接起来,便得到一个有序序列

2.2.4算法特性

  1. 稳定性:基数排序是一种稳定的排序算法,即具备相同值的元素,在排序后保持它们原有的相对顺序。
  2. 时间复杂度:基数排序的时间复杂度是O(nk),其中n是排序数组的长度,k是数字的最大位数。在某些情况下,其效率高于其他稳定性排序法。
  3. 空间复杂度:由于需要额外的空间来创建“桶”,其空间复杂度大概是O(n+k)

2.2.5算法实现

根据排序时先进后出的特点,使用队列来辅助排序。

int GetKey(int value, int k)
{
	int key = 0;
	while (k >= 0)
	{
		key = value % 10;
		value /= 10;
		--k;
	}
	return key;
}

void Distribute(int* a, int left, int right, int k, Queue* Qu[10])
{
	for (int i = left; i <= right; ++i)
	{
		int key = GetKey(a[i], k);
		//入队 ,写错了
		QueuePush(Qu[key], a[i]);		
	}
}

void Collect(int* a, Queue* Qu[10])
{
	int k = 0;
	for (int i = 0; i < RADIX; i++)
	{
		while (!QueueEmpty(Qu[i]))
		{
			a[k++] = QueueFront(Qu[i]);
			QueuePop(Qu[i]);
		}
	}
}

//基数排序,先进先出用队列
void RadixSort(int* a, int left, int right)
{
	Queue q0, q1, q2, q3, q4, q5, q6, q7, q8, q9;
	//初始化
	QueueInit(&q0);
	QueueInit(&q1);
	QueueInit(&q2);
	QueueInit(&q3);
	QueueInit(&q4);
	QueueInit(&q5);
	QueueInit(&q6);
	QueueInit(&q7);
	QueueInit(&q8);
	QueueInit(&q9);
	//建数组
	Queue* Qu[10] = {&q0 ,&q1, &q2, &q3, &q4, &q5, &q6, &q7, &q8, &q9 };

	for (int i = 0; i < K; i++)
	{
		//分发数据
		Distribute(a, left, right, i, Qu);
		//回收数据
		Collect(a, Qu);
	}

	QueueDestory(&q0);
	QueueDestory(&q1);
	QueueDestory(&q2);
	QueueDestory(&q3);
	QueueDestory(&q4);
	QueueDestory(&q5);
	QueueDestory(&q6);
	QueueDestory(&q7);
	QueueDestory(&q8);
	QueueDestory(&q9);
}

 2.2.6 应用场景

基数排序最初被设计用于整数排序,因为它们具有易于定义位的特征(例如,个位、十位、百位等)。然而,基数排序也可以扩展用于任何可以被分成较小部分的数据类型,并且这些部分可以被独立排序。例如,字符串可以看作由字符组成的序列,可以对字符串集合使用基数排序,例如按字典顺序排列单词。此外,定长字符串(如电话号码、日期等)也可以通过每个字符的ASCII值进行排序。

三、算法复杂度及稳定性分析

 

总结

计数排序的优点在于它是稳定的排序算法,并且当输入的数据范围k不是很大时,它的时间复杂度为O(n+k),其中n是数组的长度,k是输入的最大值。这使得它在某些特定情况下非常高效。然而,如果k很大,那么就需要大量的额外空间来存储计数数组。

总之,计数排序是一种在特定条件下非常高效的排序算法,但它并不适用于所有情况。

基数排序是一种高效的非比较型排序算法,特别适用于整数和字符串等可以分割成独立部分的数据类型。它通过按位分配和收集元素来实现排序,具有稳定性和较高的时间效率。然而,其性能也依赖于数据的分布和基数的选择。

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

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

相关文章

【高等代数笔记】线性空间(五-九)

3. 线性空间 主线任务&#xff1a;研究线性空间和它的子空间的结构 研究平面 π \pi π上向量共线与不共线的问题&#xff1a; c ⃗ \vec{c} c 与 a ⃗ ≠ 0 \vec{a}\ne\boldsymbol{0} a 0共线 c ⃗ λ a ⃗ ⇔ λ ∈ R ⇔ − λ a ⃗ 1 c ⃗ 0 ⃗ \vec{c}\lambda\vec{…

【白皮书下载】分布式功能安全的创新与突破

近日&#xff0c;Imagination 推出全新性能最高且具有高等级功能安全性的汽车 GPU IP——Imagination DXS GPU&#xff0c;并且是Imagination 第一款带有“分布式安全机制”的处理器。 下载白皮书&#xff0c;获取完整分布式安全机制解决方案 根据 ISO 26262 汽车安全完整性等级…

STL 源码剖析 | 第1章:概论

STL 是一套程序库 1、STL 概论 1、从子程序、程序、函数、类别&#xff0c;到函数库、类别库、各种组件&#xff0c;从结构化设计、模块化设计、面向对象设计&#xff0c;到模式的归纳整理 为的就是 复用性 的提升 复用性 必须建立在某种标准之上 —— 不论是 语言层次的标…

关于MATLAB计算3维图的向量夹角总是不正确的问题记录

文章目录 问题描述解决方法完整代码 问题描述 因为最近在做无人机的一个项目&#xff0c;所以需要画出无人机的轨迹&#xff0c;然后再提取特征值&#xff0c;我这里在计算夹角的时候发现为什么在视觉上明明看的是钝角但是实际计算出来却是锐角的角度。 如下图所示&#xff0c…

大觅网之环境部署(Environment Deployment of Da Mi Network)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

数据保护从现在开始:如何抵御 .[RestoreBackup@cock.li].SRC 勒索病毒

导言 勒索病毒是一种不断演变的网络威胁&#xff0c;.[RestoreBackupcock.li].SRC、[chewbaccacock.li].SRC勒索病毒便是其中一种新型的攻击手段。该病毒通过加密用户文件并要求支付赎金来恢复访问&#xff0c;给个人和企业带来了严重的安全风险和经济损失。本文91数据恢复将探…

uniapp使用uview2上传图片功能

官网地址Upload 上传 | uView 2.0 - 全面兼容 nvue 的 uni-app 生态框架 - uni-app UI 框架 前提&#xff0c;需要下载vuew2插件 <view class"upload"><view class"u-demo-block__content"><view class"u-page__upload-item"&…

进程状态的优先级

1.进程的状态&#xff08;所有系统&#xff09; 因为是对于所有系统的&#xff0c;所以描述会很抽象。 补充知识&#xff1a; 并行和并发 并行&#xff1a;多个进程再多个cpu下分别同时运行并发&#xff1a;多个进程在一个cpu下采取进程切换的方式&#xff0c;在一段时间内&…

fiddler抓包06_抓取https请求(chrome)

课程大纲 首次安装Fiddler&#xff0c;抓https请求&#xff0c;除打开抓包功能&#xff08;F12&#xff09;还需要&#xff1a; ① Fiddler开启https抓包 ② Fiddler导出证书&#xff1b; ③ 浏览器导入证书。 否则&#xff0c;无法访问https网站&#xff08;如下图&#xff0…

prometheus通过nginx-vts-exporter监控nginx

Prometheus监控nginx有两种方式。 一种是通过nginx-exporter监控&#xff0c;需要开启nginx_stub_status,主要是nginx自身的status信息&#xff0c;metrics数据相对较少&#xff1b; 另一种是使用nginx-vts-exporter监控&#xff0c;但是需要在编译nginx的时候添加nginx-module…

MyBatis 分批次执行(新增,修改,删除)

import com.google.common.collect.Lists;import java.util.Iterator; import java.util.List; import java.util.function.Consumer;/*** Description mybatis分批插入数据使用* Author WangKun* Date 2024/9/19 11:20* Version*/ public class MyBatisSqlUtils {/*** param d…

用户态缓存:高效数据交互与性能优化

目录 1. 用户态缓存区工作背景 1.1 为什么每条连接都需要读写缓存区 1.1.1 读缓存区&#xff08;Read Buffer&#xff09; 1.1.2 写缓存区&#xff08;Write Buffer&#xff09; 1.2 用户态缓存区的工作流程 1.3 用户态缓存区的重要性 2. UDP 和 TCP 的设计差异 2.1 UD…

神经网络 卷积层 参数共享

参数共享常用于神经网络卷积层中&#xff0c;共享的实际上就是说卷积核中的参数一直保持不变&#xff0c;如下所示就可以称为共享参数啦&#xff01;&#xff01;

C# 实时流转换为m3u8

主要通过FFmpeg 执行命令进行转换 FFmpeg 下载地址 命令行 ffmpeg -i "rtsp://your_rtsp_stream_address" -codec: copy -start_number 0 -hls_time 10 -hls_list_size 12 -f hls "output.m3u8"start_number 设置播放列表中最先播放的索引号&#xff0c;…

JVM基础篇学习笔记

【注&#xff1a;本文章为自学笔记&#xff0c;仅供学习使用。】 一、JVM简介 JVM是Java虚拟机的缩写&#xff0c;本质上是运行在计算机上面的程序&#xff0c;作用是运行Java字节码文件。 1.1 JVM的功能 Java如果不做优化&#xff0c;则性能不如C/C&#xff0c;因为后者会…

uv-ui组件的使用——自定义输入框的样式

一、官网的使用 二、自定义修改样式 我是在小程序中使用此组件 想要自定义修改样式的话&#xff0c;需要placeholderClass加上 placeholderStyle配合使用 tip1&#xff1a;单独使用placeholderClass&#xff0c;他只会第一次渲染时生效&#xff0c;输入文字再清除后就不生效…

Spring面试题合集

Spring 1.谈谈你对Spring的理解 首先Spring是一个轻量级的开源框架&#xff0c;为Java程序的开发提供了基础架构支持&#xff0c;简化了应用开发&#xff0c;让开发者专注于开发逻辑&#xff1b; 同时Spring是一个容器&#xff0c;它通过管理Bean的生命周期和依赖注入&#…

flask项目初始化

1、初始环境 python3.8 2、flask文档地址&#xff1a;https://flask.palletsprojects.com/en/latest/installation/#install-flask 3、初始化项目 $ mkdir myproject $ cd myproject $ python3 -m venv .venv $ . .venv/bin/activate $ pip install Flask4、打开项目mypr…

机器翻译之多头注意力(MultiAttentionn)在Seq2Seq的应用

目录 1.多头注意力&#xff08;MultiAttentionn&#xff09;的理念图 2.代码实现 2.1创建多头注意力函数 2.2验证上述封装的代码 2.3 创建 添加了Bahdanau的decoder 2.4训练 2.5预测 3.知识点个人理解 1.多头注意力&#xff08;MultiAttentionn&#xff09;的理念图…

云服务器使用

最近搭建一个内网穿透工具&#xff0c;推荐一个云服务器&#xff1a; 三丰台&#xff1a;https://www.sanfengyun.com/ 作为学生党这个服务器是免费的可以体验使用&#xff01;可以使用免费虚拟主机和云服务器&#xff0c;写一个申请的基本步骤方便大家构建 申请步骤&#x…