Java数据结构(十一)——归并排序、计数排序

news2024/9/20 9:13:25

文章目录

  • 归并排序
    • 算法介绍
    • 代码实现
    • 非递归实现
    • 复杂度和稳定性
  • 计数排序
    • 算法介绍
    • 代码实现
    • 复杂度和稳定性

归并排序

算法介绍

归并排序是一种分而治之的排序算法。基本思想是: 将一个数组分成两半,对每半部分递归地应用归并排序先进行分解,然后将排序好的两半合并在一起。

相比于快速排序,归并排序每次都是从中间位置将序列分解为两个子序列,这使得归并排序不会出现像快速排序那样的最坏时间复杂度。

分解成什么程度开始合并?

当某次递归分解得到的左右子区间都有序时进行合并。

如果归并之前左右子区间无序,怎么办?

我们可以从更小的不能再被分割的子区间(左右区间都是一个元素)开始归并。所以我们可以采用递归的方式来实现这样的一个思路。其思路类似于二叉树的后序遍历。

合并的思路是怎样的?

假设现在有一个数组,该数组的前半部分有序,后半部分有序(模拟归并排序的场景),将它们合并成一个有序的序列,思考在原数组进行合并吗?不行,会出现数据覆盖的情况,所以我们得需要一块额外的空间来将合并后的序列存放在这里,合并完毕后,需要将额外空间中的数据拷贝回原数组。

合并的思路:创建两个指针分别指向两个有序区间的开始位置,然后依次比较取小的放到新的数组里,当一个有序区间的所有元素全部放到新数组里后,再将另外一个有序区间剩下的所有元素放到新数组中。


图示:

在这里插入图片描述

归并排序属于易分难合的排序,上面演示的就是每次合并的思路。整体的归并排序可以使用下图表示:

在这里插入图片描述

