排序的基本概念
-
内部排序和外部排序:
- 内部排序:整个排序过程完全在内存中进行。
- 外部排序:数据量较大需要借助外部存储设备才能完成。
-
主关键字和次关键字:排序通常依据一个或多个关键字进行,主关键字是主要排序依据,次关键字是次要排序依据。
-
排序的稳定性:
- 稳定排序:对于相同的元素来说,在排序之前和之后的顺序是一样的。
- 不稳定排序:排序后相同元素的顺序可能会发生变化。
排序算法的分类
排序算法可以根据其实现方式和特点进行分类,常见的排序算法包括:
-
插入类排序:
插入排序(Insertion Sort)是一种简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,找到相应位置并插入时,不需要移动元素,只需将要插入的元素移动到前面即可。
插入排序的工作原理
-
初始状态:假设第一个元素是已排序的(其实只有一个元素,当然已排序)。
-
从第二个元素开始:取出下一个元素,在已经排序的元素序列中从后向前扫描。
-
找到合适位置并插入:如果该元素(已取出)大于新元素,则将该元素移到下一位置。重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。将新元素插入到该位置后。
-
重复步骤2~4:对剩余元素继续上述过程,直到所有元素均已排序。
插入排序的示例
假设我们要对数组 {5, 2, 9, 1, 5, 6}
进行插入排序:
- 初始数组:
{5, 2, 9, 1, 5, 6}
- 第一步:将第二个元素
2
插入到已排序部分(即第一个元素5
)的适当位置,得到:{2, 5, 9, 1, 5, 6}
- 第二步:将第三个元素
9
插入到已排序部分(即前两个元素2, 5
)的适当位置,由于9
大于5
和2
,所以保持不变:{2, 5, 9, 1, 5, 6}
- 第三步:将第四个元素
1
插入到已排序部分(即前三个元素2, 5, 9
)的适当位置,得到:{1, 2, 5, 9, 5, 6}
- 第四步:将第五个元素
5
插入到已排序部分(即前四个元素1, 2, 5, 9
)的适当位置,得到:{1, 2, 5, 5, 9, 6}
- 第五步:将第六个元素
6
插入到已排序部分(即前五个元素1, 2, 5, 5, 9
)的适当位置,得到:{1, 2, 5, 5, 6, 9}
插入排序的时间复杂度
- 最好情况:O(n)(当输入数组已经是排序好的情况下,每次插入操作都不需要移动元素)
- 最坏情况:O(n^2)(当输入数组是逆序的情况下,每次插入操作都需要将已排序部分的所有元素向后移动一位)
- 平均情况:O(n^2)
插入排序的空间复杂度
插入排序是原地排序算法,因此其空间复杂度为O(1)。
插入排序的适用场景
- 插入排序适用于少量数据的排序,其时间复杂度为O(n^2),在数据量较大时效率较低。
- 对于部分已排序的数组,插入排序的效率较高,因为每次插入操作可以利用前面的有序部分,从而减少比较和移动的次数。
插入排序的种类
- 直接插入排序:将每个新元素插入到已排序序列的适当位置。
- 折半插入排序:利用折半查找法确定新元素的插入位置,减少比较次数。
- 希尔排序:又称缩小增量排序法,通过多次分组和插入排序来逐步减少数据的有序性,提高排序效率。
交换类排序:
交换类排序的主要算法
-
冒泡排序
- 算法思想:冒泡排序通过反复扫描待排序记录序列,在扫描的过程中顺次比较相邻的两元素的大小,若逆序就交换位置。每一轮比较后,最大(或最小)的元素会像气泡一样“浮”到数组的一端。
- 算法描述:对于包含n个元素的数组,进行n-1轮比较。在每一轮比较中,从数组的第一个元素开始,依次比较相邻的两个元素。如果它们的顺序不符合排序要求(例如在升序排序中,前面的元素大于后面的元素),则交换这两个元素的位置。
- 优化:冒泡排序可以进行优化,比如可以在交换的地方加一个标记,如果那一趟排序没有交换元素,说明这组数据已经有序,不用再继续下去。或者可以记下最后一次交换的位置,后边没有交换,必然是有序的,然后下一次排序从第一个比较到上次记录的位置结束即可。
- 时间和空间复杂度:当待排序序列是有序序列时,只需进行n次比较,时间复杂度为O(n);当待排序序列为逆有序序列时,要进行(n²-1)/2次比较,即时间复杂度为O(n²);故平均时间复杂度为O(n²)。排序过程除了需要一个临时变量之外,无需其他的空间,故空间复杂度为O(1)。
-
快速排序
- 算法思想:快速排序通过选择一个目标桩(通常是第一个元素),将小于或等于目标桩的放到目标桩的左边,大于目标桩的放到目标桩的右边。然后根据目标桩将序列分为左边序列和右边序列,再对左边序列和右边序列分别重复上述过程。
- 算法步骤:假设待划分序列为r[left], r[left+1], ..., r[right],具体实现过程可以设两个指针i和j,它们的初值分别是left和right。首先将基准记录r[left]移至变量x中,然后反复进行以下两步,直到i和j相遇:
- 从右往左扫描,找到第一个不大于x的元素,将其与i所指元素交换。
- 从左往右扫描,找到第一个不小于x的元素,将其与j所指元素交换。
- 重复上述步骤,直到i和j相遇,此时将x放到i(或j)的位置上。
- 时间和空间复杂度:当待排序序列是有序序列时,时间复杂度为O(n²);当为逆序序列时,需要时间复杂度为O(nlog₂n);故平均时间复杂度为O(nlog₂n)。排序过程当中利用递归实现,故空间复杂度为O(log₂n)。
交换类排序的特点
- 基于比较和交换:交换类排序算法都是基于比较和交换元素位置来进行排序的。
- 稳定性:冒泡排序是一种稳定的排序方法,即如果两个元素相等,它们在排序后的相对位置不会改变。而快速排序则不是稳定的排序方法。
- 适用性:冒泡排序简单直观但效率较低,适用于小规模数据的排序。快速排序则以其高效的平均性能在实际应用中得到了广泛应用,特别是在处理大规模数据时表现出色。
选择类排序:
选择类排序是一类基于选择元素进行排序的算法。其核心思想是:在待排序的元素序列中,每次选择最小(或最大)的元素,将其放在已排序序列的最前面(或最后面),然后剩余的元素构成新的待排序列,依次类推,直到待排序元素序列中没有待排元素。以下是关于选择类排序的详细介绍:
选择类排序的主要算法
-
简单选择排序
- 算法思想:简单选择排序通过n-i次关键字的比较,从n-i+1个记录中选出关键字最小的记录,并和第i个记录进行交换。共需进行i-1趟比较,直到所有记录排序完成为止。
- 算法步骤:
- 从待排序序列中选出最小(或最大)元素,存放在序列的起始位置。
- 然后再从剩余元素中选出最小(或最大)元素,然后放到已排序序列的末尾。
- 以此类推,直到所有元素均排序完毕。
- 时间和空间复杂度:简单选择排序的时间复杂度为O(n²),其中n是待排序元素的个数。这是因为每一趟都需要遍历剩余元素以找到最小(或最大)元素。空间复杂度为O(1),因为排序过程中只需要一个常量级的额外空间用于交换元素。
-
堆排序
- 算法思想:堆排序是简单选择排序算法的改进,它利用二叉树的性质对元素进行排序。在堆排序中,将完全二叉树从上到下、从左到右依次编号,如果每一个双亲节点元素值大于(或小)等于该孩子节点的元素值,则根据编号构成的元素序列就是一个大(小)顶堆。堆排序的过程包括创建堆和调整堆。
- 创建堆:假设待排序元素有n个,依次放在数组a中,第1个元素的关键字a[1]表示二叉树的根节点,剩下的元素a[2...n]依次与完全二叉树中的编号一一对应。建立大顶堆的算法思想是从位于元素序列中最后一个非叶子结点(第⌊n/2⌋元素)开始,逐层比较调整元素的位置使其满足大顶堆的性质,直至根节点为止。
- 调整堆:输出堆顶元素(即最大元素)之后,将堆中剩余n-1个元素重新调整成新堆,然后输出新的堆顶元素,重复此过程直到堆中没有元素为止。输出的元素就是一个有序序列。
- 时间和空间复杂度:堆排序的时间复杂度为O(nlog₂n),其中n是待排序元素的个数。这是因为堆排序的主要时间耗费在建立堆和调整堆上,一个深度为h、元素个数为n的堆,其调整算法的比较次数最多是2(h-1)次,建立一个堆最多比较次数为4n。一个完整的堆排序过程总共比较次数少于2nlog₂n。空间复杂度为O(1),因为排序过程中只需要一个常量级的额外空间用于辅助操作(如交换元素)。
选择类排序的特点
- 基于选择:选择类排序算法都是基于选择元素来进行排序的,即每次从待排序序列中选择最小(或最大)元素进行排序。
- 不稳定性:简单选择排序和堆排序都是不稳定的排序方法,即如果两个元素相等,它们在排序后的相对位置可能会发生变化。
- 适用性:简单选择排序适用于小规模数据的排序或作为其他排序算法的辅助手段;堆排序则适用于大规模数据的排序,特别是当需要快速找到最大(或最小)元素时表现出色。
归并类排序:
归并类排序,特别是归并排序(Merge Sort),是一种高效的排序算法,它采用了分治法(Divide and Conquer)的策略来排序数据。以下是关于归并类排序的详细介绍:
归并排序的基本概念
归并排序是一个递归的算法,其基本思想是将一个待排序的数组分成两个小数组,分别对这两个小数组进行排序,然后将这两个已排序的小数组合并成一个最终的已排序数组。归并排序的时间复杂度是O(n log n),其中n是待排序元素的个数,并且它是一个稳定的排序算法。
归并排序的算法流程
归并排序主要分为两个步骤:分解和合并。
- 分解:将待排序的数组分割成两个子数组。如果数组的长度小于或等于1,则直接返回数组(因为它已经是排序的)。否则,将数组从中间分割成两个子数组,并递归地对这两个子数组进行归并排序。
- 合并:创建一个新的数组用于存储合并后的结果。使用两个指针分别指向两个子数组的起始位置,比较两个指针指向的元素,将较小的元素放入新数组,并移动指针。继续从两个子数组中取出较小的元素,直到其中一个子数组被完全处理。将未处理的另一个子数组中的剩余元素全部添加到新数组中。最后,将新数组的内容复制回原数组中。
归并排序的示例
假设我们要对数组{5, 2, 9, 1, 5, 6}
进行归并排序:
- 分解:首先将数组分解成两部分,即
{5, 2, 9, 1}
和{5, 6}
。 - 继续分解:对
{5, 2, 9, 1}
继续分解,得到{5, 2}
和{9, 1}
。对{5, 2}
继续分解,得到{5}
和{2}
。此时,所有子数组的长度都不大于1,分解过程结束。 - 合并:开始合并过程。首先合并
{5}
和{2}
,得到{2, 5}
。然后合并{9}
和{1}
,得到{1, 9}
。接着合并{2, 5}
和{1, 9}
,得到{1, 2, 5, 9}
。最后,合并{1, 2, 5, 9}
和{5, 6}
,得到最终排序结果{1, 2, 5, 5, 6, 9}
。
归并排序的性能分析
- 时间复杂度:归并排序的时间复杂度为O(n log n),其中n是待排序元素的个数。这是因为每次分解都将数组分成两半,而合并过程需要遍历整个数组。
- 空间复杂度:归并排序的空间复杂度较高,为O(n)。这是因为需要创建一个新的数组用于存储合并后的结果。然而,归并排序是原地排序算法的一种变体(在合并过程中需要额外的空间),除了递归调用栈的空间外,不需要额外的空间(但在实际应用中,通常会使用额外的数组来存储合并结果,因此空间复杂度为O(n))。
- 稳定性:归并排序是一种稳定的排序算法。如果两个元素相等,它们在排序后的相对位置不会发生变化。
归并排序的优缺点
-
优点:
- 归并排序具有稳定的排序性能,时间复杂度始终为O(n log n)。
- 归并排序是稳定的排序算法,可以保持相等元素的相对顺序。
-
缺点:
- 归并排序的空间复杂度较高,需要额外的空间来存储合并结果(但在实际应用中,可以通过一些技巧来减少空间使用)。
- 归并排序是递归算法,对于递归深度较大的情况,可能会导致栈溢出的问题(尽管在现代计算机中,这通常不是问题)。
基数类排序:
基数排序的基本概念
基数排序又称桶排序(Bucket Sort)或分配式排序,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。具体来说,基数排序通过键值的各个位的值,将要排序的元素分配至某些“桶”中,以达到排序的作用。
基数排序的算法流程
基数排序有两种主要方法:最高位优先法(MSD, Most Significant Digit first)和最低位优先法(LSD, Least Significant Digit first)。以下是LSD法的详细流程:
- 计算最大数位数:首先找出待排序数组中最大数的位数。
- 统一数位长度:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
- 按位排序:从最低位开始,依次进行一次排序。排序时,根据当前位的数值,将元素分配到对应的桶中。
- 合并桶中元素:将各个桶中的元素依次取出,合并成一个新的有序数组。
- 重复步骤3和4:对更高位进行排序,直到最高位排序完成。
MSD法的流程与LSD法类似,但它是从最高位向最低位依次按位进行排序。在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。
基数排序的示例
假设初始序列为:Array{3, 1, 13, 24, 36, 23, 21, 44, 25, 19}。
-
按个位数排序:
- 个位数为0~9,可以视为10个桶。
- 将Array中的元素按个位数分配到对应的桶中。
- 从桶中依次取出元素,得到新的序列:{1, 21, 3, 13, 23, 24, 44, 25, 36, 19}。
-
按十位数排序:
- 将上一步得到的序列按十位数分配到对应的桶中。
- 从桶中依次取出元素,得到最终的排序结果:{1, 3, 13, 19, 21, 23, 24, 25, 36, 44}。
基数排序的性能分析
- 时间复杂度:基数排序的时间复杂度一般为O(d(n+k)),其中d是最大数的位数,n是数组的长度,k是桶的数量。对于大多数情况,基数排序的效率高于传统的比较排序算法。
- 空间复杂度:基数排序需要额外的存储空间来创建桶,其空间复杂度为O(n+k)。在数据量很大的情况下,这可能会成为一个问题。但可以通过优化基数和桶的数量来降低空间复杂度。
- 稳定性:基数排序是一种稳定的排序算法,即相等的元素在排序后的序列中保持原有的顺序。
基数排序的优缺点
-
优点:
- 基数排序是非比较型排序算法,适用于大范围数据排序,打破了计数排序的限制。
- 基数排序的时间复杂度较低,对于大数据量排序有较好的性能表现。
- 基数排序是稳定的排序算法,可以保持相等元素的相对顺序。
-
缺点:
- 基数排序需要额外的存储空间来创建桶,空间复杂度较高。
- 基数排序主要适用于整数排序,对于浮点数或字符串等其他类型的数据,需要进行额外的转换或处理。
外部排序:
外部排序是指对大规模数据集合进行排序的方法,这些数据集由于规模庞大,无法一次性加载到内存中,因此需要在内存和外部存储器(如磁盘)之间进行多次数据交换,以达到排序整个文件的目的。以下是对外部排序的详细介绍:
外部排序的原理
外部排序的基本思想是将待排序的大文件分割成多个能够加载到内存中的小块,然后在内存中对这些小块进行排序,最后将排序后的块写回磁盘或其他存储介质,并合并这些块以得到最终的排序结果。这个过程通常涉及三个主要阶段:分割阶段、排序阶段和合并阶段。
- 分割阶段:将大型数据集分割成适合内存加载的块。这些块的大小通常根据内存容量来确定,以确保每个块都能完全加载到内存中。
- 排序阶段:将每个块加载到内存中,在内存中使用常规排序算法(如快速排序、归并排序等)对块进行排序。排序完成后,将排序后的块写回磁盘或其他存储介质。
- 合并阶段:将排序后的块合并成更大的块,直到整个数据集排序完成。合并过程可以使用多路归并排序等算法来实现。
常见的外部排序算法
- 多路归并排序:多路归并排序是外部排序中最常用的算法之一。它使用内部排序算法对划分的多个部分进行排序,并将排序后的结果存储到临时文件中。在最后,使用多路归并的方式将这些临时文件合并成一个有序的文件。增大归并路数可以减少归并趟数,进而减少总的磁盘I/O次数,提高排序速度。
- 置换-选择排序:置换-选择排序是另一种常见的外部排序算法。它通过在内存中维护一个大小为M的最小堆(通常称为置换选择树),并利用置换策略来选择最小元素进行排序。当最小堆中的元素被选取后,需要从同一块中选择下一个最小元素来填充该位置,以保持最小堆的性质。
外部排序的优化方法
- 减少初始归并段数量:通过优化分割策略或排序算法,可以减少初始归并段的数量,从而降低归并阶段的复杂度。
- 使用败者树等数据结构:败者树是一种用于外部排序算法中的数据结构,它能够在多路归并排序过程中快速找到当前最小的元素。使用败者树可以优化归并过程,提高排序效率。
外部排序的应用场景
外部排序常用于需要处理大量数据的场景,如数据库系统中对大型表进行排序、处理大规模日志文件或数据备份文件等。在这些场景中,由于数据量庞大,无法一次性加载到内存中,因此需要使用外部排序算法来处理。
排序算法的性能比较
-
时间复杂度:
- 平均时间性能而言,快速排序最佳,但在最坏情况下,时间性能不如堆排序和归并排序。
- 堆排序和归并排序的时间复杂度在最坏情况下均为O(n log n)。
- 插入排序、冒泡排序和简单选择排序的时间复杂度在最坏情况下为O(n^2)。
-
空间复杂度:
- 大多数排序算法的空间复杂度为O(1)(原地排序),但归并排序需要额外的空间来存储合并后的子序列。
-
稳定性:
- 基数排序、归并排序、直接插入排序、折半插入排序和冒泡排序是稳定的排序算法。
- 快速排序、简单选择排序和堆排序是不稳定的排序算法。
排序算法的应用场景
-
快速排序:适用于大多数需要高效排序的场景,特别是当数据规模较大且分布较为均匀时。
-
堆排序:适用于需要频繁插入和删除操作的数据结构,如优先队列。
-
归并排序:适用于需要稳定排序的场景,以及需要处理大数据集且内存有限的场景(外部排序)。
-
基数排序:适用于关键字较小的序列,特别是当关键字是字符串且长度较短时。
-
插入排序和冒泡排序:适用于数据规模较小或基本有序的场景。