【数据结构与算法】深入理解归并排序(Merge Sort)

2024-08-28 12:20

本文主要是介绍【数据结构与算法】深入理解归并排序(Merge Sort),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

问题背景与引入

排序问题是计算机科学中的基础问题之一,几乎在所有的数据处理过程中都会遇到。例如,在数据库查询中,我们通常需要按某个字段对数据进行排序,以便快速检索。在图形处理和数据分析中,排序也是一项关键操作。

归并排序(Merge Sort)是计算机科学中最经典的排序算法之一,它以其稳定的时间复杂度和良好的排序性能在各种应用场景中得到广泛应用。

归并排序的算法原理

归并排序是一种基于“分治法”的排序算法。分治法是一种递归分解问题、解决子问题并将其合并的策略。归并排序正是通过递归地将一个序列分割成若干子序列,将每个子序列排序后再将其合并,从而达到对整个序列排序的目的。

分割阶段

在分割阶段,归并排序通过递归地将待排序序列从中间分成两部分,直到每个子序列只有一个元素或为空。对于一个长度为 n 的序列,这个分割过程大约会进行 log₂(n) 次。

合并阶段

在合并阶段,归并排序将之前分割出的子序列逐一合并。每次合并时,两个有序的子序列通过比较元素大小,依次合并成一个新的有序序列。

示例

我们用一个简单的例子来说明这个过程

如上图,我们先将完整的序列划分到不能再分为止,即子序列长度为1

然后将其合并,合并时的操作是将两个有序序列合并成一个新的有序序列。合并后的新序列与另一个新序列合并,直到得到最终排序结果

在这个过程中,最主要的操作就是将两个有序序列合并成一个有序序列,这个操作其实是很简单的,我们只需要比较两个元素大小,将较小的元素插入到新数组中,然后继续比较下一个元素,直到其中一个数组被完全遍历,然后将另一个数组剩余的元素插入到新数组中。

归并排序的实现细节

以下是我实现的一个归并排序的 Python 代码:

def mergesort(arr, i, j):"""使用归并排序算法对数组进行排序。参数:arr -- 待排序的数组i -- 排序的起始索引j -- 排序的结束索引"""# 当数组只有一个元素时,不需要排序if j - i == 1:return# 计算中间索引,将数组分成两半mid = (i + j) // 2# 递归调用mergesort,对左半部分进行排序mergesort(arr, i, mid)# 递归调用mergesort,对右半部分进行排序mergesort(arr, mid, j)# 将排序好的左右两部分合并arr[i:j] = merge(arr[i:mid], arr[mid:j])def merge(arr1, arr2):"""将两个已排序的数组合并为一个排序的数组。参数:arr1 -- 第一个已排序数组arr2 -- 第二个已排序数组返回:合并后的排序数组"""# 初始化一个新的数组用于存储合并后的排序数组arr = []# 初始化第二个数组的索引j = 0# 遍历第一个数组for i in range(len(arr1)):# 当第二个数组还有元素且其元素小于第一个数组当前元素时,将第二个数组的元素添加到新数组中while j < len(arr2) and arr1[i] > arr2[j]:arr.append(arr2[j])j += 1# 否则,将第一个数组当前元素添加到新数组中else:arr.append(arr1[i])else:# 将第二个数组剩余的元素添加到新数组中arr.extend(arr2[j:])# 返回合并后的排序数组return arr

代码解析

  1. mergesort 函数首先判断数组是否已经不能再分割(即长度小于等于 1),如果是,直接返回。

  2. 否则,将数组分为左右两部分,分别递归调用 mergesort 进行排序。

  3. 排序后的两个子数组通过 merge 函数合并成一个有序数组。

    merge 函数是合并两个有序数组的关键部分。它通过两个指针 ij 分别遍历 arr1arr2 数组,并将较小的元素插入到结果数组 arr 中。最后,将剩余未处理的元素直接添加到 arr 中。

运行示例

这里给出一个测试用例确认排序结果是否正确

arr = [38, 27, 43, 3, 9, 82, 10]
mergesort(arr, 0, len(arr))
print(arr)

运行结果为:

[3, 9, 10, 27, 38, 43, 82]

复杂度分析

时间复杂度

