归并排序 与 计数排序

简介: 归并排序 与 计数排序

1.归并排序

基本思想:

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

归并排序核心步骤:

动图演示:

1.1 递归实现归并排序:

归并排序类似于二叉树中的后序遍历,先让整个数组分为两个子序列,归并这两部份子序列,但是归并需要两部份子序列有序,然后取小的尾插到一个新开辟的数组中,归并完成后后再拷贝回原数组,如何让子序列有序,还要再次将每个子序列分为两部分,直到每个子序列只有一个值,这时已经递归到最深处,然会递归向回归并。

递归代码实现:

//归并排序
//开辟好空间后由下面元素调用此函数
void _MergeSort(int* arr, int* tmp, int begin, int end)
{
  if (begin == end)
  {
    return;
  }
  int midi = (begin + end) / 2;
  _MergeSort(arr, tmp, begin, midi);
  _MergeSort(arr, tmp, midi+1, end);
  int begin1 = begin;
  int end1 = midi;
  int begin2 = midi + 1;
  int end2 = end;
  int i = begin;
  //归并  取小的尾插到开辟的空间
  while (begin1 <= end1 && begin2 <= end2)
  {
    if (arr[begin1] <= arr[begin2])
    {
      tmp[i++] = arr[begin1++];
    }
    else
    {
      tmp[i++] = arr[begin2++];
    }
  }
  while (begin1 <= end1)
  {
    tmp[i++] = arr[begin1++];
  }
  while (begin2 <= end2)
  {
    tmp[i++] = arr[begin2++];
  }
    //将归并好的两组数据拷贝会原数组
  memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* arr, int n)
{
    //开辟空间
  int* tmp = (int*)malloc(sizeof(int) * n);
  _MergeSort(arr, tmp, 0, n - 1);
}

小区间优化

//小区间优化
if (end - begin +1<10)
{
    //使用插入排序
  InsertSort(arr + begin, end - begin + 1);
  return;
}

优化的本质是减小递归调用的次数,由于二叉树的性质。我们可以得出满二叉树后三层大约占总个数的85%。为了减小递归开销,我们可以将小区间的递归调用改为直接插入排序可以提高一点排序的性能,但也不会提高很多。快排也可以使用这种方式优化。

1.2 非递归实现归并排序

我们可以先让每组gap=1个数据,每次归并两组,然后在让gap*=2,再次归并,直到gap>n。

代码实现:

//非递归实现归并排序
void MergeSortNonR1(int* arr, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  //每组有gap个数据,归并两组
  int gap = 1;
  while (gap < n)
  {
    int j = 0;
    for (int i = 0; i < n; i += 2 * gap)
    {
      int begin1 = i, end1 = i + gap - 1;
      int begin2 = i + gap, end2 = i + 2 * gap - 1;
      if (end1 >= n || begin2 >= n)//不需要归并
      {
        break;
      }
      //修正
      if (end2 >= n)
      {
        end2 = n - 1;
      }
      //归并
      while (begin1 <= end1 && begin2 <= end2)
      {
        if (arr[begin1] <= arr[begin2])
        {
          tmp[j++] = arr[begin1++];
        }
        else
        {
          tmp[j++] = arr[begin2++];
        }
      }
      while (begin1 <= end1)
      {
        tmp[j++] = arr[begin1++];
      }
      while (begin2 <= end2)
      {
        tmp[j++] = arr[begin2++];
      }
      //将归并后的两组数据 拷贝回原数组 
      memcpy(arr + i, tmp + i, sizeof(int) * (end2 - i + 1));
    }
    gap *= 2;
  }
}

边界越界问题:

int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;

begin1不会越界,因为begin1 = i,i 复合循环条件 。

  1. end1,begin2,end2都越界
  2. begin2,end2越界
  3. end2越界

1. end1,begin2,end2都越界

  此时不需要归并直接跳出循环。

2. begin2,end2越界

此时也不需要归并直接跳出循环。

3. end2越界

此时需要归并,但是我们要修改end2,将end2改为n-1。

代码:

if (end1 >= n || begin2 >= n)//不需要归并
  {
    break;
  }
  //修正
  if (end2 >= n)
  {
    end2 = n - 1;
  }

1.3 归并排序的特性总结:

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定

1.4 外部排序

概念:当数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

在我们所学的排序算法中,只有非递归归并排序的思想可以用于外部排序。其他排序算法都只适用于内部排序,因为他们都使用了下标来进行随机存取,而非递归归并排序不需要,是顺序存取,这里举个例子:

假如我们由100亿个整数要排序,也就是大约40G,而我们的内存中只有1G,步骤:

  1. 把40G的文件分为40份。
  2. 让每份文件依次放到内部中排序,让40份文件内部有序。
  3. 两两归并,分别从两个文件中读一个数据,然后选小的写文件,这时就与非递归归并排序相同了。

2.计数排序

思想:计数排序又称为鸽巢原理,是一种非比较排序,是对哈希直接定址法的变形应用。

2.1 操作步骤:

  1. 统计相同元素出现次数
  2. 根据统计的结果将序列回收到原来的序列中

代码实现:

