归并排序和计数排序讲解

在这里插入图片描述
在这里插入图片描述

.

个人主页:晓风飞
专栏:数据结构|Linux|C语言
路漫漫其修远兮,吾将上下而求索


文章目录

  • 前言
  • 归并排序(递归)
    • 动图:
    • 代码实现
    • 以下是代码详细讲解:
  • 归并排序非递归
    • 代码实现
    • 以下是代码详细讲解:
  • 计数排序
    • 代码实现
    • 以下是代码详细讲解:
  • 时间复杂度和空间复杂度
  • 完整代码
  • 总结


前言

本文将深入介绍归并排序和计数排序这两种经典的排序算法,分别从递归和非递归的角度,以及对于整数排序的特殊情况,展开讨论它们的原理和实现。


归并排序(递归)

动图:

在这里插入图片描述

归并排序是一种分治算法,它将一个数组分为两个子数组,分别对这两个子数组进行排序,然后合并这两个有序子数组,得到一个完全有序的数组。以下是递归实现的归并排序的C代码:

代码实现

void _MergeSort(int* a, int begin, int end, int* tmp)// 归并排序实现(递归)
{
    // 基准情况:如果开始位置大于等于结束位置,表示当前数组已经有序或为空,直接返回
    if (begin >= end)
        return;

    // 计算中间位置 mid
    int mid = (begin + end) / 2;

    // 递归对左半部分进行归并排序
    _MergeSort(a, begin, mid, tmp);
    // 递归对右半部分进行归并排序
    _MergeSort(a, mid + 1, end, tmp);

    // 合并两个有序数组,将结果保存在临时数组 tmp 中
    int begin1 = begin, end1 = mid;
    int begin2 = mid + 1, end2 = end;
    int i = begin;

    // 比较左右两个部分的元素,将较小的元素放入 tmp 数组
    while (begin1 <= end1 && begin2 <= end2)
    {
        if (a[begin1] < a[begin2])
        {
            tmp[i++] = a[begin1++];
        }
        else
        {
            tmp[i++] = a[begin2++];
        }
    }

    // 处理剩余的元素,如果左半部分还有剩余,将其复制到 tmp 中
    while (begin1 <= end1)
    {
        tmp[i++] = a[begin1++];
    }

    // 如果右半部分还有剩余,将其复制到 tmp 中
    while (begin2 <= end2)
    {
        tmp[i++] = a[begin2++];
    }

    // 将排序好的部分复制回原数组 a
    memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

void MergeSort(int* a, int n)// 归并排序
{
    // 为临时数组 tmp 分配内存
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL)
    {
        perror("malloc fail");
        return;
    }

    // 调用递归排序函数
    _MergeSort(a, 0, n - 1, tmp);

    // 释放临时数组内存
    free(tmp);
}

以下是代码详细讲解:

基准情况检查: 如果开始位置 begin 大于等于结束位置 end,表示当前数组已经有序或为空,直接返回,作为递归的终止条件。

计算中间位置 mid: 根据开始位置和结束位置计算中间位置。

递归排序左右部分: 递归调用 _MergeSort 函数,对左半部分和右半部分进行归并排序。

合并两个有序数组: 创建两个索引 begin1/end1begin2/end2 分别表示左半部分和右半部分的范围。创建一个索引 i 表示当前合并位置。通过比较左右两部分的元素,将较小的元素放入临时数组 tmp 中,然后将剩余的元素复制到 tmp 中。

复制回原数组: 将排序好的部分复制回原数组 a

释放临时数组内存: 最终释放用于合并的临时数组的内存。

通过递归地将数组划分为更小的部分,然后合并有序部分,最终完成整个数组的排序。这有助于理解每个步骤的具体实现。


归并排序非递归

为了避免递归调用的开销,我们还提供了归并排序的非递归版本。这种实现方式使用迭代的方式进行归并,通过不断扩大合并的范围,最终完成整个数组的排序。

代码实现

