排序算法:计数排序

        前文说到,19591959 年 77 月,希尔排序通过交换非相邻元素,打破了O(n^2)的魔咒,使得排序算法的时间复杂度降到了O(nlogn) 级,此后的快速排序、堆排序都是基于这样的思想,所以他们的时间复杂度都是 O(nlogn)。

        那么,排序算法最好的时间复杂度就是 O(nlogn) 吗?是否有比 O(nlogn) 级还要快的排序算法呢?能否在O(n^2)的时间复杂度下完成排序呢?

        事实上,O(n) 级的排序算法存在已久,但他们只能用于特定的场景。

        计数排序就是一种时间复杂度为 O(n) 的排序算法,该算法于 1954 年由 Harold H. Seward 提出。在对一定范围内的整数排序时,它的复杂度为 O(n+k)(其中 k 是整数的范围大小)。

伪计数排序

        举个例子,我们需要对一列数组排序,这个数组中每个元素都是[1,9] 区间内的整数。那么我们可以构建一个长度为 9 的数组用于计数,计数数组的下标分别对应区间内的 9 个整数。然后遍历待排序的数组,将区间内每个整数出现的次数统计到计数数组中对应下标的位置。最后遍历计数数组,将每个元素输出,输出的次数就是对应位置记录的次数。

算法实现如下(以 [1,9]为例 ):

public static void countingSort9(int[] arr) {// 建立长度为 9 的数组,下标 0~8 对应数字 1~9int[] counting = new int[9];// 遍历 arr 中的每个元素for (int element : arr) {// 将每个整数出现的次数统计到计数数组中对应下标的位置counting[element - 1]++;}int index = 0;// 遍历计数数组,将每个元素输出for (int i = 0; i < 9; i++) {// 输出的次数就是对应位置记录的次数while (counting[i] != 0) {arr[index++] = i + 1;counting[i]--;}}
}

        算法非常简单,但这里的排序算法 并不是 真正的计数排序。因为现在的实现有一个非常大的弊端:排序完成后,arr 中记录的元素已经不再是最开始的那个元素了,他们只是值相等,但却不是同一个对象。

        在纯数字排序中,这个弊端或许看起来无伤大雅,但在实际工作中,这样的排序算法几乎无法使用。因为被排序的对象往往都会携带其他的属性,但这份算法将被排序对象的其他属性都丢失了。

        就好比业务部门要求我们将 1 号商品,2 号商品,3 号商品,4 号商品按照价格排序,它们的价格分别为 8 元、6 元,6 元,9 元。 我们告诉业务部门:排序完成后价格为 6 元、 6 元、8 元,9元,但不知道这些价格对应哪个商品。这显然是不可接受的。

伪计数排序 2.0

        对于这个问题,我们很容易想到一种解决方案:在统计元素出现的次数时,同时把真实的元素保存到列表中,输出时,从列表中取真实的元素。算法实现如下:

public static void countingSort9(int[] arr) {// 建立长度为 9 的数组,下标 0~8 对应数字 1~9int[] counting = new int[9];// 记录每个下标中包含的真实元素,使用队列可以保证排序的稳定性HashMap<Integer, Queue<Integer>> records = new HashMap<>();// 遍历 arr 中的每个元素for (int element : arr) {// 将每个整数出现的次数统计到计数数组中对应下标的位置counting[element - 1]++;if (!records.containsKey(element - 1)) {records.put(element - 1, new LinkedList<>());}records.get(element - 1).add(element);}int index = 0;// 遍历计数数组,将每个元素输出for (int i = 0; i < 9; i++) {// 输出的次数就是对应位置记录的次数while (counting[i] != 0) {// 输出记录的真实元素arr[index++] = records.get(i).remove();counting[i]--;}}
}

        在这份代码中,我们通过队列来保存真实的元素,计数完成后,将队列中真实的元素赋到 arr 列表中,这就解决了信息丢失的问题,并且使用队列还可以保证排序算法的稳定性。

但是,这也不是 真正的计数排序,计数排序中使用了一种更巧妙的方法解决这个问题。