代码实现

    public void mergeSort(int[] array, int left, int right) {
        //分组
        if(left >= right) {
            return;
        }
        int mid = (left + right) >> 1;
        mergeSort(array, left, mid);
        mergeSort(array, mid + 1, right);
        //合并
        int[] tmp = new int[right - left + 1];
        int begin1 = left;
        int end1 = mid;
        int begin2 = mid + 1;
        int end2 = right;
        int index = 0;
        while(begin1 <= end1 && begin2 <= end2) {
            if(array[begin1] < array[begin2]) {
                tmp[index++] = array[begin1++];
            }else {
                tmp[index++] = array[begin2++];
            }
        }
        while(begin1 <= end1) {
            tmp[index++] = array[begin1++];
        }
        while(begin2 <= end2) {
            tmp[index++] = array[begin2++];
        }

        //拷贝
        for(int i = 0; i < tmp.length; i++) {
            array[i+left] = tmp[i];
        }
  • 注意问题:拷贝时拷回原数组的位置,不一定是从0下标位置开始拷回。

非递归实现

怎么分解、合并?

最开始将每一个元素看作有序序列,即每次将两个元素个数(gap)为1的序列合并,合并后如下:

在这里插入图片描述

第一次合并后,每两个元素是一个元素个数(gap)为2的有序序列,继续合并:

在这里插入图片描述

确定leftmidright的规律:

在这里插入图片描述

  • left = i;
  • mid = left + gap - 1;
  • right = mid + gap;

以此类推:

在这里插入图片描述

  • 观察上图可以发现:按照上面推导的公式计算leftmidright,可能会导致 rightmid越界,这需要我们特殊处理,如果还是不理解,可以结合接下来的代码实现理解。

    public void mergeSortNoR(int[] array, int left, int right) {
        int gap = 1;
        while(gap < array.length) {
            for (int i = 0; i < array.length; i = i + 2 * gap) {
                //创建_left、_mid、_right三个变量方便理解,读者实现时可以优化掉。
                int _left = i;
                int _mid = _left + gap - 1;
                //_mid可能越界
                if(_mid >= array.length) {
                    _mid = array.length - 1;
                }
                int _right = _mid + gap;
                //_right可能越界
                if(_right >= array.length) {
                    _right = array.length - 1;
                }
                
                //合并
                int begin1 = _left;
                int end1 = _mid;
                int begin2 = _mid + 1;
                int end2 = _right;
                int index = 0;
                int[] tmp = new int[end2 - begin1 + 1];
                while(begin1 <= end1 && begin2 <= end2) {
                    if(array[begin1] < array[begin2]) {
                        tmp[index++] = array[begin1++];
                    }else {
                        tmp[index++] = array[begin2++];
                    }
                }
                while(begin1 <= end1) {
                    tmp[index++] = array[begin1++];
                }
                while(begin2 <= end2) {
                    tmp[index++] = array[begin2++];
                }

                //拷贝
                for(int j = 0; j < tmp.length; j++) {
                    array[j+_left] = tmp[j];
                }
            }
            gap *= 2;
        }
    }
  • 注意for循环的i变化规则i = i + 2 * gap,这样就能找到当前gap的下一组合并的开始位置,即left

复杂度和稳定性

时间复杂度O(N*log2N)

  • 最优情况:O(N*log2N)
  • 平均情况:O(N*log2N)
  • 最差情况:O(N*log2N)

归并排序的时间复杂度在所有情况下都是O(N*log2N),这是因为它总是将数组分成两半进行递归排序,然后将它们合并。合并操作的时间复杂度是线性的,即O(N),但由于这个过程需要递归地发生log2N次(因为每次数组大小减半),所以总的时间复杂度是O(N*log2N)

空间复杂度O(N)

归并排序在合并过程中需要额外的存储空间来存储临时数组,因此其空间复杂度是O(N)。在最坏的情况下,它需要与原始数组同样大小的额外空间来进行合并操作。

稳定性稳定

对于归并排序还有一些补充的知识:

归并排序又被称为外排序,表明了归并排序能用来对外存数据排序,如硬盘。一般的电脑的内存大小只有4~8G,如果这时候要对硬盘里的10个G的数据进行排序,我们只能依赖归并排序,假设这时候内存只能使用1G,思路是:

先将10个G的数据分为10块1G的数据,一块一块地置入内存(读文件),利用快速排序将10块1G的数据排好序,写入文件,然后利用归并的思想归并完所有的数据。

为什么这种情况只能使用归并排序?补充一点原因

文件的读和写只能依次进行读写,归并排序满足这样的特点。而快速排序需要分别从头和尾部向中间遍历,这样的思想与读写文件的固有特点相违背。


计数排序

算法介绍

计数排序与之前的排序算法不同,它是一种 非基于比较 的排序算法。它特别适用于待排序元素为整数且范围较小的情况,能够在这些情况下实现高效的排序。以下是计数排序的基本原理

计数排序通过统计每个元素出现的次数,然后利用这些次数信息将原始序列重新组合成有序序列。具体来说,它首先确定待排序元素的范围,然后创建一个计数数组(或称为桶),该数组的长度等于待排序元素的最大值加1。接下来,遍历待排序数组,统计每个元素出现的次数,并将这些次数存储在计数数组的相应位置上。最后,根据计数数组的信息,依次将元素放回原始数组中的正确位置,完成排序。

在这里插入图片描述

考虑:如果要排序的数组中的元素只出现了例如100、97、78、99这样的较大数,按照上面的思想,计数数组大小为最大值100+1 = 101,此时计数数组中0~77下标位置的空间全部都浪费了,为了减少空间浪费并提高排序效率,将计数数组大小的计算优化为:length = maxVal - minVal + 1

但这同时意味着优化前的填充计数数组的规则不适用了,新的填充规则为:count[array[i] - minVal]++,具体如下图:

在这里插入图片描述


代码实现

    public void countSort() {
        //寻找最值,确定范围
        int minVal = array[0];
        int maxVal = array[0];
        for(int i = 1; i < array.length; i++) {
            if(array[i] < minVal) {
                minVal = array[i];
            }
            if(array[i] > maxVal) {
                maxVal = array[i];
            }
        }
        int[] count = new int[maxVal - minVal + 1];
        //计数
        for (int i = 0; i < array.length; i++) {
            int tmp = array[i];
            count[tmp - minVal]++;
        }
        //填充
        int index = 0;
        for(int i = 0; i < count.length; i++) {
            while(count[i] > 0) {
                array[index] = i + minVal;
                index++;
                count[i]--;
            }
        }
    }

复杂度和稳定性

时间复杂度O(N+k)

其中N是输入数组的长度,k是输入数组中的最大值与最小值之差(即输入数组的范围)。

注意,如果k远小于N(即输入元素范围远小于元素数量),则时间复杂度接近线性O(N)。然而,如果k与N接近或更大,则时间复杂度可能会变得不那么理想。

空间复杂度O(k),其中k是输入数组的范围。这是因为计数排序需要一个大小为 k+1 的计数数组来存储每个元素的频率。

稳定性稳定,这是因为计数排序按照元素的值将它们放入输出数组的相应位置,而不会改变具有相同值的元素的相对顺序。


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

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

相关文章

数据权限的设计与实现系列9——前端筛选器组件Everright-filter集成框架开发2

功能实现 ‍ 规则转换为 SQL 片段‍ 规则解析 首先我们来构造一个典型的规则&#xff0c;包括两个条件组&#xff0c;每个组由两个条件组成&#xff0c;由且与或两种逻辑关系&#xff0c;如下图&#xff1a; 然后看看生成的规则&#xff0c;如下&#xff1a; {"filt…

spring中对于servlet API的封装---springWeb

目录 一.springweb概述 二.springweb的特点 三.springweb的运行流程 四.springweb组件 五.springweb的搭建 1.导包 2.配置 DispatcherServlet 3.开启 springweb 注解 4.处理器的搭建 六.springweb注解 七.springweb拦截器 1.拦截器概述 2.拦截器的实现 (1)添加 servelt api 依赖…

开源 AI 智能名片链动 2+1 模式 O2O 商城小程序在社群活动中的应用与时机选择

摘要&#xff1a;本文探讨了开源 AI 智能名片链动 21 模式 O2O 商城小程序在社群经济中的重要性&#xff0c;着重分析了如何借助该小程序适时举办大型活动以维持和引爆社群活跃度。通过对活动时机选择的研究&#xff0c;强调了针对社群用户量身定制活动时机的必要性&#xff0c…

基于python+django+vue的外卖管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于pythondjangovueMySQL的外…

C/C++笔记

C/CPP笔记 杂记 struct msg_train和typedef struct msg_train 大小不一样 cstdio和stdio #include <stdio.h>int main() {printf("Hello, World!\n");return 0; } #include <cstdio>int main() {std::printf("Hello, World!\n");return 0; } 命…

20个Python入门基础语法要点

今天&#xff0c;我们将聚焦于Python的20个基础语法要点&#xff0c;让你的编程之旅更加顺畅。 第一部分&#xff1a;环境搭建与基本概念 1. Hello, World! 你的第一行代码&#xff1a;这是编程旅程的传统起点。 这行代码告诉Python显示文本&#xff0c;print是关键函数&…

常耀斌:AI赋能企业数字化转型(清华社发行)

新书地址&#xff1a; 清华出版社&#xff1a;清华大学出版社-图书详情-《AI赋能企业数字化转型》 京东&#xff1a;《AI赋能企业数字化转型 常耀斌 清华大学出版社 9787302669081》【摘要 书评 试读】- 京东图书 内容简介&#xff1a; 在数字经济时代&#xff0c;企业发…

曲线图如何绘制美观,曲线图10种美化方法

曲线图是比较常用的图形&#xff0c;本文以二维曲线图为例&#xff0c;展示曲线的图的不同美化方法&#xff0c;如图1所示&#xff0c;是一个标准的曲线图&#xff0c;横坐标为x&#xff0c;纵坐标为y, 图1 标准曲线图 调整方法1 首先可以通过改变线的颜色&#xff0c;不同…

从零开始学PostgreSQL (十四):高级功能

目录 1. 简介 2. 视图 3. 外键 4. 事务 5. 窗口函数 6. 继承 7. 结论 简介 PostgreSQL是一个强大且开源的关系型数据库管理系统&#xff0c;以其稳定性、功能丰富性和对SQL标准的广泛支持而闻名。它不仅提供了传统的关系型数据库功能&#xff0c;如事务处理、外键约束和视图&am…

递归基础训练-路径总和

路径总和 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有节点值相加等于目标和 targetSum 。如果存在&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 我们可以把之前的…

【图虫创意-注册安全分析报告-无验证方式导致安全隐患】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 1. 暴力破解密码&#xff0c;造成用户信息泄露 2. 短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉 3. 带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造…

R语言统计分析——散点图2(散点图矩阵、高密度散点图)

参考资料&#xff1a;R语言实战【第2版】 1、散点图矩阵 pairs()函数可以创建基础的散点图矩阵。下面代码用于绘制一个散点图矩阵&#xff0c;包含mtcars数据集中的mpg、disp、drat和wt四个变量&#xff1a; pairs(~mpgdispdratwt,datamtcars,main"Basic Scatter Plot M…

输煤传送带异物识别检测数据集 yolo数据集 2400张

输煤传送带异物识别检测数据集 yolo数据集 2400张 输煤传送带异物识别检测数据集介绍 数据集名称 输煤传送带异物识别检测数据集&#xff08;Conveyor Belt Foreign Object Detection Dataset&#xff09; 数据集概述 该数据集专为输煤传送带上的异物识别检测设计&#xff0…

Unity携程Coroutine用法

一.携程概述 官方的解释是&#xff0c;携程允许你可以在多个帧中执行任务。在Unity中&#xff0c;携程是一个可以暂停并在后续帧中从暂停处继续执行的方法。 二.携程写法 下面示例使用携程和Update打印前5帧的时间间隔&#xff0c;展示了携程的基础写法 using System.Colle…

vmware + ubuntu + 初始配置(超级用户权限、vim安装、ssh登陆、共享文件夹、git)

1 VMware Ubuntu下载与安装 下载与安装 2 使用超级用户权限 &#xff08;1&#xff09;执行命令&#xff1a;sudo passwd root 然后在弹出的密码中输入密码即可&#xff0c;具体如下&#xff1a; 第一个密码是当前用户密码 后面两个是root用户密码 //推荐使用一个密码 3 vi…

SEMIDRIVE X9E Flash 调试要点

一、前言 客户采用芯驰 X9E 平台做的 T-BOX 产品&#xff0c;因为客户选用的 Flash 型号不在 SemiDrive_Memory 支持列表里面&#xff0c;出现机器能烧录不能启动的问题。接下来我们对这个问题进行调试。 二、SEMIDRIVE X9E Flash 调试要点 ① 客户的板子 Flash 型号为 GD25LQ…

43集 ESP32 编译调试出错的解决方法汇总

43集 ESP32 编译调试出错的解决方法汇总 1、提示找不到如下头文件&#xff0c;分别对应adf的component #include “esp_peripherals.h” esp_peripherals #include “audio_element.h” audio_pipeline #include “audio_common.h” audio_pipeline 这几个头文件都是esp-adf里…

【全网首发】2024华为OD机试 E卷D卷抽中题库清单(全真题库,持续更新)含考点说明

华为OD机试 2024E卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;E卷D卷A卷B卷C卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;私信哪吒&#xff0c;备注华为OD&#xff0c;加…

Git 原理(提交对象)(结合图与案例)

Git 原理&#xff08;提交对象&#xff09; 这一块主要讲述下 Git 的原理。 在进行提交操作时&#xff0c;Git 会保存一个提交对象&#xff08;commit object&#xff09;&#xff1a; 该提交对象会包含一个指向暂存内容快照的指针&#xff1b; 该提交对象还包含了作者的姓…

【MYSQL中数据库的约束以及表的设计】

MYSQL中数据库的约束和表的设计 一、数据库的约束1.1 NULL约束1.2 UNIQUE&#xff1a;唯一约束1.3 DEFAULT&#xff1a;默认值约束1.4 PRIMARY KEY&#xff1a;主键约束1.5 FOREIGN KEY &#xff1a;外键约束1.6 CHECK 约束 二、表的设计2.1 第一范式&#xff08;1NF&#xff0…