排序算法之——归并排序,计数排序

devtools/2024/10/4 12:27:48/

文章目录

  • 前言
  • 一、归并排序
    • 1. 归并排序的思想
    • 2. 归并排序时间复杂度及空间复杂度
    • 3. 归并排序代码实现
      • 1)递归版本
      • 2)非递归版本
  • 二、计数排序
    • 1. 计数排序的思想
    • 2. 计数排序的时间复杂度及空间复杂度
    • 3. 计数排序代码实现
  • 总结(算法>排序算法稳定性)


前言

今天我们一起来了解归并排序(MergeSort),以及一种非比较的计数排序(CountSort)~

在这里插入图片描述


一、归并排序

1. 归并排序的思想

归并排序(Merge Sort)是一种基于分治法(Divide and Conquer)的高效算法>排序算法。它的核心步骤包括将一个数组分割成更小的子数组,对这些子数组进行排序,然后将它们合并成一个有序的数组。以下是归并排序的核心步骤详解:

归并排序的核心步骤:

  1. 分割(Divide)

    • 将待排序数组从中间分割成两个子数组(通常是左半部分和右半部分)。
    • 继续递归地将这两个子数组再分割,直到每个子数组的大小为 1(即只有一个元素),因为一个元素的数组是有序的。
  2. 归并(Merge)

    • 将两个有序的子数组合并成一个有序的数组。
    • 使用两个指针分别指向两个子数组的开头,比较指针指向的元素,将较小的元素放入新数组中,并移动相应的指针。
    • 当其中一个子数组的元素被全部放入新数组后,将另一个子数组剩余的元素直接复制到新数组的后面。
  3. 组合(Combine)

    • 在归并的过程中,逐步形成更大的有序数组,最终得到一个完整的有序数组。

总结:
归并排序是一种高效的算法>排序算法,利用分治法的思想,通过将大问题分解为小问题来解决。其稳定性、时间复杂度 O(n log n) 以及适用于大规模数据的特性使其在实际应用中非常流行。

我们一起来看下面的图:
在这里插入图片描述

通过这张图我们可以很直观的感受到归并排序的分解和合并过程,它的步骤是这样的:

归并排序步骤总结 :

  1. 定义归并排序主函数

    • MergeSort 函数接受待排序数组和数组的大小作为参数。
    • 在函数中分配一个临时数组 tmp,用于辅助合并数组。其实我们合并出来的数据是在tmp中的,最后拷贝回去。
    • 调用 _MergeSort 函数进行排序。
    void MergeSort(int* arr, int n) {int* tmp = (int*)malloc(sizeof(int) * n); // 创建临时数组_MergeSort(arr, 0, n - 1, tmp); // 调用归并排序free(tmp); // 释放临时数组
    }
    
  2. 定义归并排序递归函数

    • _MergeSort 函数接受数组、左边界、右边界和临时数组作为参数。
    • 基本情况:如果 left 大于或等于 right,则返回,表示该子数组已经有序。
    void _MergeSort(int* arr, int left, int right, int* tmp) {if (left >= right) {return; // 基本情况}...
    }
    
  3. 分割数组

    • 计算中间索引 mid,将数组分割为左右两个部分。
    • 递归调用 _MergeSort 对左半部分(leftmid)和右半部分(mid + 1right)进行排序。

    在这里插入图片描述
    对于它的每一个左右两侧都要继续分割(以左侧为例):
    在这里插入图片描述

    int mid = (left + right) / 2;
    _MergeSort(arr, left, mid, tmp); // 递归排序左半部分
    _MergeSort(arr, mid + 1, right, tmp); // 递归排序右半部分
    
  4. 合并有序子数组

    • 定义两个指针 begin1begin2,分别指向左子数组和右子数组的起始位置。
    • 使用 index 指针在临时数组 tmp 中进行合并。

    在这里插入图片描述

    int begin1 = left, end1 = mid; // 左子数组范围
    int begin2 = mid + 1, end2 = right; // 右子数组范围
    int index = begin1; // 临时数组下标while (begin1 <= end1 && begin2 <= end2) {if (arr[begin1] < arr[begin2]) {tmp[index++] = arr[begin1++]; // 复制较小的元素} else {tmp[index++] = arr[begin2++];}
    }
    
  5. 处理剩余元素

    • 如果左子数组还有剩余元素,将其复制到 tmp 中。
    • 如果右子数组还有剩余元素,也将其复制到 tmp 中。
    while (begin1 <= end1) {tmp[index++] = arr[begin1++]; // 复制左子数组剩余元素
    }
    while (begin2 <= end2) {tmp[index++] = arr[begin2++]; // 复制右子数组剩余元素
    }
    
  6. 将合并结果复制回原数组

    • 将临时数组 tmp 中的有序元素复制回原数组 arr 的相应位置。
    for (int i = left; i <= right; i++) {arr[i] = tmp[i]; // 将临时数组的内容复制回原数组
    }
    