真正的计数排序

        举个例子,班上有 10 名同学:他们的考试成绩分别是:7,8,9,7,6,7,6,8,6,6,他们需要按照成绩从低到高坐到 0~9 共 10 个位置上。
用计数排序完成这一过程需要以下几步:

  • 第一步仍然是计数,统计出:4 名同学考了 6 分,3 名同学考了 7 分,2 名同学考了 8 分,1 名同学考了 9 分;
  • 然后从头遍历数组:第一名同学考了 77 分,共有 44 个人比他分数低,所以第一名同学坐在 44 号位置(也就是第 55 个位置);
  • 第二名同学考了 88 分,共有 77 个人(44 + 33)比他分数低,所以第二名同学坐在 77 号位置;
  • 第三名同学考了 99 分,共有 99 个人(44 + 33 + 22)比他分数低,所以第三名同学坐在 99 号位置;
  • 第四名同学考了 7 分,共有 4 个人比他分数低,并且之前已经有一名考了 7 分的同学坐在了 4 号位置,所以第四名同学坐在 5 号位置。
  • ...依次完成整个排序。

        区别就在于计数排序并不是把计数数组的下标直接作为结果输出,而是通过计数的结果,计算出每个元素在排序完成后的位置,然后将元素赋值到对应位置。

代码如下:

public static void countingSort9(int[] arr) {// 建立长度为 9 的数组,下标 0~8 对应数字 1~9int[] counting = new int[9];// 遍历 arr 中的每个元素for (int element : arr) {// 将每个整数出现的次数统计到计数数组中对应下标的位置counting[element - 1]++;}// 记录前面比自己小的数字的总数int preCounts = 0;for (int i = 0; i < counting.length; i++) {int temp = counting[i];// 将 counting 计算成当前数字在结果中的起始下标位置。位置 = 前面比自己小的数字的总数。counting[i] = preCounts;// 当前的数字比下一个数字小,累计到 preCounts 中preCounts += temp;}int[] result = new int[arr.length];for (int element : arr) {// counting[element - 1] 表示此元素在结果数组中的下标int index = counting[element - 1];result[index] = element;// 更新 counting[element - 1],指向此元素的下一个下标counting[element - 1]++;}// 将结果赋值回 arrfor (int i = 0; i < arr.length; i++) {arr[i] = result[i];}
}

首先我们将每位元素出现的次数记录到 counting 数组中。

然后将 counting[i] 更新为数字 i 在最终排序结果中的起始下标位置。这个位置等于前面比自己小的数字的总数。

例如本例中,考 7 分的同学前面有 4 个比自己分数低的同学,所以 7 对应的下标为 4。

这一步除了使用 temp 变量这种写法以外,还可以通过多做一次减法省去 temp 变量:

// 记录前面比自己小的数字的总数
int preCounts = 0;
for (int i = 0; i < counting.length; i++) {// 当前的数字比下一个数字小,累计到 preCounts 中preCounts += counting[i];// 将 counting 计算成当前数字在结果中的起始下标位置。位置 = 前面比自己小的数字的总数。counting[i] = preCounts - counting[i];
}

接下来从头访问 arr 数组,根据 counting 中计算出的下标位置,将 arr 的每个元素直接放到最终位置上,然后更新 counting 中的下标位置。这一步中的 index 变量也是可以省略的。

最后将 result 数组赋值回 arr,完成排序。

这就是计数排序的思想,我们还剩下最后一步,那就是根据 arr 中的数字范围计算出计数数组的长度。使得计数排序不仅仅适用于 
[1,9],代码如下:

public static void countingSort(int[] arr) {// 判空及防止数组越界if (arr == null || arr.length <= 1) return;// 找到最大值,最小值int max = arr[0];int min = arr[0];for (int i = 1; i < arr.length; i++) {if (arr[i] > max) max = arr[i];else if (arr[i] < min) min = arr[i];}// 确定计数范围int range = max - min + 1;// 建立长度为 range 的数组,下标 0~range-1 对应数字 min~maxint[] counting = new int[range];// 遍历 arr 中的每个元素for (int element : arr) {// 将每个整数出现的次数统计到计数数组中对应下标的位置,这里需要将每个元素减去 min,才能映射到 0~range-1 范围内counting[element - min]++;}// 记录前面比自己小的数字的总数int preCounts = 0;for (int i = 0; i < range; i++) {// 当前的数字比下一个数字小,累计到 preCounts 中preCounts += counting[i];// 将 counting 计算成当前数字在结果中的起始下标位置。位置 = 前面比自己小的数字的总数。counting[i] = preCounts - counting[i];}int[] result = new int[arr.length];for (int element : arr) {// counting[element - min] 表示此元素在结果数组中的下标result[counting[element - min]] = element;// 更新 counting[element - min],指向此元素的下一个下标counting[element - min]++;}// 将结果赋值回 arrfor (int i = 0; i < arr.length; i++) {arr[i] = result[i];}
}

这就是完整的计数排序算法。

倒序遍历的计数排序

计数排序还有一种写法,在计算元素在最终结果数组中的下标位置这一步,不是计算初始下标位置,而是计算最后一个下标位置。最后倒序遍历 arr 数组,逐个将 arr 中的元素放到最终位置上。

代码如下:

public static void countingSort(int[] arr) {// 防止数组越界if (arr == null || arr.length <= 1) return;// 找到最大值,最小值int max = arr[0];int min = arr[0];for (int i = 1; i < arr.length; i++) {if (arr[i] > max) max = arr[i];else if (arr[i] < min) min = arr[i];}// 确定计数范围int range = max - min + 1;// 建立长度为 range 的数组,下标 0~range-1 对应数字 min~maxint[] counting = new int[range];// 遍历 arr 中的每个元素for (int element : arr) {// 将每个整数出现的次数统计到计数数组中对应下标的位置,这里需要将每个元素减去 min,才能映射到 0~range-1 范围内counting[element - min]++;}// 每个元素在结果数组中的最后一个下标位置 = 前面比自己小的数字的总数 + 自己的数量 - 1。我们将 counting[0] 先减去 1,后续 counting 直接累加即可counting[0]--;for (int i = 1; i < range; i++) {// 将 counting 计算成当前数字在结果中的最后一个下标位置。位置 = 前面比自己小的数字的总数 + 自己的数量 - 1// 由于 counting[0] 已经减了 1,所以后续的减 1 可以省略。counting[i] += counting[i - 1];}int[] result = new int[arr.length];// 从后往前遍历数组,通过 counting 中记录的下标位置,将 arr 中的元素放到 result 数组中for (int i = arr.length - 1; i >= 0; i--) {// counting[arr[i] - min] 表示此元素在结果数组中的下标result[counting[arr[i] - min]] = arr[i];// 更新 counting[arr[i] - min],指向此元素的前一个下标counting[arr[i] - min]--;}// 将结果赋值回 arrfor (int i = 0; i < arr.length; i++) {arr[i] = result[i];}
}

两种算法的核心思想是一致的,并且都是稳定的。第一种写法理解起来简单一些,第二种写法在性能上更好一些。

在计算下标位置时,不仅计算量更少,还省去了 preCounts 这个变量。在《算法导论》一书中,便是采用的此种写法。

实际上,这个算法最后不通过倒序遍历也能得到正确的排序结果,但这里只有通过倒序遍历的方式,才能保证计数排序的稳定性。

时间复杂度 & 空间复杂度

从计数排序的实现代码中,可以看到,每次遍历都是进行 n 次或者 k 次,所以计数排序的时间复杂度为 O(n+k),k 表示数据的范围大小。

用到的空间主要是长度为 k 的计数数组和长度为 n 的结果数组,所以空间复杂度也是 O(n+k)。

需要注意的是,一般我们分析时间复杂度和空间复杂度时,常数项都是忽略不计的。但计数排序的常数项可能非常大,以至于我们无法忽略。不知你是否注意到计数排序的一个非常大的隐患,比如我们想要对这个数组排序:

