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

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

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



前言

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


一、什么是归并排序

归并排序(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基础,才能更好的吸收知识。

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

目录
打赏
0
0
0
0
19
分享
相关文章
|
4月前
|
算法系列之数据结构-二叉树
树是一种重要的非线性数据结构,广泛应用于各种算法和应用中。本文介绍了树的基本概念、常见类型(如二叉树、满二叉树、完全二叉树、平衡二叉树、B树等)及其在Java中的实现。通过递归方法实现了二叉树的前序、中序、后序和层次遍历,并展示了具体的代码示例和运行结果。掌握树结构有助于提高编程能力,优化算法设计。
98 9
 算法系列之数据结构-二叉树
|
4月前
|
算法系列之数据结构-Huffman树
Huffman树(哈夫曼树)又称最优二叉树,是一种带权路径长度最短的二叉树,常用于信息传输、数据压缩等方面。它的构造基于字符出现的频率,通过将频率较低的字符组合在一起,最终形成一棵树。在Huffman树中,每个叶节点代表一个字符,而每个字符的编码则是从根节点到叶节点的路径所对应的二进制序列。
116 3
 算法系列之数据结构-Huffman树
|
4月前
|
算法系列之数据结构-二叉搜索树
二叉查找树(Binary Search Tree,简称BST)是一种常用的数据结构,它能够高效地进行查找、插入和删除操作。二叉查找树的特点是,对于树中的每个节点,其左子树中的所有节点都小于该节点,而右子树中的所有节点都大于该节点。
105 22
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
139 29
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
195 25
C 408—《数据结构》算法题基础篇—数组(通俗易懂)
408考研——《数据结构》算法题基础篇之数组。(408算法题的入门)
175 23
数据结构(C语言)之对归并排序的介绍与理解
归并排序是一种基于分治策略的排序算法,通过递归将数组不断分割为子数组,直到每个子数组仅剩一个元素,再逐步合并这些有序的子数组以得到最终的有序数组。递归版本中,每次分割区间为[left, mid]和[mid+1, right],确保每两个区间内数据有序后进行合并。非递归版本则通过逐步增加gap值(初始为1),先对单个元素排序,再逐步扩大到更大的区间进行合并,直至整个数组有序。归并排序的时间复杂度为O(n*logn),空间复杂度为O(n),且具有稳定性,适用于普通排序及大文件排序场景。
【C++数据结构——内排序】二路归并排序(头歌实践教学平台习题)【合集】
本关任务是实现二路归并算法,即将两个有序数组合并为一个有序数组。主要内容包括: - **任务描述**:实现二路归并算法。 - **相关知识**: - 二路归并算法的基本概念。 - 算法步骤:通过比较两个有序数组的元素,依次将较小的元素放入新数组中。 - 代码示例(以 C++ 为例)。 - 时间复杂度为 O(m+n),空间复杂度为 O(m+n)。 - **测试说明**:平台会对你编写的代码进行测试,提供输入和输出示例。 - **通关代码**:提供了完整的 C++ 实现代码。 - **测试结果**:展示代码运行后的排序结果。 开始你的任务吧,祝你成功!
176 10
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
202 2
|
8月前
|
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
182 58

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问