排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。本文将依次介绍上述八大排序算法。
插入排序是一种简单直观的排序算法,其基本思想是将待排序的元素逐个插入到已排序序列的合适位置,最终得到完整的有序序列。这个过程类似于打扑克牌时将手中的牌按照大小顺序插入已经排好序的牌堆中的过程。
下面是插入排序的基本步骤:
1. 初始状态:将第一个元素视为已排序序列,其余元素为待排序序列。
2. 遍历待排序序列:从第二个元素开始,依次将每个元素插入到已排序序列的合适位置。
3. 插入操作:对于当前待排序元素,从其前一个元素开始向前遍历已排序序列,直到找到小于或等于当前元素的位置为止。然后将当前元素插入到该位置后面,使得插入后的序列仍然保持有序。
4. 重复操作:重复步骤2和步骤3,直到所有待排序元素都被插入到已排序序列中。
下面是插入排序的示例(以升序排序为例):
假设初始序列为:[5, 2, 4, 6, 1, 3]
1. 第一轮排序:已排序序列为[5],待排序序列为[2, 4, 6, 1, 3] 将2插入到已排序序列中,得到[2, 5]。
2. 第二轮排序:已排序序列为[2, 5],待排序序列为[4, 6, 1, 3] 将4插入到已排序序列中,得到[2, 4, 5]。
3. 第三轮排序:已排序序列为[2, 4, 5],待排序序列为[6, 1, 3] 将6插入到已排序序列中,得到[2, 4, 5, 6]。
4. 第四轮排序:已排序序列为[2, 4, 5, 6],待排序序列为[1, 3] 将1插入到已排序序列中,得到[1, 2, 4, 5, 6]。
5. 第五轮排序:已排序序列为[1, 2, 4, 5, 6],待排序序列为[3] 将3插入到已排序序列中,得到[1, 2, 3, 4, 5, 6]。
经过5轮插入操作,最终得到了完整的有序序列。
插入排序的时间复杂度为O(n^2),其中n为待排序序列的长度。它是一种稳定的排序算法,适用于小规模数据或者部分有序的数据。
希尔排序(Shell Sort)是插入排序的一种改进版本,也被称为“缩小增量排序”。它通过将待排序的序列分成若干个子序列,对每个子序列进行插入排序,最终逐步减小子序列的间隔,直至间隔为1,完成排序。
希尔排序的基本思想是,通过使数组中任意间隔为h的元素都是有序的,从而使得整个数组基本有序。这样的有序数组对插入排序的效率非常高,因为只需要少量的移动操作就能完成排序。
下面是希尔排序的基本步骤:
1. 选择间隔序列:选择一个递减的间隔序列(增量序列),通常以n/2开始,之后每次缩小一半,直至间隔为1。
2. 对每个间隔进行插入排序:对于每个间隔h,对数组中以h为间隔的元素进行插入排序。
3. 重复缩小间隔:重复以上步骤,不断缩小间隔直至间隔为1。
希尔排序的关键在于间隔序列的选择,不同的间隔序列会影响到排序的效率。常用的间隔序列有希尔原始序列(h = 3 * h + 1),也可以根据实际情况选择其他的间隔序列。
下面是希尔排序的示例(以升序排序为例):
假设初始序列为:[8, 3, 6, 4, 2, 9, 1, 5, 7]
1. 第一轮排序:间隔为4
○ 对子序列[8, 2], [3, 9], [6, 1], [4, 5], [2, 7]分别进行插入排序。
○ 得到子序列[2, 3, 1, 4, 2], [8, 9, 6, 5, 7],整个序列变为[2, 3, 1, 4, 2, 8, 9, 6, 5, 7]。
2. 第二轮排序:间隔为2
○ 对子序列[2, 1, 2, 9, 5], [3, 4, 8, 6, 7]分别进行插入排序。
○ 得到子序列[1, 2, 2, 5, 9], [3, 4, 6, 7, 8],整个序列变为[1, 2, 2, 5, 9, 3, 4, 6, 7, 8]。
3. 第三轮排序:间隔为1
○ 对整个序列进行插入排序。
○ 最终得到有序序列[1, 2, 2, 3, 4, 5, 6, 7, 8, 9]。
希尔排序的时间复杂度取决于间隔序列的选择,但通常情况下为O(n log n)到O(n^2)之间。希尔排序相比于简单的插入排序有更好的性能,特别是对于大型数据集合。
选择排序(Selection Sort)是一种简单直观的排序算法,它的基本思想是每次从未排序的序列中选择最小(或最大)的元素,将其放到已排序序列的末尾(或开头),直到所有元素都被排序完成。
下面是选择排序的基本步骤:
1. 初始状态:将整个序列分为两部分,一部分是已排序序列,另一部分是未排序序列。初始时已排序序列为空,未排序序列包含所有待排序的元素。
2. 选择最小元素:从未排序序列中选择最小的元素,将其与未排序序列的第一个元素交换位置,使得未排序序列的第一个元素成为已排序序列的最后一个元素。
3. 增加已排序部分的长度:将已排序序列的长度增加1,将未排序序列的长度减少1。
4. 重复操作:重复步骤2和步骤3,直到未排序序列的长度为0,即所有元素都被排序完成。
选择排序的特点是每次交换都会确定一个元素的最终位置,因此它是一种不稳定的排序算法。
下面是选择排序的示例(以升序排序为例):
假设初始序列为:[5, 2, 4, 6, 1, 3]
1. 第一轮排序:从未排序序列中选择最小的元素1,与未排序序列的第一个元素5交换位置,得到序列[1, 2, 4, 6, 5, 3]。
2. 第二轮排序:从未排序序列中选择最小的元素2,与未排序序列的第一个元素2交换位置(实际上不需要交换),得到序列[1, 2, 4, 6, 5, 3]。
3. 第三轮排序:从未排序序列中选择最小的元素3,与未排序序列的第一个元素4交换位置,得到序列[1, 2, 3, 6, 5, 4]。
4. 以此类推,直到所有元素都被排序完成。
经过多轮的选择和交换操作,最终得到了完整的有序序列。
选择排序的时间复杂度为O(n^2),其中n为待排序序列的长度。它是一种简单但不是很高效的排序算法,适用于小规模数据或者简单的排序任务。
冒泡排序(Bubble Sort)是一种简单直观的排序算法,它重复地遍历待排序序列,依次比较相邻的元素,并且在需要时交换它们的位置,直到整个序列有序为止。
基本思想是从待排序序列的起始位置开始,依次比较相邻的两个元素,如果顺序不符合排序要求(比如要求升序排列,但左边的元素比右边的大),则交换这两个元素的位置,使得较大(或较小)的元素向右(或向左)移动。这样一轮下来,最大(或最小)的元素就会“冒泡”到序列的末尾。然后,再对剩余的未排序部分重复这个过程,直到整个序列有序。
下面是冒泡排序的基本步骤:
1. 外层循环:从序列的起始位置开始,进行n-1轮遍历(n为序列的长度)。
2. 内层循环:在每轮遍历中,从第一个元素开始,依次比较相邻的两个元素,如果顺序不符合排序要求,则交换它们的位置。
3. 重复交换:每轮遍历会将当前未排序部分的最大(或最小)元素“冒泡”到序列的末尾,因此下一轮遍历时可以减少一个元素的比较次数。
4. 重复操作:重复以上步骤,直到所有元素都被排序好。
下面是冒泡排序的示例(以升序排序为例):
假设初始序列为:[5, 2, 4, 6, 1, 3]
1. 第一轮排序:
○ 比较相邻的元素:[5, 2, 4, 6, 1, 3],发现5比2大,交换它们的位置,序列变为[2, 5, 4, 6, 1, 3]。
○ 继续比较:[2, 5, 4, 6, 1, 3],发现5比4大,交换它们的位置,序列变为[2, 4, 5, 6, 1, 3]。
○ 继续比较:[2, 4, 5, 6, 1, 3],6比1大,交换它们的位置,序列变为[2, 4, 5, 1, 6, 3]。
○ 继续比较:[2, 4, 5, 1, 6, 3],6比3大,交换它们的位置,序列变为[2, 4, 5, 1, 3, 6]。
○ 此时最大的元素6已经冒泡到了序列末尾。
2. 第二轮排序:
○ 继续按照上述步骤比较交换,直到序列变为[2, 4, 1, 3, 5, 6]。
3. 第三轮排序:
○ 继续按照上述步骤比较交换,直到序列变为[2, 1, 3, 4, 5, 6]。
4. 第四轮排序:
○ 继续按照上述步骤比较交换,直到序列变为[1, 2, 3, 4, 5, 6]。
经过4轮排序,最终得到了完整的升序序列。
冒泡排序的时间复杂度为O(n^2),其中n为待排序序列的长度。它是一种稳定的排序算法,适用于小规模数据或者基本有序的数据。然而,由于其时间复杂度较高,不适用于大规模数据的排序。
归并排序(Merge Sort)是一种经典的、高效的排序算法,采用了分治策略。它将待排序序列分成两个子序列,分别对子序列进行递归排序,然后将已排序的子序列合并成一个有序序列。其核心思想是将两个有序的子序列合并成一个更大的有序序列。
归并排序的基本步骤如下:
1. 分解:将待排序序列分解成两个子序列,直到子序列的长度为1,即无法再分解为止。
2. 解决:对每个子序列进行排序,可以使用递归调用归并排序算法。
3. 合并:将已排序的子序列合并成一个有序序列。
下面是归并排序的详细过程:
1. 分解:将待排序序列分解成长度相等(或相差不超过1)的两个子序列,分别递归地对这两个子序列进行归并排序,直到子序列的长度为1。
2. 解决:递归地对每个子序列进行归并排序,直到所有的子序列都变成了只有一个元素的有序序列。
3. 合并:将已排序的子序列两两合并成更大的有序序列,具体方法如下:
○ 创建一个临时数组,用来存放合并后的结果。
○ 设置两个指针分别指向两个已排序的子序列的起始位置。
○ 比较两个子序列当前位置的元素,将较小的元素放入临时数组中,并将相应指针向后移动一位。
○ 重复上述步骤,直到其中一个子序列被合并完毕。
○ 将剩余未合并的子序列直接复制到临时数组中。
○ 将临时数组中的元素复制回原始数组的相应位置,完成合并操作。
下面是归并排序的示例(以升序排序为例):
假设初始序列为:[38, 27, 43, 3, 9, 82, 10]
1. 第一步分解:
○ 将序列分解成[38, 27, 43, 3]和[9, 82, 10]两个子序列。
2. 第二步解决:
○ 递归地对子序列进行归并排序,得到[3, 27, 38, 43]和[9, 10, 82]。
3. 第三步合并:
○ 将两个已排序的子序列[3, 27, 38, 43]和[9, 10, 82]合并成[3, 9, 10, 27, 38, 43, 82]。
经过以上步骤,最终得到了完整的升序序列。
归并排序的时间复杂度为O(n log n),其中n为待排序序列的长度。由于归并排序是一种稳定的排序算法,并且适用于各种数据类型,因此它被广泛应用于实际场景中。然而,归并排序需要额外的空间来存储临时数组,因此空间复杂度较高。
快速排序(Quick Sort)是一种高效的排序算法,它采用了分治策略。快速排序的基本思想是通过选取一个基准元素,将待排序序列分成两个子序列,其中一个子序列中的所有元素都小于等于基准元素,另一个子序列中的所有元素都大于基准元素。然后对这两个子序列分别进行递归排序,最终将整个序列排序完成。
快速排序的基本步骤如下:
1. 选择基准:从待排序序列中选择一个基准元素,通常选择序列中的第一个元素、最后一个元素或者中间的元素。
2. 分区:将待排序序列中的元素按照基准元素的大小分成两个子序列,一个子序列中的元素都小于等于基准元素,另一个子序列中的元素都大于基准元素。
3. 递归排序:对这两个子序列分别递归地应用快速排序算法,直到子序列的长度为1或0,即无法再分解为止。
4. 合并:将排好序的子序列合并成一个有序序列。
下面是快速排序的详细过程:
1. 选择基准:从待排序序列中选择一个基准元素,例如选择序列中的第一个元素。
2. 分区:将序列中的元素按照基准元素的大小进行分区,将比基准元素小的元素放到基准元素的左边,将比基准元素大的元素放到基准元素的右边。这一步通常使用双指针法来实现,一个指针从左边开始,一个指针从右边开始,当两个指针相遇时分区结束。
3. 递归排序:对分区后的两个子序列分别递归地应用快速排序算法,直到子序列的长度为1或0。
4. 合并:由于快速排序是原地排序算法,因此不需要显式地合并子序列。
下面是快速排序的示例(以升序排序为例):
假设初始序列为:[38, 27, 43, 3, 9, 82, 10]
1. 第一步选择基准:选择序列中的第一个元素38作为基准。
2. 第二步分区:
○ 将序列按照基准元素38进行分区,小于38的放在左边,大于38的放在右边。
○ 分区后序列变为:[27, 3, 9, 10, 38, 43, 82]
3. 第三步递归排序:
○ 对左边的子序列[27, 3, 9, 10]和右边的子序列[43, 82]分别递归地应用快速排序算法。
○ 子序列[27, 3, 9, 10]的基准元素为27,分区后变为[3, 9, 10, 27]。
○ 子序列[43, 82]的基准元素为43,分区后不变。
4. 最终合并:由于快速排序是原地排序算法,不需要显式合并。
经过以上步骤,最终得到了完整的升序序列。
快速排序的时间复杂度为O(n log n),其中n为待排序序列的长度。由于快速排序是一种原地排序算法,并且具有较高的平均性能,因此它被广泛应用于各种场景中。然而,在最坏情况下,快速排序的时间复杂度会退化为O(n^2),即当选择的基准元素不合适时,会导致递归树的深度过大,造成性能下降。
堆排序(Heap Sort)是一种基于完全二叉树结构的排序算法,它利用了堆数据结构的特性进行排序。堆是一种特殊的树形数据结构,它满足堆属性:对于每个节点i,其父节点的值要么大于等于(最大堆)或小于等于(最小堆)节点i的值。
堆排序的基本思想是首先将待排序序列构建成一个堆(通常是最大堆),然后反复将堆顶元素(最大元素)与堆的最后一个元素交换,并调整堆使其满足堆属性,最终得到一个有序序列。
堆排序的主要步骤如下:
1. 构建堆:将待排序序列构建成一个堆。从序列的中间位置向前遍历,依次将每个非叶子节点作为根节点,通过向下调整(heapify)操作使得其满足堆属性。这一步的时间复杂度为O(n),其中n为序列的长度。
2. 堆调整:交换堆顶元素(最大元素)与堆的最后一个元素,然后通过向下调整操作重新调整堆,使其满足堆属性。这一步的时间复杂度为O(log n),其中n为堆的大小。
3. 重复操作:重复步骤2,直到堆中只剩下一个元素。
4. 得到有序序列:此时堆中只剩下一个元素,即已经排好序的元素,将其取出即可得到有序序列。
下面是堆排序的详细过程(以升序排序为例):
假设初始序列为:[7, 6, 3, 5, 4, 1, 2]
1. 构建堆:从序列中间位置开始,依次向前遍历,对每个非叶子节点进行向下调整操作,得到最大堆。经过调整后,序列变为:[7, 6, 3, 5, 4, 1, 2] -> [7, 6, 3, 5, 4, 1, 2] -> [7, 6, 3, 5, 4, 1, 2] -> [7, 6, 3, 5, 4, 1, 2] -> [7, 6, 3, 5, 4, 1, 2] -> [7, 6, 3, 5, 4, 1, 2] -> [7, 6, 3, 5, 4, 1, 2] -> [7, 6, 3, 5, 4, 1, 2]。
2. 堆调整:交换堆顶元素7与最后一个元素2,并调整堆,得到[2, 6, 3, 5, 4, 1, 7]。
3. 重复操作:继续交换堆顶元素与最后一个元素,并进行堆调整,得到[1, 6, 3, 5, 4, 2, 7] -> [1, 5, 3, 6, 4, 2, 7] -> [1, 4, 3, 5, 2, 6, 7] -> [1, 3, 2, 5, 4, 6, 7] -> [1, 2, 3, 5, 4, 6, 7]。
4. 得到有序序列:最后得到有序序列[1, 2, 3, 4, 5, 6, 7]。
堆排序的时间复杂度为O(n log n),其中n为待排序序列的长度。堆排序是一种原地排序算法,不需要额外的空间,但由于其对数据的访问方式不够友好,因此在实际应用中,性能可能不如快速排序。然而,堆排序在实现上相对简单,且在对内存空间有限制的情况下也能够很好地工作。
基数排序(Radix Sort)是一种非比较性的整数排序算法,它利用了“按位分配”的思想,对于有限范围内的整数排序非常高效。基数排序将待排序的整数按照位数从低位到高位依次进行排序,每次排序根据当前位的值进行桶排序或计数排序,直到所有位数都被考虑完毕,最终得到有序序列。
基数排序的基本步骤如下:
1. 确定位数:首先确定待排序整数的最大位数,以确定需要进行多少轮排序。
2. 按位分配:从低位到高位依次考虑每一位数值,对整数进行按位分配。
3. 桶排序或计数排序:对于每一位数值,可以选择使用桶排序或计数排序对待排序整数进行排序。
4. 重复操作:重复上述步骤,直到所有位数都被考虑完毕。
5. 得到有序序列:最终得到有序序列。
下面是基数排序的详细过程:
假设初始序列为:[170, 45, 75, 90, 802, 24, 2, 66]
1. 确定位数:对于给定的序列,最大的整数为802,它是三位数,因此需要进行三轮排序。
2. 第一轮排序(个位数):按照个位数的值对整数进行桶排序或计数排序。得到排序后的序列:[170, 90, 802, 2, 24, 45, 75, 66]。
3. 第二轮排序(十位数):按照十位数的值对整数进行桶排序或计数排序。得到排序后的序列:[802, 2, 24, 45, 66, 170, 75, 90]。
4. 第三轮排序(百位数):按照百位数的值对整数进行桶排序或计数排序。得到排序后的序列:[2, 24, 45, 66, 75, 90, 170, 802]。
经过三轮排序,最终得到了完整的升序序列。
基数排序的时间复杂度为O(n*k),其中n为待排序序列的长度,k为最大位数。当待排序序列中的数值范围较大时,基数排序的性能可能会受到影响。基数排序是一种稳定的排序算法,并且适用于整数等非负数值的排序。