int[] arr = new int[]{1, Integer.MAX_VALUE};

尽管它只包含两个元素,但数据范围是 [1,2^31],我们知道 java 中 int 占 4 个字节,一个长度为 2^31次方的 int 数组大约会占 8G 的空间。如果使用计数排序,仅仅排序这两个元素,声明计数数组就会占用超大的内存,甚至导致 OutOfMemory 异常。

所以计数排序只适用于数据范围不大的场景。例如对考试成绩排序就非常适合计数排序,如果需要排序的数字中存在一位小数,可以将所有数字乘以 10,再去计算最终的下标位置。

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

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

相关文章

6.2.3 【MySQL】InnoDB的B+树索引的注意事项

6.2.3.1 根页面万年不动窝 B 树的形成过程是这样的&#xff1a; 每当为某个表创建一个 B 树索引&#xff08;聚簇索引不是人为创建的&#xff0c;默认就有&#xff09;的时候&#xff0c;都会为这个索引创建一个 根节点 页面。最开始表中没有数据的时候&#xff0c;每个 B 树…

动手学深度学习——Windows下的环境安装流程(一步一步安装,图文并配)

目录 环境安装官网步骤图文版安装Miniconda下载包含本书全部代码的压缩包使用conda创建虚拟&#xff08;运行&#xff09;环境使用conda创建虚拟环境并安装本书需要的软件激活之前创建的环境打开Jupyter记事本 环境安装 文章参考来源&#xff1a;http://t.csdn.cn/tu8V8 官网…

使用Spring Security保障你的Web应用安全

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

Vim的基础操作

前言 本文将向您介绍关于vim的基础操作 基础操作 在讲配置之前&#xff0c;我们可以新建一个文件 .vimrc&#xff0c;并用vim打开在里面输入set nu 先给界面加上行数&#xff0c;然后shift &#xff1b;输入wq退出 默认打开&#xff1a;命令模式 在命令模式中&#xff1a…

MySQL阅读网上MySQL文章有感的杂记

前言&#xff0c;本篇文章将会记录各大MySQL文章的一些有意思的内容摘取&#xff0c;以及一些问题的提问&#xff0c;并且持续更新。 并且MySQL专栏将会记录MySQL常考的场景题等实战。 问题归类&#xff1a; 1.MySQL从加锁范围上分为哪三类? 2.全局锁加锁方法的执行命令是什么…

【配电变电站的最佳位置和容量】基于遗传算法的最优配电变电站放置(Matlab代码实现)

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

数据结构之美:如何优化内存和性能

文章目录 什么是数据结构&#xff1f;内存优化使用紧凑的数据类型避免冗余存储使用位运算压缩数据 性能优化使用适当的数据结构减少不必要的复制使用合适的算法 数据结构优化的案例分析结论 &#x1f389;欢迎来到数据结构学习专栏~探索数据结构之美&#xff1a;如何优化内存和…

Fiddler抓http数据

目录 参考博客 一、Fiddler配置二、分析Http请求1. Http消息结构简介1.1 Request请求消息1.2 Response响应消息 2. 分析Get接口2.1 请求示例2.2 查看Get请求2.3 查看Get响应 3 分析Post接口 参考博客 一、Fiddler配置 首先需要对Fiddler抓取Https请求进行相关配置&#xff1a…

11-集合和学生管理系统

1.ArrayList 集合和数组的优势对比&#xff1a; 长度可变添加数据的时候不需要考虑索引&#xff0c;默认将数据添加到末尾 1.1 ArrayList类概述 什么是集合 ​ 提供一种存储空间可变的存储模型&#xff0c;存储的数据容量可以发生改变 ArrayList集合的特点 ​ 长度可以变化…

计算机视觉面试题整理

1、介绍目标检测网络yolo系列以及ssd系列的原理&#xff0c;yolo对小目标检测不好的原因&#xff0c;除了缩小anchor外还可以如何改善&#xff1f; Yolo目标检测&#xff1a;YOLO是一种实时目标检测算法&#xff0c;其核心思想是将目标检测问题归为一个回归问题&#xff0c;直…