void MergeSortNonR(int* a, int n)// 归并排序(非递归)
{
    // 为临时数组 tmp 分配内存
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }

    // 初始 gap 为 1,不断扩大 gap 直到数组全部有序
    int gap = 1;
    while (gap < n)
    {
        // 对数组进行多轮归并
        for (int i = 0; i < n; i += 2 * gap)
        {
            // 确定左右两部分的起始和结束位置
            int begin1 = i;
            int end1 = i + gap - 1;
            int begin2 = i + gap;
            int end2 = i + 2 * gap - 1;

            // 处理边界情况,防止数组越界
            if (end1 >= n || begin2 >= n)
                break;
            if (end2 >= n)
                end2 = n - 1;

            // 合并两部分有序数组,将结果保存在临时数组 tmp 中
            int j = begin1;
            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] < a[begin2])
                {
                    tmp[j++] = a[begin1++];
                }
                else
                {
                    tmp[j++] = a[begin2++];
                }
            }

            // 处理剩余的元素,如果左半部分还有剩余,将其复制到 tmp 中
            while (begin1 <= end1)
            {
                tmp[j++] = a[begin1++];
            }

            // 如果右半部分还有剩余,将其复制到 tmp 中
            while (begin2 <= end2)
            {
                tmp[j++] = a[begin2++];
            }

            // 将排序好的部分复制回原数组 a
            memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
        }

        // 每轮归并后,将 gap 扩大为原来的两倍
        gap *= 2;
    }

    // 释放临时数组内存
    free(tmp);
}

以下是代码详细讲解:

为临时数组分配内存: 创建一个临时数组 tmp 用于在归并过程中存储中间结果。

初始化 gap: 初始时,gap 设置为 1。

外层循环(gap 循环): 外层循环通过不断扩大 gap 的大小,直到 gap 大于等于数组长度 n。

内层循环(归并循环): 内层循环对整个数组进行多轮归并。每一轮归并根据当前的 gap 对数组进行分段,然后对相邻的两个段进行归并。

确定左右两部分的起始和结束位置: 根据当前的 gap,确定左半部分 [begin1, end1] 和右半部分 [begin2, end2] 的范围。

处理边界情况: 防止数组越界,检查左右两部分的结束位置是否超出数组长度,进行相应的调整。

归并两部分: 类似递归版本的归并排序,合并两部分有序数组,并将结果存储在临时数组 tmp 中。

复制回原数组: 将排序好的部分复制回原数组 a。

扩大 gap: 每一轮归并后,将 gap 扩大为原来的两倍。

释放临时数组内存: 最终释放用于归并的临时数组的内存。

这个非递归版本的归并排序通过迭代的方式实现了归并过程,避免了递归调用的开销。


计数排序

计数排序是一种非比较性的排序算法,适用于一定范围内的整数排序。以下是计数排序的C代码:

代码实现

void CountSort(int* a, int n)//计数排序
{
    // 寻找数组中的最小值和最大值
    int min = a[0];
    int max = a[0];
    for (int i = 0; i < n; i++)
    {
        if (a[i] > max)
        {
            max = a[i];
        }
        if (a[i] < min)
        {
            min = a[i];
        }
    }

    // 计算数值范围
    int range = max - min + 1;

    // 为计数数组分配内存,并初始化为 0
    int* count = (int*)calloc(range, sizeof(int));
    if (count == NULL)
    {
        printf("calloc fail\n");
        return;
    }

    // 统计每个元素出现的次数
    for (int i = 0; i < n; i++)
    {
        count[a[i] - min]++;
    }

    // 根据计数数组重建原数组
    int i = 0;
    for (int j = 0; j < range; j++)
    {
        while (count[j]--)
        {
            a[i++] = j + min;
        }
    }

    // 释放计数数组内存
    free(count);
}

计数排序适用于元素范围相对较小的情况,具有线性时间复杂度,是一种高效的排序算法。

以下是代码详细讲解:

寻找最小值和最大值: 遍历数组,找到其中的最小值和最大值,以确定数值范围。

计算数值范围: 计算数组中元素的范围,即 max - min + 1

分配计数数组内存: 使用 calloc 分配一个计数数组 count,其大小为数值范围,初始值全部为 0。

统计每个元素的出现次数: 遍历数组,对于每个元素,通过 a[i] - min 将其映射到计数数组中的位置,并将对应位置的计数值加一。

根据计数数组重建原数组: 遍历计数数组,根据计数值的多少,将元素的值从小到大重建到原数组 a 中。

释放计数数组内存: 释放用于存储计数数组的内存。

计数排序是一种非比较性的排序算法,适用于一定范围内的整数排序。在元素范围相对较小的情况下,计数排序具有线性时间复杂度,是一种非常高效的排序算法。