2. 归并排序时间复杂度及空间复杂度

归并排序(Merge Sort)是一种高效的算法>排序算法,其时间复杂度和空间复杂度如下:

时间复杂度 :

  • 归并排序的时间复杂度为 O(n log n)
    • 分割阶段:每次将数组分成两个子数组,深度为 log n,表示分割的层数。

    • 合并阶段:在每一层中,合并两个子数组的过程需要 O(n) 的时间,因为每个元素都要被处理一次。

    • 因此,整个算法的时间复杂度可以表示为:
      在这里插入图片描述

    • 根据主定理,得到归并排序的时间复杂度为 O(n log n)。

空间复杂度 :

  • 归并排序的空间复杂度为 O(n)
    • 归并排序需要使用额外的临时数组来存储合并过程中的结果。在每次合并操作中,都会分配一个大小为 n 的临时数组,因此其空间复杂度为 O(n)。

总结 :

  • 时间复杂度:O(n log n)
  • 空间复杂度:O(n)

3. 归并排序代码实现

1)递归版本

//归并排序
void _MergeSort(int* arr, int left, int right, int* tmp)
{if (left >= right){return;}int mid = (left + right) / 2;_MergeSort(arr, left, mid, tmp);_MergeSort(arr, mid + 1, right, tmp);int begin1 = left, end1 = mid;int begin2 = mid + 1, end2 = right;//index 为tmp下标,不一定是0,而是左边界int index = begin1;while (begin1 <= end1 && begin2 <= end2){if (arr[begin1] < arr[begin2]){tmp[index++] = arr[begin1++];}else{tmp[index++] = arr[begin2++];}}while (begin1 <= end1){tmp[index++] = arr[begin1++];}while (begin2 <= end2){tmp[index++] = arr[begin2++];}for (int i = left; i <= right; i++){arr[i] = tmp[i];}
}void MergeSort(int* arr, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);_MergeSort(arr, 0, n - 1, tmp);free(tmp);
}

2)非递归版本


/*
非递归排序与递归排序相反,将一个元素与相邻元素构成有序数组,
再与旁边数组构成有序数组,直至整个数组有序。
要有mid指针传入,因为不足一组数据时,重新计算mid划分会有问题
需要指定mid的位置
*/
void merge(int* a, int left, int mid, int right, int* tmp)
{// [left, mid]// [mid+1, right]int begin1 = left, end1 = mid;int begin2 = mid + 1, end2 = right;int index = left;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2])tmp[index++] = a[begin1++];elsetmp[index++] = a[begin2++];}while (begin1 <= end1){tmp[index++] = a[begin1++];}while (begin2 <= end2){tmp[index++] = a[begin2++];}memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}/*
k用来表示每次k个元素归并
*/
void mergePass(int* arr, int k, int n, int* temp)
{int i = 0;//从前往后,将2个长度为k的子序列合并为1个while (i < n - 2 * k + 1){merge(arr, i, i + k - 1, i + 2 * k - 1, temp);i += 2 * k;}//合并区间[i, n - 1]有序的左半部分[i, i + k - 1]以及不及一个步长的右半部分[i + k, n - 1]if (i < n - k){merge(arr, i, i + k - 1, n - 1, temp);}}

二、计数排序

1. 计数排序的思想

计数排序是一种非比较算法>排序算法,适用于范围有限的整数数据。它的核心思想是利用数组的索引来直接计算元素的位置,达到排序的目的。
1)统计相同元素出现次数
2)根据统计的结果将序列回收到原来的序列