git clone报错Failed to connect to github.com port 443 after 21055 ms:

git 设置代理端口号 git config --global http.proxy http://127.0.0.1:10085 和 git config --global https.proxy http://127.0.0.1:10085 然后就可以成功git clone hugging face的数据集了 如果是https://huggingface.co/datasets/shibing624/medical/tree/main 那么…

SqlServer在尝试加载程序集 ID 65917 时 Microsoft .NET Framework 出错。服务器可能资源不足,或者不信任该程序集

问题&#xff1a;在尝试加载程序集 ID 65917 时 Microsoft .NET Framework 出错。服务器可能资源不足&#xff0c;或者不信任该程序集&#xff0c;因为它的 PERMISSION_SET 设置为 EXTERNAL_ACCESS 或 UNSAFE。 检查数据库属性&#xff1a;检查服务器是否信任该程序集 解决方法…

Prometheus+Grafana可视化监控【Nginx状态】

文章目录 一、安装Docker二、安装Nginx(Docker容器方式)三、安装Prometheus四、安装Grafana五、Pronetheus和Grafana相关联六、安装nginx_exporter七、Grafana添加Nginx监控模板 一、安装Docker 注意&#xff1a;我这里使用之前写好脚本进行安装Docker&#xff0c;如果已经有D…

自动化测试、压力测试、持续集成

因为项目的原因&#xff0c;前段时间研究并使用了 SoapUI 测试工具进行自测开发的 api。下面将研究的成果展示给大家&#xff0c;希望对需要的人有所帮助。 SoapUI 是什么&#xff1f; SoapUI 是一个开源测试工具&#xff0c;通过 soap/http 来检查、调用、实现 Web Service 的…

Spring 篇

1、什么是 Spring&#xff1f; Spring是一个轻量级的IOC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架&#xff0c;目的是用于简化企业应用程序的开发&#xff0c;它使得开发者只需要关心业务需求。常见的配置方式有三种&#xff1a;基于XML的配置、基于注解的配置…

Dockerfile打包nginx镜像

Dockerfile&#xff1a; FROM nginxENV WORK_DIR /project ENV GATEWAY_IP127.0.0.1USER root RUN mkdir ${WORK_DIR}#拷贝前端项目 ADD chinaunicom-digitward-portal-web-view.tar.gz ${WORK_DIR} ADD mdt-view.tar.gz ${WORK_DIR} ADD unicom-cloud-medical-admin-view.tar…

Python 基本语法

相关内容 代码行 单行代码 每行代码结尾不需要加标点 a 123多行换行 多行代码&#xff0c;直接换行 a 123 b a 1复杂过长的计算、操作可用括号然后缩进换行 income (gross_wages taxable_interest (dividends - qualified_dividends)- ira_deduction- student_loan_…

Python Union联合类型注解

视频版教程 Python3零基础7天入门实战视频教程 我们看下如下的示例&#xff1a; my_list2: list[int] [1, 2, 3, 4] my_dict2: dict[str, float] {"python222": 3.14, "java1234": 4.35} l1 [1, "python222", True] # 如何注解多种元素类型…

高速DSP系统设计参考指南(二)传输线(TL)效应

&#xff08;二&#xff09;传输线&#xff08;TL&#xff09;效应 1. 概述2. 传输线理论3. 并行终端仿真4. 传输线的目标阻抗5. TL仿真和实验结果对比5.1 无负载或源端接的传输线5.2 传输线源端串联 6. 接地网络对传输线的影响 1. 概述 传输线(TL)效应是高速 DSP 系统中噪声问…

mysql组合索引详解

组合索引 什么是组合索引组合索引具体使用组合索引注意事项 什么是组合索引 索引是一种数据结构&#xff0c;用于优化数据库查询速度&#xff0c;而组合索引指的是在 数据库中使用多个字段来创建一个索引。这种类型的索引允许数据库 更快地检索和排序数据&#xff0c;特别是…