[数据结构 -- 手撕排序算法第七篇] 递归实现归并排序

简介: [数据结构 -- 手撕排序算法第七篇] 递归实现归并排序

1、归并的思想

这是我们第二次了解归并的思想了,第一次在我们之前的链表oj题里面,合并两个有序链表,我们当时解题的思想就是归并的思想。
我们这次来系统的学习一下归并的思想(本篇以升序为例展开):


归并两个数组(链表)时,我们使用两个指针指向不同的数组首元素,控制并遍历两个数组,比较两个指针指向的值,小的我们放入开辟的临时数组里面,然后让指向小值的指针往后走一步,继续比较。不断去比,总有一个数组先被遍历完,由于是有序数组,另外那个没有被遍历完的数组直接连接到临时数组的后面就完成了排序。


我们画图来理解这个思想:



2、归并排序的思想

2.1 基本思想

归并排序是建立在归并操作上的一种有效的排序算法。


1.归并排序的思想是,将一个数组二分为左右两个数组,然后对左右两个数组继续进行二分,直到分为的子区间为一个数据的时候,这一个数据一定是有序的,便不再二分。


2.接下来就对左右两个子区间进行排序合并,不断排序合并,最终就实现了整个数组的排序。

2.2 图解分析


我们归并的时候不是直接在原数组上就交换的,直接在原数组上操作会产生覆盖,导致错误。因此我们是malloc一个与原数组大小相同的空间tmp,将每次合并后的值拷贝到tmp中,合并一次拷贝一次,最中tmp数组为有序,再将tmp使用memcpy拷贝回原数组,再free(tmp) (防止内存泄漏),整个排序就完成了。

3、归并排序递归版本代码实现

// 归并排序递归实现
// 时间复杂度:O(N*logN)
// 空间复杂度:O(N + logN)
void _MergeSort(int* a, int begin, int end, int* tmp)
{
  if (begin >= end)
    return;
  int mid = (begin + end) / 2;
  //[begin, mid] [mid+1, end]
  _MergeSort(a, begin, mid, tmp);
  _MergeSort(a, mid+1, end, tmp);
  int begin1 = begin, end1 = mid;
  int begin2 = mid + 1, end2 = end;
  int i = begin;
  while (begin1 <= end1 && begin2 <= end2)
  {
    if (a[begin1] <= a[begin2])
      tmp[i++] = a[begin1++];
    else
      tmp[i++] = a[begin2++];
  }
  while (begin1 <= end1)
  {
    tmp[i++] = a[begin1++];
  }
  while (begin2 <= end2)
  {
    tmp[i++] = a[begin2++];
  }
  memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  if (NULL == tmp)
  {
    perror("malloc fail:");
    return;
  }
  _MergeSort(a, 0, n-1, tmp);
  free(tmp);
}

3.1 代码解析


我们在这一段使用了递归,递归的思想就是二分,将数组分为左右两个区间,然后再对左右两个区间继续进行二分,当 begin >= end 时,就是最小的区间,然后我们就进行排序+合并。

这一段就是排序+合并,我们对两个子区间进行排序+合并,思路就是我们归并的思想。这里要注意,我们tmp的下标是begin,因为我们需要将每一层合并的值先放进tmp中,然后再拷贝到原数组里,如果是0,拷贝回去的时候就会出现错误。

3.2 注意事项

在划分左右区间的时候一定是 [begin, mid],[mid+1, end],

不能是 [begin, mid-1],[mid, end]!!!
这里很容易出错,看着逻辑上没问题,但是在排序的时候就会出问题。

3.2.1错误划分:[begin, mid-1],[mid, end]



3.2.2 正确划分:[begin, mid], [mid+1, end]


总结:造成第一种划分死循环的原因是,当我们 mid = begin+end/2 时,计算器是向下取整的,所以一旦向下取整的时候,我们的区间就要包含mid这个值,这样就会补上向下取整造成的舍去。

4、归并排序的测试

void Print(int* a, int n)
{
  for (int i = 0; i < n; i++)
  {
    printf("%d ", a[i]);
  }
  printf("\n");
}
void _MergeSort(int* a, int begin, int end, int* tmp)
{
  if (begin >= end)
    return;
  int mid = (begin + end) / 2;
  // [begin, mid] [mid+1, end]
  _MergeSort(a, begin, mid, tmp);
  _MergeSort(a, mid + 1, end, tmp);
  int begin1 = begin, end1 = mid;
  int begin2 = mid + 1, end2 = end;
  int i = begin;
  while (begin1 <= end1 && begin2 <= end2)
  {
    if (a[begin1] <= a[begin2])
      tmp[i++] = a[begin1++];
    else
      tmp[i++] = a[begin2++];
  }
  while (begin1 <= end1)
  {
    tmp[i++] = a[begin1++];
  }
  while (begin2 <= end2)
  {
    tmp[i++] = a[begin2++];
  }
  memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  if (NULL == tmp)
  {
    perror("malloc fail:");
    return;
  }
  _MergeSort(a, 0, n - 1, tmp);
  free(tmp);
}
void test()
{
  int a[] = { 6,3,2,1,5,7,9 };
  Print(&a, sizeof(a) / sizeof(int));
  MergeSort(&a, sizeof(a) / sizeof(int) - 1);
  Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
  test();
  return 0;
}

测试结果:



5、时间复杂度、空间复杂度分析

5.1 时间复杂度

归并排序区间不断二分,时间复杂度为O(logN);然后再合并,时间复杂度为O(N)。

整体的时间复杂度为o(N*logN)。

5.2 空间复杂度

因为我们开辟了一个临时空间用于排序,因此空间复杂度O(N)。

相关文章
|
24天前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
61 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
21天前
|
存储 算法 Java
Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性
Java Set因其“无重复”特性在集合框架中独树一帜。本文解析了Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性,并提供了最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的hashCode()与equals()方法。
31 4
|
28天前
|
算法 搜索推荐 Shell
数据结构与算法学习十二:希尔排序、快速排序(递归、好理解)、归并排序(递归、难理解)
这篇文章介绍了希尔排序、快速排序和归并排序三种排序算法的基本概念、实现思路、代码实现及其测试结果。
18 1
|
28天前
|
搜索推荐 算法
数据结构与算法学习十四:常用排序算法总结和对比
关于常用排序算法的总结和对比,包括稳定性、内排序、外排序、时间复杂度和空间复杂度等术语的解释。
17 0
数据结构与算法学习十四:常用排序算法总结和对比
|
27天前
|
机器学习/深度学习 搜索推荐 算法
探索数据结构:初入算法之经典排序算法
探索数据结构:初入算法之经典排序算法
|
28天前
|
算法 Java 索引
数据结构与算法学习十五:常用查找算法介绍,线性排序、二分查找(折半查找)算法、差值查找算法、斐波那契(黄金分割法)查找算法
四种常用的查找算法:顺序查找、二分查找(折半查找)、插值查找和斐波那契查找,并提供了Java语言的实现代码和测试结果。
17 0
|
5天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
64 9
|
2天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
4天前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
26 4
|
28天前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
26 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器