我们来看下面这张图:
在这里插入图片描述
这是我们待排序的数组。

  • 第一步:定义一个新数组,使得旧数组的元素值是新数组的下标

假设,我们定义成这样行不行?
在这里插入图片描述
如果完全按照旧数组的元素值来开空间会造成大量的空间浪费,因此不可取。

这里还有一个问题,如果旧数组的元素值是负数怎么办呢?

这里空间问题可以和负数问题一起解决:

首先找到旧数组的最小值,以及最大值
在这里插入图片描述

遍历一遍数组很容易找到数组最小值为100,最大值为109.

那我们开的空间 range 就是最值差
在这里插入图片描述

  • 第二步:旧数组中每一个元素减去最小值min作为新数组下标,出现了几个新数组对应的下标中的元素就是多少,这样也顺便解决了负数问题,因为负数 - 更小的负数 = 正数。

在这里插入图片描述

  • 第三步:遍历新数组,只要数组数据不为零就循环次数(数组值),输出内容 (i + min),将所有元素拷贝回原来的数组,就完成了排序。
    在这里插入图片描述

2. 计数排序的时间复杂度及空间复杂度

计数排序是一种有效的算法>排序算法,特别适用于数据范围有限的情况。根据你提供的代码,以下是计数排序的时间复杂度和空间复杂度分析:

时间复杂度:

计数排序的时间复杂度为 O(n + k),其中:

  • n:待排序数组的大小。
  • k:待排序元素的范围(最大值 - 最小值 + 1)。

分析过程

  1. 找最大最小值:遍历数组一次,找到最大值和最小值,时间复杂度为 O(n)。
  2. 创建计数数组:分配大小为 k 的计数数组,时间复杂度为 O(k)。
  3. 统计元素出现次数:再次遍历原数组,将每个元素的出现次数记录在计数数组中,时间复杂度为 O(n)。
  4. 放回排序结果:遍历计数数组,根据记录的次数将元素放回原数组,最坏情况下需要遍历 k 次,时间复杂度为 O(k)。

因此,总的时间复杂度为:
在这里插入图片描述

空间复杂度 :

计数排序的空间复杂度为 O(k),其中 k 是待排序元素的范围。

分析过程

  • 需要一个计数数组 count,其大小为 k,用于存储每个元素出现的次数。
  • 如果输出数组与原数组相同,空间复杂度会增加到 O(n + k)(包含原数组和计数数组),但通常我们只考虑额外空间,因此空间复杂度通常记为 O(k)。

总结

  • 时间复杂度:O(n + k)
  • 空间复杂度:O(k)

这种复杂度特性使得计数排序在处理范围有限的整数数据时非常高效。适合用于大规模的正整数排序,尤其在数值分布相对均匀的情况下。
但缺点也很明显,数据过于分散太浪费空间,而且只能处理整数数据


3. 计数排序代码实现

// 计数排序
void CountSort(int* arr, int n)
{int max = arr[0], min = arr[0];//找最大最小值for (int i = 1; i < n; i++){if (arr[i] > max)max = arr[i];if (arr[i] < min)min = arr[i];}//定义新数组空间大小int range = max - min + 1;int* count = (int*)malloc(range * sizeof(int));if (count == NULL){perror("malloc fail!");exit(1);}//初始化range数组中所有的数据为0memset(count, 0, sizeof(int) * range);//原来的下标成为元素,原来的(元素 - min) 成为下标,出现几次新的元素就加几次for (int i = 0; i < n; i++){count[arr[i] - min]++;}//把排好序的元素放回原数组int index = 0;for (int i = 0; i < range; i++){while (count[i]--){arr[index++] = i + min;}}
}