// 计数排序
void CountSort(int* arr, int n)
{
  //遍历 确定最大值与最小值
  int max = arr[0];
  int min = arr[0];
  for (int i = 0; i < n; i++)
  {
    if (arr[i] < min)
    {
      min = arr[i];
    }
    if (arr[i] > max)
    {
      max = arr[i];
    }
  }
  //遍历计数
  int range = max - min + 1;
  int* CountA = (int*)malloc(sizeof(int) * range);
  memset(CountA, 0, sizeof(int) * range);
  for (int i = 0; i < n; i++)
  {
    CountA[arr[i] - min]++;
  }
  //回收到原数组
  int j = 0;
  for (int i = 0; i < range; i++)
  {
    while (CountA[i]--)
    {
      arr[j++] = i + min;
    }
  }
}

2.2 计数排序的特性总结:

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

3. 7种常见比较排序比较

排序方法 平均情况 最好情况 最坏情况 辅助空间 稳定性
冒泡排序 O(N^2) O(N) O(N^2) O(1) 稳定
简单选择排序 O(N^2) O(N^2) O(N^2) O(1) 不稳定
直接插入排序 O(N^2) O(N) O(N^2) O(1) 稳定
希尔排序 O(NlogN)~O(N^2) O(N^1.3) O(N^2) O(1) 不稳定
堆排序 O(NlogN) O(NlogN) O(N*logN) O(1) 不稳定
归并排序 O(NlogN) O(NlogN) O(N*logN) O(n) 稳定
快速排序 O(NlogN) O(NlogN) O(N^2) O(logn)~O(n) 不稳定

本篇结束!

相关文章
|
数据可视化 Java 数据安全/隐私保护
食品生产管理系统 毕业设计 JAVA+Vue+SpringBoot+MySQL
食品生产管理系统 毕业设计 JAVA+Vue+SpringBoot+MySQL
293 0
|
10月前
|
SQL 数据可视化 BI
VeryReport和FineReport两款报表软件深度分析对比
VeryReport和FineReport两款报表软件深度分析对比
|
机器学习/深度学习 人工智能 运维
智能运维:AI驱动的IT运维革命###
【10月更文挑战第21天】 随着数字化转型的深入,智能运维(AIOps)正逐步成为企业IT管理的核心。本文将探讨AI技术如何赋能运维领域,通过自动化、智能化手段提升系统稳定性和效率,降低运营成本,并分享实施智能运维的最佳实践与挑战应对策略。 ###
954 1
|
存储 JSON 开发框架
给开源大模型带来Function Calling、 Respond With Class
OpenAI 在他的多个版本的模型里提供了一个非常有用的功能叫 Function Calling,就是你传递一些方法的信息给到大模型,大模型根据用户的提问选择合适的方法,然后输出给你,你再来执行。
|
11月前
|
监控 安全 网络协议
计算机端口:网络通信的桥梁
计算机端口是网络通信的逻辑通道,支持数据传输和服务识别。本文介绍端口定义、分类(知名、注册、动态端口)、作用及管理方法,涵盖常用知名端口如HTTP(80)、HTTPS(443)等,并强调端口安全配置的重要性,帮助读者全面理解这一关键组件。
927 6
|
Shell 测试技术 Android开发
掌握ADB:详解操作命令及完整用法指南(二)
**ADB摘要** Android Debug Bridge (ADB) 是用于PC与Android设备通信的调试工具。本文聚焦于`adb shell am`命令,用于控制设备执行操作,如启动应用: ```bash adb shell am start &lt;package&gt;/&lt;activity&gt; ``` 它还用于测量应用启动时间。`dumpsys window`命令获取包名和界面信息。ADB还能模拟手机按键,如点击、滑动、输入文本和控制音量。此外,通过`dumpsys cpuinfo`和`meminfo`可检查CPU和内存使用情况。这些在自动化测试中非常有用。
|
存储 双11 云计算
云计算的前世今生来世(一)
云计算历经四阶段:从大型机到个人电脑,再到互联网和云计算革命。它主要解决计算、网络、存储和应用问题。计算发展表现为“分合分”的模式:物理设备、虚拟化、云计算及容器化。如今,云计算已渗透到生活各个方面,包括工业、日常交易如双十一购物节,以及办公、社交应用等。技术路径涵盖IaaS(计算、网络、存储)和PaaS(应用)层面。
|
运维 安全 Linux
深入理解 Linux 文件系统的权限管理
【5月更文挑战第30天】 在 Linux 操作系统中,文件系统权限管理是维护系统安全与数据完整性的基石。本文旨在深入探讨 Linux 权限模型的核心概念、实现机制及其对系统安全性的影响。通过对用户身份、文件权限和访问控制列表(ACL)等关键元素的剖析,揭示权限管理在实际操作中的应用细节。文章还将讨论如何有效配置权限来优化系统性能和提升安全性,以及解决常见的权限问题的策略。
|
SQL Java 数据库连接
SQL游标的基本使用方法与示例
SQL游标的基本使用方法与示例
|
JSON JavaScript 数据格式
jwt-auth插件实现了基于JWT(JSON Web Tokens)进行认证鉴权的功能
jwt-auth插件实现了基于JWT(JSON Web Tokens)进行认证鉴权的功能
400 1