时间复杂度和空间复杂度

算法时间复杂度空间复杂度
归并排序O(n log n)O(n)
计数排序O(n + k)O(k)

完整代码

可以来我的github参观参观,看完整代码
路径点击这里–>所有排序练习

总结

通过深入了解归并排序和计数排序这两种排序算法的原理和实现,我们可以更好地理解它们的优劣势以及适用场景。选择合适的排序算法对于提高程序的效率和性能至关重要。希望本文的介绍能够为读者提供有益的知识,启发对排序算法的深入思考。

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

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

相关文章

c# cad2016选择封闭多段线获取多段线面积

在C#中&#xff0c;如果你想要通过AutoCAD .NET API来选择封闭多段线内部的其他闭合多段线并计算它们各自的面积&#xff0c;可以遵循以下基本步骤&#xff1a; 1、加载AutoCAD库&#xff1a; 确保你的C#项目引用了Autodesk.AutoCAD.Interop和Autodesk.AutoCAD.Interop.Common…

C语言-预处理

1.C语言的编译过程&#xff1a; 预处理、编译、汇编、链接 gcc -E hello.c -o hello.i 1、预处理 gcc -S hello.i –o hello.s 2、编译 gcc -c hello.s -o hello.o 3、汇编 gcc hello.o -o hello_elf 4、链接 1&#xff1a;预编译…

浅谈WPF之样式与资源

WPF通过样式&#xff0c;不仅可以方便的设置控件元素的展示方式&#xff0c;给用户呈现多样化的体验&#xff0c;还简化配置&#xff0c;避免重复设置元素的属性&#xff0c;以达到节约成本&#xff0c;提高工作效率的目的&#xff0c;样式也是资源的一种表现形式。本文以一个简…

数学建模论文笔记

一、概述 1. 数学建模论文组成 论文电子版&#xff1a;摘要页、正文、参考文献、附录支撑材料&#xff1a;源程序代码以及调用说明、中间结果、支撑数据等首页&#xff1a;论文题目、摘要、关键词论文正文&#xff1a;问题重述、问题分析、模型假设、符号说明、模型建立与求解…

centos 7 增加临时路由及永久路由

centos 7 增加临时路由及永久路由 如果增加临时路由&#xff0c;要先安装net-tools , sudo yum install net-tools route add -net 10.1.0.0 gw 10.1.1.1 netmask 255.255.0.0 意思是增加了一条动态路由&#xff0c;网关10.1.1.1 ,10.1.x.x 的所有ip都走这个网关 此种方式&am…

常见OLAP对比

Olap&#xff08;On-line Analytical Processing&#xff0c;联机分析处理&#xff09;&#xff1a;是在基于数据仓库多维模型的基础上实现的面向分析的各类操作的集合。可以比较下其与传统的OLTP&#xff08;On-line Transaction Processing&#xff0c;联机事务处理&#xff…

C语言第十弹---函数(上)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 函数 1、函数的概念 2、库函数 2.1、标准库和头文件 2.2、库函数的使用方法 2.2.1、功能 2.2.2、头文件包含 2.2.3、实践 2.2.4、库函数文档的⼀般格式 …

Unity中URP下额外灯的距离衰减

文章目录 前言一、额外灯的距离衰减二、DistanceAttenuation函数的传入参数1、distanceSqr2、distanceAndSpotAttenuation3、_AdditionalLightsAttenuation4、GetPunctualLightDistanceAttenuation函数三、DistanceAttenuation函数的程序体 前言 在上一篇文章中&#xff0c;我…

组件冲突、data函数、组件通信

文章目录 1.组件的三大组成部分 - 注意点说明2.组件的样式冲突&#xff08;用 scoped 解决&#xff09;3.data是一个函数4.组件通信1.什么是组件通信&#xff1f;2.不同的组件关系 和 组件通信方案分类 5.prop详解prop 校验①类型校验②完整写法&#xff08;类型&#xff0c;非…

计算机毕业设计 | SSM 凌云招聘平台(附源码)

1&#xff0c;绪论 人力资源是企业产生效益、创造利润的必不可少的、最重要的资源。人作为人力资源的个体可看作是一个承载着有效知识、能力的信息单元。这样的信息单元可看作是一个为企业产生价值和利润的个体。从而使得这样的信息单元所具有的信息就是一个有价值的信息。 校…