总结(算法>排序算法稳定性)

到了这里我们所有排序思想已经学完了,再来一起总结一下吧!

算法>排序算法复杂度及稳定性分析:

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的
相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,⽽在排序后的序列中,r[i]仍在r[j]之
前,则称这种算法>排序算法是稳定的;否则称为不稳定的。

在这里插入图片描述

在这里插入图片描述

谢谢大家~

在这里插入图片描述


http://www.ppmy.cn/devtools/121253.html

相关文章

【Qt Quick】基础语法:变量类型

在本节中&#xff0c;我们将讨论 QML 中的变量类型。与 C 相似&#xff0c;QML 也有多种变量类型&#xff0c;但在 QML 中&#xff0c;主要分为值类型和对象类型。由于 QML 没有指针的概念&#xff0c;因此在值类型和对象类型的传递中有一些不同点。 值类型和对象类型 值类型…

《深度学习》OpenCV 图像拼接 拼接原理、参数解析、案例实现

目录 一、图像拼接 1、直接看案例 图1与图2展示&#xff1a; 合并完结果&#xff1a; 2、什么是图像拼接 3、图像拼接步骤 1&#xff09;加载图像 2&#xff09;特征点检测与描述 3&#xff09;特征点匹配 4&#xff09;图像配准 5&#xff09;图像变换和拼接 6&am…

HBase 性能优化 详解

HBase 是基于 Hadoop HDFS 之上的分布式 NoSQL 数据库&#xff0c;具有高伸缩性和强大的读写能力。然而&#xff0c;由于其分布式架构和复杂的数据存储模式&#xff0c;在高并发、大规模数据场景下&#xff0c;HBase 性能优化至关重要。从底层原理和源代码层面理解 HBase 的特性…

dockerhub 镜像拉取超时的解决方法

在几个月前&#xff0c;因为一些原因&#xff0c;导致 dockerhub 官网上面的镜像拉取超时&#xff0c;目前可以通过修改仓库地址&#xff0c;通过 daocloud 拉取 public-image-mirror 方式一 源仓库替换仓库cr.l5d.iol5d.m.daocloud.iodocker.elastic.coelastic.m.daocloud.io…

Redis篇(数据类型)

目录 讲解一&#xff1a;简介 讲解二&#xff1a;常用 一、String类型 1. 简介 2. 常见命令 3. Key结构 4. 操作String 5. 实例 二、Hash类型 1. 简介 2. 常见命令 3. 3操作hash 4. 实例 三、List类型 1. 简介 2. 特征 3. 应用场景 4. 常见命令 5. 操作list …

1-仙灵之谜(区块链游戏详情介绍)

1-仙灵之谜&#xff08;区块链游戏详情介绍&#xff09; 前言&#xff08;该游戏仅供娱乐&#xff09;正文 前言&#xff08;该游戏仅供娱乐&#xff09; 依稀记得本科那会儿参加了一个区块链实验室&#xff0c;那时每周末大家都会爬山或者抽出一下午讨论区块链以及未来&#x…

用责任链模式改造 if else

我的上一篇文章&#xff0c;因为if else 多了&#xff0c;捣鼓很久&#xff0c;今天用责任链模式改造一下。 代码写着写着&#xff0c;if else if 逻辑忘记了&#xff0c;哎。。。-CSDN博客 责任链模式&#xff08;Chain of Responsibility Pattern&#xff09; 1. 什么是责任…

国产RISC-V蓝牙MCU推荐

RAMSUN蓝牙MCU配套成熟的网络协议栈和丰富的示例代码及多平台APP工具。无需二次开发&#xff0c;即连即用&#xff1b;提供特色蓝牙/串口/USB三通芯片&#xff0c;为更多复杂无线应用赋能。 32位RISC-V设计的工业级通用微控制器。全系产品加入硬件堆栈区、快速中断入口等设计&…