【数据结构排序算法篇】----归并排序【实战演练】

简介: 【数据结构排序算法篇】----归并排序【实战演练】

作为一名对技术充满热情的学习者,我一直以来都深刻地体会到知识的广度和深度。在这个不断演变的数字时代,我远非专家,而是一位不断追求进步的旅行者。通过这篇博客,我想分享我在某个领域的学习经验,与大家共同探讨、共同成长。请大家以开放的心态阅读,相信你们也会在这段知识之旅中找到启示。



前言

我们继续学习下一个排序,归并排序,这是一个重要的排序,排序思想也很重要,这种排序也很高效,希望同学们可以认真阅读。


一、什么是归并排序

归并排序(Merge Sort)是一种高效的排序算法,采用分治的策略来对数组或列表进行排序。基本思想是将原始数组分成若干子数组,对每个子数组进行排序,然后将它们合并成一个全部有序的数组

归并排序算法的主要步骤如下:

  1. 分解:递归地将当前数组分成两个长度差不多的子数组。
  2. 解决:递归地对子数组进行归并排序,如果子数组长度是 1 或者更小,则不需要继绀做分解,因为长度为 1 的数组自然而然就是有序的。
  3. 合并:将两个有序的子数组合并成一个有序的数组。

拿一个简单的数组 [38, 27, 43, 3, 9, 82, 10] 举例,归并排序的过程如下:

  1. 分解数组为 [38, 27, 43, 3][9, 82, 10]
  2. 继续分解 [38, 27, 43, 3][38, 27][43, 3],以及将 [9, 82, 10] 分解为 [9, 82][10]
  3. 进一步分解 [38, 27][38][27][43, 3][43][3],以及 [9, 82][9][82]。因为每个数组只有一个元素,开始合并。
  4. 合并 [38][27][27, 38],合并 [43][3][3, 43],以及 [9][82][9, 82]
  5. 将两个有序数组 [27, 38][3, 43] 合并成一个有序数组 [3, 27, 38, 43],将 [9, 82][10] 合并成 [9, 10, 82]
  6. 最终,将 [3, 27, 38, 43][9, 10, 82] 合并成最终的有序数组 [3, 9, 10, 27, 38, 43, 82]

归并排序需要额外的内存空间来存储临时数组,所以它不是一个原地排序算法。不过,归并排序特别适合处理大数据集,并且是稳定的排序算法,即具有相等键值的元素经排序后其相对位置不变。

归并排序算法的时间复杂度为 O(n log n),其中 n 是数组或列表中元素的数量。

这个复杂度来自于两个主要步骤:

  1. 分解步骤:数组被一分为二,直到每个子数组只有一个元素。分解数组的过程是对数阶的,即每次操作减少一半的问题规模,所以分解步骤大约需要 log n 层递归。
  2. 合并步骤:在每一层递归中,都要合并子数组。虽然每层的合并操作涉及到整个数组,但由于有 log n 层,合并操作的总时间复杂度是线性对数阶的,即 O(n log n)。

总结起来:归并排序的时间复杂度主要由多次的切分(对数阶次数,即 log n)和在每个切分阶段执行的合并操作(每次操作需要线性时间,即 n)决定,所以总体上是 O(n log n)。这使得归并排序在最坏、平均和最好情况下的时间复杂度都是 O(n logn),这是一种非常稳定的性能表现。

二、练习

简单练习

考虑以下整数数组:

[29, 10, 14, 37, 13]

让我们用归并排序一步步排序这个数组。

Step 1: 分割数组

首先,将整个数组分割成更小的部分,直到每个部分仅包含一个元素:

  1. 初始数组: [29, 10, 14, 37, 13]
  2. 分割为两部分: [29, 10][14, 37, 13]
  3. 继续分割: [29], [10], [14], [37, 13]
  4. 最后分割 [37, 13] 为: [37][13]

Step 2: 合并与排序

现在开始合并这些单个元素,组成排序好的子数组:

  1. 合并 [29][10] 得到 [10, 29]
  2. 合并 [37][13] 得到 [13, 37]
  3. 由于 [14] 已经是排序好的,它可以直接与 [13, 37] 合并为 [13, 14, 37]

现在,我们有 [10, 29][13, 14, 37],要合并它们成为一个排序好的大数组。

Step 3: 最终合并

  1. [10][13] 中较小的数,所以 [10] 是第一个。
  2. [29][13] 比较,选择 [13] 为下一个数,接着是 [14]
  3. [29] 下一个与 [37] 比较,我们选 [29]
  4. 最后的数是 [37]

合并后的数组是:[10, 13, 14, 29, 37]

较难练习

让我们对以下数组进行排序:

[34, -42, 0, 15, -9, 27, 51, 3, 12, 18]

Step 1: 分割数组

首先,分割数组直到每个子数组都是单个元素:

  1. 初始数组: [34, -42, 0, 15, -9, 27, 51, 3, 12, 18]
  2. 分割数组为两部分: [34, -42, 0, 15, -9][27, 51, 3, 12, 18]
  3. 继续分割前半部分: [34, -42][0, 15, -9]
  4. 继续分割后半部分: [27, 51][3, 12, 18]
  5. 继续分割,直到每个子数组只包含一个元素,我们得到: [34], [-42], [0], [15], [-9], [27], [51], [3], [12], [18]

Step 2: 合并与排序

然后开始合并子数组,同时也排序这些数组:

  1. 合并 [34][-42] 得到 [-42, 34]
  2. 合并 [0][15] 得到 [0, 15]
  3. [-9] 已经是单个元素,因此它暂时保持不变
  4. 合并 [27][51] 得到 [27, 51]
  5. 合并 [3][12] 得到 [3, 12]
  6. [18] 已经是单个元素,因此它暂时保持不变

现在有,[-42, 34][0, 15][-9][27, 51][3, 12][18]

继续合并剩余的数组:

  1. 合并 [-42, 34][0, 15] 得到 [-42, 0, 15, 34]
  2. 合并 [-9] 和上一步中已经排序的数组 [-42, 0, 15, 34] 得到 [-42, -9, 0, 15, 34]
  3. 合并 [27, 51][3, 12] 得到 [3, 12, 27, 51]
  4. 合并 [18] 和上一步中的 [3, 12, 27, 51] 得到 [3, 12, 18, 27, 51]

现在,我们有 [-42, -9, 0, 15, 34][3, 12, 18, 27, 51]

Step 3: 最终合并

  1. 比较 [-42, -9, 0, 15, 34][3, 12, 18, 27, 51] 中的第一个元素,取出 -42
  2. 比较 -93,接下来选择 -9
  3. 接着是 0
  4. 之后 315 小,所以选择 3
  5. 继续这样比较并取出元素直到两个数组都空了。

最终合并排序后的数组是:

[-42, -9, 0, 3, 12, 15, 18, 27, 34, 51]

这样,通过一系列的分割、合并和排序步骤,我们完成了复杂整数数组的归并排序。

三、经典面试题

  • 面试题:
    假设你有一个整数数组,数组中的一些数可能重复。请实现一个归并排序的版本,该版本除了排序数组外,还能统计数组中每个数字出现的次数。最后输出排序后的数组以及一个包含每个数字及其出现次数的映射(Map)。

这个问题的复杂之处在于,在对数组进行排序的同时,你还需要跟踪每个数字的出现次数。

  • 解法:

以下是一个可能的Java解决方案,其中包含了详细的注释来解释各部分代码的功能。

首先,实现一个标准的归并排序过程:

public static void mergeSort(int[] array, int left, int right, Map<Integer, Integer> frequencyMap) {
    if (left < right) {
        // 找到中间位置,分割数组为两部分
        int mid = left + (right - left) / 2;
        // 对两个子数组递归调用归并排序
        mergeSort(array, left, mid, frequencyMap);
        mergeSort(array, mid + 1, right, frequencyMap);
        // 合并两个已排序的子数组
        merge(array, left, mid, right, frequencyMap);
    }
}
private static void merge(int[] array, int left, int mid, int right, Map<Integer, Integer> frequencyMap) {
    // 创建临时数组存放左右两边的数组
    int[] leftArray = new int[mid - left + 1];
    int[] rightArray = new int[right - mid];
    // 填充这两个临时数组
    for (int i = 0; i < leftArray.length; ++i)
        leftArray[i] = array[left + i];
    for (int j = 0; j < rightArray.length; ++j)
        rightArray[j] = array[mid + 1 + j];
    // 合并临时数组回原数组中
    int i = 0, j = 0;
    int k = left;
    while (i < leftArray.length && j < rightArray.length) {
        if (leftArray[i] <= rightArray[j]) {
            array[k] = leftArray[i];
            updateFrequencyMap(frequencyMap, leftArray[i]);
            i++;
        } else {
            array[k] = rightArray[j];
            updateFrequencyMap(frequencyMap, rightArray[j]);
            j++;
        }
        k++;
    }
    // 处理剩下的元素
    while (i < leftArray.length) {
        array[k] = leftArray[i];
        updateFrequencyMap(frequencyMap, leftArray[i]);
        i++;
        k++;
    }
    while (j < rightArray.length) {
        array[k] = rightArray[j];
        updateFrequencyMap(frequencyMap, rightArray[j]);
        j++;
        k++;
    }
}
private static void updateFrequencyMap(Map<Integer, Integer> frequencyMap, int key) {
    frequencyMap.put(key, frequencyMap.getOrDefault(key, 0) + 1);
}

在上面的代码中,我们用 mergeSort 方法递归地将数组分成更小的片段,直到片段只包含单一元素。然后,通过 merge 方法将这些片段合并回去,从而得到排序好的数组。我们也增加了 updateFrequencyMap 辅助方法,用于在合并过程中更新数字的出现次数。

这样的实现能够在排序过程中,忽略额外的遍历,即时地构建频率映射。调用 mergeSort 函数时,应传入一个空的 Map 用来存放数字频率,例如:

Map<Integer, Integer> frequencyMap = new HashMap<>();
int[] array = {4, 5, 6, 4, 6, 3};
mergeSort(array, 0, array.length - 1, frequencyMap);
System.out.println(Arrays.toString(array));         // 输出排序后的数组
System.out.println(frequencyMap.toString());        // 输出数字及其频率的映射

这个方法在一遍归并排序过程中,就填入了数字频率的信息,既高效又简洁。面试时,这种能力展示出你有能力处理复杂问题,并能优雅地在现有算法框架中添加额外功能。

四、思考

updateFrequencyMap 方法中,为什么要使用 frequencyMap.getOrDefault(key, 0) + 1 而不是直接使用 frequencyMap.get(key) + 1


总结

博主持续输出排序算法,更加偏向Java方面,对于面试题的学习,希望同学们有Java基础,才能更好的吸收知识。

感谢大家抽出宝贵的时间来阅读博主的博客,新人博主,感谢大家关注点赞,祝大家未来的学习工作生活一帆风顺,加油!!!

目录
相关文章
|
2月前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
70 1
|
2月前
|
机器学习/深度学习 算法 数据挖掘
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构。本文介绍了K-means算法的基本原理,包括初始化、数据点分配与簇中心更新等步骤,以及如何在Python中实现该算法,最后讨论了其优缺点及应用场景。
165 4
|
16天前
|
存储 人工智能 算法
【C++数据结构——内排序】二路归并排序(头歌实践教学平台习题)【合集】
本关任务是实现二路归并算法,即将两个有序数组合并为一个有序数组。主要内容包括: - **任务描述**:实现二路归并算法。 - **相关知识**: - 二路归并算法的基本概念。 - 算法步骤:通过比较两个有序数组的元素,依次将较小的元素放入新数组中。 - 代码示例(以 C++ 为例)。 - 时间复杂度为 O(m+n),空间复杂度为 O(m+n)。 - **测试说明**:平台会对你编写的代码进行测试,提供输入和输出示例。 - **通关代码**:提供了完整的 C++ 实现代码。 - **测试结果**:展示代码运行后的排序结果。 开始你的任务吧,祝你成功!
32 10
|
16天前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
39 2
|
1月前
|
存储 运维 监控
探索局域网电脑监控软件:Python算法与数据结构的巧妙结合
在数字化时代,局域网电脑监控软件成为企业管理和IT运维的重要工具,确保数据安全和网络稳定。本文探讨其背后的关键技术——Python中的算法与数据结构,如字典用于高效存储设备信息,以及数据收集、异常检测和聚合算法提升监控效率。通过Python代码示例,展示了如何实现基本监控功能,帮助读者理解其工作原理并激发技术兴趣。
57 20
|
2月前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
2月前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
2月前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
132 23
|
2月前
|
算法
数据结构之蜜蜂算法
蜜蜂算法是一种受蜜蜂觅食行为启发的优化算法,通过模拟蜜蜂的群体智能来解决优化问题。本文介绍了蜜蜂算法的基本原理、数据结构设计、核心代码实现及算法优缺点。算法通过迭代更新蜜蜂位置,逐步优化适应度,最终找到问题的最优解。代码实现了单链表结构,用于管理蜜蜂节点,并通过适应度计算、节点移动等操作实现算法的核心功能。蜜蜂算法具有全局寻优能力强、参数设置简单等优点,但也存在对初始化参数敏感、计算复杂度高等缺点。
78 20
|
2月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
84 1