什么是SQL,什么是MYSQL?MYSQL的架构以及SQL执行语句的过程是什么?有哪些数据库的类型?一篇文章带你弄懂!

文章目录 前言一、为什么需要数据库二、数据库的相关概念1.什么是结构化查询语言 (SQL)2.什么是数据库管理系统 (DBMS)3.什么是 MySQL 数据库 三、数据库分类1.关系型数据库&#xff08;SQL&#xff09;2.非关系型数据库&#xff08;NoSQL&#xff09; 四、MYSQL架构1.各组件功…

初识MQRabbitMQ快速入门

一、同步和异步通讯 微服务间通讯有同步和异步两种方式&#xff1a; 同步通讯&#xff1a;就像打电话&#xff0c;需要实时响应。 异步通讯&#xff1a;就像发邮件&#xff0c;不需要马上回复。 两种方式各有优劣&#xff0c;打电话可以立即得到响应&#xff0c;但是你却不能…

「JavaSE」类和对象4

类和对象4 &#x1f349;内部类&#x1f34c;实例内部类&#x1f34c;静态内部类&#x1f34c;局部内部类&#x1f34c;匿名内部类 &#x1f349;总结 &#x1f349;内部类 在 Java 中&#xff0c;我们可以将一个类定义在另一个类或者一个方法的内部&#xff0c;前者称为内部类…

IS-IS:04 DIS

IS-IS 协议只支持两种网络类型&#xff0c;即广播网络和点到点网络。与 OSPF 协议相同&#xff0c; IS-IS 协议在广播网络中会将网络视为一个伪节点 &#xff08; Pesudonde&#xff0c;简称 PSN&#xff09;&#xff0c;并选举出一台DIS &#xff08;Designated IS&#xff09…

探索Pyecharts之美-绘制多彩旭日图的艺术与技巧【第37篇—python:旭日图】

文章目录 引言准备工作绘制基本旭日图调整颜色和样式添加交互功能定制标签和标签格式嵌套层级数据高级样式与自定义进阶主题&#xff1a;动态旭日图数据源扩展&#xff1a;外部JSON文件总结 引言 数据可视化在现代编程中扮演着重要的角色&#xff0c;而Pyecharts是Python中一个…

Tomcat怎么优化

目录 性能方面的优化&#xff1a; 安全方面的优化&#xff1a; 引言&#xff1a;面试官问到的Tomcat怎么优化&#xff0c;这两个方面直接得到他认可&#xff01;&#xff01; 性能方面的优化&#xff1a; 内存优化&#xff1a;-Xms java虚拟机初始化时的最小内存、-Xmx java虚…

操作系统的引入

操作系统 【1】什么是操作系统 操作系统是一种管理的计算机硬件的软件资源的程序。它充当了计算机系统和应用程序之间的接口。使得计算机用户能够地使用计算机系统来完成各种任务。操作系统是负责管理和分配计算机的处理器、内存、硬盘等等硬件资源&#xff0c;同时也提供一些…

Vue3在css中使用v-bind绑定js/ts变量,也可以在scss和less中使用方式

主要介绍Vue3中的新增的v-bind()的常用使用方式&#xff0c;主要包括在css,less,scss中的使用&#xff0c;可以参考官方文档查看&#xff1a;Vue3官方文档 特别提醒 如果你想在scss中或者less中使用&#xff0c;可能会报各种乱七八糟的错误&#xff0c;最快最好用的方式就是单…

Android P 背光机制流程分析

在android 9.0中&#xff0c;相比android 8.1而言&#xff0c;背光部分逻辑有较大的调整&#xff0c;这里就对android P背光机制进行完整的分析。 1.手动调节亮度 1.1.在SystemUI、Settings中手动调节 在界面(SystemUI)和Settings中拖动进度条调节亮度时&#xff0c;调节入口…

[docker] Docker的私有仓库部署——Harbor

一、Docker原生私有仓库—— Registry 1.1 Registry的简单了解 关于Docker的仓库分为私有库和公有仓库&#xff0c;共有仓库只要在官方注册用户&#xff0c;登录即可使用。但对于仓库的使用&#xff0c;企业还是会有自己的专属镜像&#xff0c;所以私有库的搭建也是很有必要的…