归并排序的时间复杂度为 O(n log n),其中 n 是数组的长度,log n 是由于分割数组的递归层数。在每一层递归中,合并操作的时间复杂度为 O(n),因此总体时间复杂度为 O(n log n)

这种复杂度在所有情况下(最坏、最好、平均)都是相同的,这使得归并排序在处理大型数据集时表现非常稳定。

我们可以与最简单的冒泡排序还有内置的timsort排序进行一个对比

def bubble_sort(arr):for i in range(len(arr)):for j in range(len(arr) - i - 1):if arr[j] > arr[j + 1]:arr[j], arr[j + 1] = arr[j + 1], arr[j]arr = [random.randint(-9999, 9999) for _ in range(10000)]
arr1 = arr.copy()
arr2 = arr.copy()
arr3 = arr.copy()
print('归并排序')
t = time.time()
mergesort(arr1, 0, len(arr1))
print(time.time() - t)
print('冒泡排序')
t = time.time()
bubble_sort(arr2)
print(time.time() - t)
print('内置排序')
t = time.time()
arr3.sort()
print(time.time() - t)print(f'正确性检验arr1==arr2==arr3:{arr1 == arr2 == arr3}')

运行结果如下

归并排序
0.016516447067260742
冒泡排序
4.049731254577637
内置排序
0.0010013580322265625
正确性检验arr1==arr2==arr3:True

可以很直观的看到,归并排序的速度要远快于冒泡排序。当然,跟内置的timsort排序相比,归并排序还是要慢许多

空间复杂度

归并排序的空间复杂度为 O(n)。这是因为在合并两个子序列时,需要额外的存储空间来保存中间结果。这也是归并排序的一个主要缺点,相比于一些就地排序算法,如快速排序,它需要更多的内存。

归并排序的优缺点

优点

  1. 稳定性:归并排序是稳定排序算法,即相同元素在排序后的相对顺序保持不变。这对于某些应用场景非常重要,如在多关键字排序中。

  2. 性能稳定:归并排序在最坏情况下的时间复杂度为 O(n log n),这使得它在处理各种情况下表现都很出色,尤其是在处理大规模数据时。

  3. 适合外部排序:由于归并排序的分割和合并特性,它可以很好地用于外部排序,即当数据量大到无法全部加载到内存时,归并排序可以与磁盘存储相结合进行排序。

缺点

  1. 空间复杂度较高:由于需要额外的存储空间来保存合并结果,归并排序在空间使用上不如一些原地排序算法(如快速排序)高效。

  2. 较高的常数因素:尽管归并排序的时间复杂度为 O(n log n),与快速排序相同,但其递归结构和额外的合并操作使得其实际运行时间往往比快速排序稍长,尤其是在处理小规模数据时。

归并排序的应用场景

归并排序因其稳定性和良好的性能,适用于许多实际应用场景:

  1. 大规模数据排序:当数据集非常庞大时,归并排序的 O(n log n) 时间复杂度使其非常适合。

  2. 外部排序:在处理超过内存容量的数据集时,归并排序可以与外部存储(如磁盘)结合使用,进行高效排序。

  3. 合并有序文件:归并排序的合并过程可以直接应用于将多个有序文件合并为一个有序文件

结语

归并排序是一种经典且强大的排序算法,通过分治策略和递归实现,能够在各种情况下提供稳定的性能表现。尽管它在空间使用上存在不足,但其稳定性和适应大规模数据的能力使其在许多应用场景中占据重要地位。

这篇关于【数据结构与算法】深入理解归并排序(Merge Sort)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1114755

相关文章

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

关于Java内存访问重排序的研究

《关于Java内存访问重排序的研究》文章主要介绍了重排序现象及其在多线程编程中的影响,包括内存可见性问题和Java内存模型中对重排序的规则... 目录什么是重排序重排序图解重排序实验as-if-serial语义内存访问重排序与内存可见性内存访问重排序与Java内存模型重排序示意表内存屏障内存屏障示意表Int

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

认识、理解、分类——acm之搜索

普通搜索方法有两种:1、广度优先搜索;2、深度优先搜索; 更多搜索方法: 3、双向广度优先搜索; 4、启发式搜索(包括A*算法等); 搜索通常会用到的知识点:状态压缩(位压缩,利用hash思想压缩)。

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig