排序

一、什么是排序?

在计算机里,排序的意思就是把一堆杂乱无章的数据,整理得整整齐齐、有条有理 。这是计算机在处理各种问题时,常常要做的一项工作。

打个比方,就好像你有一堆打乱顺序的纸牌,排序就是要按照一定的规则,比如从大到小或者从小到大,把这些纸牌重新整理好。

而排序算法呢,就是具体用来整理这堆数据的方法,它规定了按照什么样的特定方式,去把那些杂乱的数据变得有序。就好比整理纸牌时,你可以一张一张地比较大小,然后把它们摆好,这就是一种整理纸牌的 “算法” 。

二、为什么要排序?

排序在生活和计算机领域都非常有用,比如:

在生活中

在计算机领域

排序就像是给东西排排队,让它们变得有条理。在生活中,排序可以帮助我们更快地找到东西,节省时间,让环境看起来更整洁,方便管理。在计算机领域,排序可以让计算机更快地处理数据,提高效率,提升用户体验。总之,排序是为了让事情变得更简单、更高效。

三、在计算机中有哪些排序算法?

在计算机中排序的方法分为以下几种类型:内部排序、外部排序、比较排序、非比较排序、稳定排序、不稳定排序、递归排序、非递归排序。

我们这里主要学习比较排序和非比较排序

类型算法
比较排序冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序
非比较排序基数排序、计数排序、桶排序

3.1 冒泡排序

1.基本思想

冒泡排序的基本思想是基于相邻元素的比较和交换 ,通过多次重复比较相邻元素并交换位置,使值较大(或较小)的元素逐渐 “冒泡” 到数组的末尾(或开头),从而实现数组的排序。

2.算法描述

3.动画演示

冒泡排序(Bubble Sort)

4.实现过程

5.代码示例:

题目1:基本冒泡排序

题目描述:给定一个整数数组,使用冒泡排序算法对其进行排序,并输出排序后的数组。 输入: • 第一行是一个整数 n ,表示数组的长度( 1 ≤ n ≤ 1000 )。 • 第二行是 n 个整数,表示待排序的数组。 输出: • 输出排序后的数组,每个元素之间用空格隔开。 示例输入: 7 64 34 25 12 22 11 90 示例输出: 11 12 22 25 34 64 90

参考代码:

题目2:优化冒泡排序

题目描述:给定一个整数数组,使用优化后的冒泡排序算法对其进行排序。优化后的冒泡排序在某一轮中如果没有发生任何交换,则提前结束排序。输出排序后的数组以及实际进行的排序轮数。 输入: • 第一行是一个整数 n ,表示数组的长度( 1 ≤ n ≤ 1000 )。 • 第二行是 n 个整数,表示待排序的数组。 输出: • 第一行输出排序后的数组,每个元素之间用空格隔开。 • 第二行输出实际进行的排序轮数。 示例输入: 7 64 34 25 12 22 11 90 示例输出: 11 12 22 25 34 64 90 6

参考代码:

3.2 选择排序

1.基本思想

选择排序是从待排序的区间中选出最小(或最大)元素,并将该元素与该区间的第1个元素交换位置。这样通过n-1次选择未排序部分的最小(或最大)元素,将其放在正确位置从而达到对整个序列进行排序的效果。


选择排序的做法是这样的:在那堆等着被排序的数里面,每次都挑出最小(要是想从大到小排,就挑最大)的那个数,然后把这个挑出来的数和这堆数最前面的那个数交换一下位置。

就这样,一次又一次地从还没排好序的那些数里找最小(或最大)的数,一共找 n−1 次(这里的 n 是这堆数的总数)。每找一次,就把找到的最小(或最大)数放到它该在的位置上。经过这么多次操作后,这整个数列就排好序啦。


2.算法描述(以升充为例)

3.动画演示

选择排序(Selection Sort)

4.实现过程

5.代码示例

核心代码:

题目1:基本选择排序 题目描述:给定一个整数数组,使用选择排序算法对其进行排序,并输出排序后的结果。 输入: • 第一行是一个整数 n ,表示数组的长度( 1 ≤ n ≤ 1000 )。 • 第二行是 n 个整数,表示待排序的数组。 输出: • 输出排序后的数组,每个元素之间用空格隔开。

参考代码:

 

3.3 插入排序

1.基本思想

将一个记录插入到已排序好的有序表中,从而得到一个新的、记录数增加1的有序表。类似于整理扑克牌的过程,假设手中已经有一部分牌是按顺序排列好的,每次从剩余未整理的牌中拿起一张,然后将其插入到已排好序的牌中的合适位置,使得插入后的牌仍然保持有序。

2.算法描述

  1. 初始化:

    • 将数组分为两部分:已排序部分和未排序部分。初始时,已排序部分只包含第一个元素,未排序部分为剩下的元素。

  2. 插入过程:

    • 从未排序部分取出第一个元素(称为待插入元素)。

    • 在已排序部分从后向前查找,找到待插入元素的合适位置。

    • 将已排序部分中所有比待插入元素大的元素向后移动一位,为待插入元素腾出空间。

    • 将待插入元素插入到找到的位置。

  3. 重复上述过程:

    • 重复上述步骤,直到未排序部分为空,整个数组有序。

3.动画演示

插入排序(Insertion Sort)

4.实现过程

假设我们有一个数组 arr ,长度为 n ,下面是插入排序的详细实现过程:

  1. 外层循环:控制排序的轮数,从第二个元素开始,到第 n 个元素。

  2. 内层循环:在每一轮中,从未排序部分取出第一个元素,然后在已排序部分从后向前查找合适的位置。

  3. 移动元素:将已排序部分中所有比待插入元素大的元素向后移动一位。

  4. 插入元素:将待插入元素插入到找到的位置。

5.代码示例

核心代码:

题目1:基本插入排序 题目描述:给定一个整数数组,使用插入排序算法对其进行排序,并输出排序后的结果。 输入: • 第一行是一个整数 n ,表示数组的长度( 1 ≤ n ≤ 1000 )。 • 第二行是 n 个整数,表示待排序的数组。 输出: • 输出排序后的数组,每个元素之间用空格隔开。

参考代码:

 

3.4 计数排序

1.基本思想

计数排序是一种非比较类排序算法,基本思想是利用数组下标来统计元素出现的次数,从而实现排序。它适用于知道待排序数据范围且数据范围相对较小的情况。

具体来说,先找出待排序数组中的最大值和最小值,确定计数数组的大小。然后遍历待排序数组,以数组元素的值为索引,在计数数组的对应位置记录该元素出现的次数。最后,根据计数数组,将元素按顺序放回原数组或新数组,完成排序。计数排序的核心在于“计数”和“定位”。

2.算法描述

  1. 确定范围:

    • 找出待排序数组中的最大值和最小值,确定数组的范围。

  2. 初始化计数数组:

    • 创建一个大小为 k 的计数数组( count ),其中 k 是最大值与最小值的差加1。初始化计数数组的所有元素为0。

  3. 统计每个元素的出现次数:

    • 遍历待排序数组,对于每个元素,将其在计数数组中对应位置的值加1。这一步记录了每个元素在待排序数组中出现的次数。

  4. 生成有序数组

    • 创建一个新的数组(也可以在原数组上覆盖)用于存储排序后的结果。从计数数组的第一个元素开始遍历,对于计数数组中的每个非零元素 ,将计数数组下标按其值的次数依次放入结果数组中。这样,结果数组就是按升序排列的。

3.动画演示

计数排序

4.实现过程

假设有一个数组 arr = {4, 2, 2, 5, 3, 3, 1} ,计数排序过程:

5.代码示例

题目1:基本计数排序 题目描述:给定一个整数数组,使用计数排序算法对其进行排序,并输出排序后的结果。 输入: • 第一行是一个整数 n ,表示数组的长度( 1 ≤ n ≤ 1000 )。 • 第二行是 n 个整数,表示待排序的数组。数组中的每个元素都在 0 到 100 之间。 输出: • 输出排序后的数组,每个元素之间用空格隔开。

参考代码:


以下排序算法为提高级内容


3.5 归并排序

3.5.1 基本思想:分治与递归

3.5.2 算法描述

  1. 分解:

    • 找到数组的中间位置 mid,将数组 arr 分成两个子数组,即左子数组 leftarr[0]arr[mid],右子数组 rightarr[mid + 1]arr[n - 1],其中 n 是数组 arr 的长度。

    • 对左子数组和右子数组分别递归调用归并排序函数,持续分解子数组,直到子数组长度为 1。

  2. 合并:

    • 当子数组长度为 1 时,开始进行合并。准备两个指针,分别指向左子数组和右子数组的起始位置。

    • 创建一个临时数组 temp 用于存储合并后的结果。比较两个子数组当前指针指向的元素,将较小(或较大,取决于排序顺序,这里以升序为例)的元素放入 temp 数组,并将对应子数组的指针后移一位。

    • 重复上述比较和放入的操作,直到其中一个子数组的元素全部放入 temp 数组。

    • 将另一个子数组剩余的元素直接复制到 temp 数组的末尾。

    • 最后,将 temp 数组中的内容复制回原数组 arr 对应的位置,完成一次合并。随着递归的返回,不断进行合并操作,最终整个数组有序。

3.5.3 动画演示

归并排序(Merge Sort)

3.5.4 实现过程

归并排序主要分为两个大的阶段:分解阶段和合并阶段。

  1. 分解阶段

    • 步骤一:确定分解点

      • 对于给定的待排序数组 arr,首先计算数组的中间位置 mid。例如,数组长度为 n,则 mid = left + (right - left) / 2,其中 left 是数组起始索引,初始为 0right 是数组末尾索引,初始为 n - 1。这个计算方式可以避免 left + right 可能产生的溢出问题。

    • 步骤二:递归分解

      • mid 为界,将数组分为左子数组 arr[left...mid] 和右子数组 arr[mid + 1...right]。然后对左子数组和右子数组分别递归调用归并排序函数。这意味着每个子数组又会经历同样的分解过程,不断将子数组一分为二,直到子数组的长度为 1。因为长度为 1 的数组天然是有序的,所以分解的递归过程到这里结束。

      • 例如,对于数组 [12, 11, 13, 5, 6, 7],初始时 left = 0right = 5mid = 2。左子数组为 [12, 11, 13],右子数组为 [5, 6, 7]。接着对 [12, 11, 13] 继续分解,mid = 1,左子数组变为 [12],右子数组变为 [11, 13],以此类推,直到每个子数组只有一个元素。

  2. 合并阶段

    • 步骤一:初始化临时数组和指针

      • 当子数组长度为 1 时,开始进行合并。对于要合并的两个子数组(假设左子数组为 leftSub,右子数组为 rightSub),先创建一个临时数组 temp 来存储合并后的结果。同时,初始化两个指针,一个指向左子数组的起始位置(设为 i),另一个指向右子数组的起始位置(设为 j),还需要一个指针(设为 k)指向临时数组 temp 要填充的起始位置。

    • 步骤二:比较与填充

      • 比较两个子数组当前指针指向的元素。如果左子数组当前元素小于等于右子数组当前元素(以升序排序为例),则将左子数组当前元素放入临时数组 temp 中,并将左子数组指针 i 后移一位;否则,将右子数组当前元素放入 temp 中,并将右子数组指针 j 后移一位。然后将 temp 数组的指针 k 后移一位。

      • 例如,假设左子数组 leftSub = [11, 13],右子数组 rightSub = [5, 6, 7]。首先比较 115,因为 5 < 11,所以将 5 放入 temp 数组,j 后移指向 6k 后移。接着比较 116,因为 6 < 11,将 6 放入 temp 数组,j 后移指向 7k 后移。再比较 117,因为 7 < 11,将 7 放入 temp 数组,j 后移,此时右子数组遍历完。

    • 步骤三:处理剩余元素

      • 当其中一个子数组的元素全部放入 temp 数组后,另一个子数组可能还有剩余元素。直接将剩余元素依次复制到 temp 数组的末尾。例如上述例子中,左子数组 [11, 13] 还有 1113 未放入 temp,将它们依次放入,此时 temp 数组为 [5, 6, 7, 11, 13]

    • 步骤四:复制回原数组

      • 最后,将临时数组 temp 中的内容复制回原数组 arr 对应的位置,完成一次合并。随着递归的返回,不断进行这样的合并操作,最终整个数组有序。例如,将 [5, 6, 7, 11, 13] 复制回原数组中对应左右子数组的位置,逐步将整个数组排序。

3.5.5 示例

3.6 快速排序

1.基本思想:分治与递归

2.算法描述

3.动画演示

快速排序

4.实现过程

5.示例

3.7 堆排序

1.基本思想

2.算法描述

3.动画演示

堆排序(Heap Sort)

4.实现过程

假设我们有一个数组 arr = {12, 11, 13, 5, 6, 7} ,我们来逐步实现堆排序:

  1. 构建最大堆:

  1. 调整堆:

  1. 重复调整:

5.示例

3.8 桶排序

1.基本思想

桶排序(Bucket Sort)是一种非比较类排序算法,基本思想是将待排序的数据分到不同的桶中,每个桶内的数据再单独进行排序,最后按照桶的顺序依次取出桶内已排序的数据,从而得到一个有序的序列。这种方法适用于数据分布较为均匀且范围相对固定的情况。

2.算法描述

3.动画演示

桶排序(Bucket Sort)

4.实现过程

假设我们有一个数组 arr = {12, 11, 13, 5, 6, 7, 999, 1, 1000} ,我们来逐步实现桶排序:

1. 确定桶的数量和范围

假设:我们使用10个桶,每个桶的范围为 [0, 100) , [100, 200) , ..., [900, 1000) 。计算:找出数组中的最大值和最小值,确定桶的数量和范围。

2. 分配元素到桶

3.对每个桶进行排序

排序:对每个桶中的元素进行排序,可以使用插入排序或其他排序算法。

4.合并桶中的元素

合并:按顺序将所有桶中的元素依次取出,合并成一个有序数组。

5.完整代码实现

3.9 基数排序

1.基本思想 基数排序(Radix Sort)是一种非比较排序算法,它基于数字的每一位进行排序。其核心思想是将整数按位数切割成不同的数字,然后按每个位数分别进行排序。从最低位到最高位依次对所有数字进行排序,经过多轮排序后,整个序列就会变为有序。例如,对于一组三位数,先按个位数字排序,再按十位数字排序,最后按百位数字排序,最终得到有序数组。

2.算法描述

3.动画演示

基数排序(Radix Sort)

4.实现过程

假设我们有一个数组 arr = {170, 45, 75, 90, 802, 24, 2, 66} ,我们来逐步实现基数排序:

  1. 确定最大值的位数:

    • 找出数组中的最大值 802 ,它有3位。

  2. 按位排序:

    • 从最低位(个位)开始,依次对每一位进行排序。

    • 使用计数排序来处理每一位上的数字。

  3. 个位排序:

    • 创建10个桶(0-9),将每个数字的个位放入对应的桶中。

    • 将桶中的数字依次取出,放回数组。

  4. 十位排序:

    • 创建10个桶(0-9),将每个数字的十位放入对应的桶中。

    • 将桶中的数字依次取出,放回数组。

  5. 百位排序:

    • 创建10个桶(0-9),将每个数字的百位放入对应的桶中。

    • 将桶中的数字依次取出,放回数组。

5.示例

3.10 各种排序方法的比较

 排序算法最佳时间复杂度平均时间复杂度最坏时间复杂度空间复杂度稳定性适用场景
比较冒泡排序O(n)O(n^2)O(n^2)O(1)稳定小规模数据或部分有序的数据
比较选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定小规模数据
比较插入排序O(n)O(n^2)O(n^2)O(1)稳定小规模数据或部分有序的数据
比较归并排序O(n\logn)O(n\logn)O(n\logn)O(n)稳定大规模数据,需要稳定排序
比较快速排序O(n\logn)O(n\logn)O(n^2)O(\logn)不稳定大规模数据,平均性能优秀
比较堆排序O(n\logn)O(n\logn)O(n\logn)O(1)不稳定大规模数据,原地排序
非比较计数排序O(n+k)O(n+k)O(n+k)O(k)稳定数据范围有限且分布均匀
非比较桶排序O(n+k)O(n+k)O(n^2)O(n+k)稳定数据范围有限且分布均匀
非比较基数排序O(nk)O(nk)O(nk)O(n+k)稳定数据范围有限且分布均匀,多位数