比较排序是一类排序算法的统称,这些排序算法主要是通过对待排序数组中的元素进行比较来实现排序的。
常见的比较排序包括:选择排序、冒泡排序、插入排序、快速排序、希尔排序、合并排序和堆排序等。
对于两个元素,通过比较它们的大小就能够决定它们的排列顺序。
在出现两个或多个元素一样大的情况时,如果所使用的比较排序算法是稳定的,则会保持它们在原数组中的相对顺序。
选择排序
选择排序(Selection Sort)是一种很简单直观的排序算法,它的主要思想是:将待排序序列分成两部分,其中一部分为已排序子序列,而另一部分为未排序子序列,分别为待排序序列的前半部分和后半部分。
起初,已排序子序列为空,而未排序子序列整个为待排序序列。
选择排序算法所要做的事情就是每次都从未排序子序列中选择出最大(小)的元素,然后将每次所取的元素放至已排序子序列的末尾,直到未排序子序列的全部元素都移到已排序子序列。
排序要点
设待排序数组为a[0]、a[1]、…、a[n-1],则选择排序的执行步骤如下。
① 初始状态时,已排序子序列为空,而未排序子序列为a[0]、a[1]、…、a[n-1]。
② 第一趟排序,从未排序子序列中找到最大(小)元素a[k],将其与未排序子序列的第一个元素a[0]对换。此时已排序子序列为a[0],长度为1,而未排序子序列长度减1,变为n-1,其中对换前的a[k]被取走。
③ 第二趟排序,从未排序子序列中找到最大(小)元素a[j],将其与未排序子序列的第一个元素a[1]对换。此时已排序子序列为a[0]、a[1],长度变为2,而未排序子序列长度继续减1,变为n-2,其中对换前的a[j]又被取走。
④ 不断进行上面的排序操作,直到经过n-1趟排序后完成整个数组的排序。最终未排序子序列为空,已排序子序列长度为n。
排序性能
选择排序以其简单性著称。但它的平均时间复杂度为O(n2),其中n为待排序元素的数量,所以当元素数量非常大时将会导致性能相当差。
此外,选择排序是非稳定排序。
冒泡排序
冒泡排序(Bubble Sort)是一种很简单的排序算法,它的主要思想就是不断比较待排序序列,每次只比较两个相邻的元素,如果这两个元素的顺序不符合要求则对换它们的位置,不断重复,直到没有相邻元素需要对换。
在不断比较的过程中,最大(小)的元素经过不停地交换会慢慢移到数列的一端,所以看起来它就像气泡一样不断往上冒,于是就叫冒泡排序。
排序要点
① 从头开始逐一比较相邻的两个元素,如果前一元素比后一元素大,则对换它们的位置,直至比较完最后一对。执行完一轮比较后,则该轮最大的元素就被交换到了最后的位置。
② 针对所有元素执行若干轮步骤①,每次执行完一轮比较后,将该轮最后位置的元素排除,不参与下一轮比较。每一轮需要比较的元素越来越少,直至没有任何一对元素需要比较。
排序性能
冒泡排序是一种性能很差的排序算法,我们在实际项目中几乎都不使用它,它更多地是被老师拿来在课堂上讲解排序的一些思维。冒泡排序的平均时间复杂度达到O(n2),其中n为待排序元素的数量。
另外,冒泡排序属于稳定排序。
插入排序
插入排序(Insertion Sort)的基本思想为:在一个已排序序列中,插入一个元素,要求在插入此元素后序列仍然有序。
每次都将一个未排序元素按照值的大小插入到已排序序列中适当的位置,直到全部元素都插入完为止。
排序要点
① 从未排序序列的第一个元素开始,该元素被认为是已排序序列。
② 从未排序序列中取出下一个元素a,在已排序序列中从后往前查找该元素合适的位置。
③ 如果元素a小于已排序序列中的元素,则继续往前比较。
④ 重复第③步,直到找到已排序序列中小于等于a的元素为止,该元素的后面即是我们要找的位置。如果在已排序子序列中找不到小于等于a的元素,说明元素a比已排序子序列中所有元素都小,元素a应该放在已排序子序列的首位。
⑤ 将元素a插入到上述所找到的位置。
⑥ 重复第②步到第⑤步,直到未排序序列被取空为止。
排序性能
插入排序与选择排序和冒泡排序的平均时间复杂度一样,都为O(n^2),其中n为待排序元素的数量。但在实际工作中插入排序却比前两种排序用得更多,因为它的实现相当容易。
此外,插入排序属于稳定排序。
快速排序
快速排序(Quick Sort)由霍尔(C.A.R.Hoare)在1961年公开发表,是冒泡排序的一种改进。
快速排序的基本思想为:通过一趟排序后将待排序数组分割成独立的两部分,其中一部分的所有值比另一部分的所有值都小,然后再对分割后的两部分分别进行快速排序,整个过程可以通过递归进行,最终整个序列变为有序序列。
排序要点
设待排序数组为a[0]、a[1]、…、a[n-1],则快速排序的过程如下。
① 初始化两个变量i、j,刚开始i=1,j=n-1。
② 将第一个元素a[0]作为基准数。
③ 从下标i开始向后搜索,若a[i]小于等于基准数,则令i=i+1并重新比较,一直比较,直到找到第一个大于基准数的元素a[i]。
④ 从下标j开始向前搜索,若a[j]大于等于基准数,则令j=j-1并重新比较,一直比较,直到找到第一个小于基准数的元素a[j]。
⑤ 将a[i]与a[j]对换。
排序性能
快速排序性能也如其名:快速,它的平均时间复杂度为O(nlog n),其中n为待排序元素的数量,如果用合适的代码实现,它能比其他排序算法快几倍。快速排序的平均时间复杂度的严格推导证明比较复杂,但我们可以从感性的角度出发将排序过程想象成一棵二叉树,该树的高度为log n,而每层需要比较的次数为n级别,所以整个平均时间复杂度就是O(nlog n)。
快速排序并不是一种稳定排序,也就是说排序后可能会改变相等元素在原来数组中的相对位置。
⑥ 重复步骤③到步骤⑤,直到i=j,然后将a[0]与a[j-1]对换。需要注意的是,步骤③和步骤④中的i和j都有可能找不到各自的目标,这时我们可以换个基准数,比如随机选择其他元素作为基准数并将其与a[0]对调,然后重新执行上面的步骤。
⑦ 序列被基准数分割成两个分区,前面分区的元素全部小于等于基准数,后面分区的元素全部大于等于基准数。
⑧ 递归地对分区子序列进行快速排序,最终完成整个排序工作。
每趟快速排序的核心工作是:选一个元素作为基准数,然后将所有小于等于它的数都放到它前面,大于等于它的数都放在它后面。
希尔排序
它的基本思想是将待排序元素进行增量分组,然后在各分组组内进行插入排序。随着增量的减少,每个分组组内的元素越来越多,直至增量减至1时,所有元素都被分到同一个组内,执行插入排序后完成整个排序操作。
排序要点
① 选取一个小于待排序数组长度n的整数i1作为第一个增量,根据增量对待排序数组进行分组,分组的依据是将所有距离为i1倍数的元素分到同一组。
② 针对每个分组,在组内进行插入排序操作。
③ 接着取下一个增量i2,其中i2 < i1,然后根据新的增量继续对待排序数组进行分组,并在每个分组组内进行插入排序操作。
④ 重复步骤③直至增量为1,即待排序数组中的所有元素都在同一分组中,再进行插入排序,完成整个排序工作。
希尔排序的过程中对于增量的选择没有标准,有多种处理方案,比如可以初次取所有元素数量的一半作为增量,之后每次减半,直到增量为1。
排序性能
希尔排序的平均时间复杂度为O(n^k),其中n为待排序元素的数量,而k为常数,取值范围为k<2。k并没有一个固定的值,而是与增量的选择有关。可以确定的是它不会达到平方级别,而是低于平方级。
此外,希尔排序属于非稳定排序。
合并排序
它的主要思想是分治法,把待排序序列分为若干个有序子序列,然后将两个或两个以上的有序子序列合并,得到一个新的有序序列。
所以首先得对所有子序列进行排序,得到有序子序列,然后不断地使更大的子序列有序,最终使得整个序列有序。
排序要点
既然是分治法,那么就涉及三部分:分、治、合。分,即是将序列分割成小序列再求解;治,即对分割后的小序列进行处理;合,即将有序子序列合并到一起。整个过程可以使用递归的方式。
① 设序列长度为L,首先将序列分为两个长度为L/2(如果不能整除则某个子序列多分一个元素)的子序列。
② 继续分别对子序列分割,不断进行直至不能再继续分割,此时每个子序列只包含一个元素。
③ 对分割后的子序列进行合并,按照顺序组合成有序子序列,有序子序列之间继续合并。
④ 不断对有序子序列进行合并,最终合并成一个完整的长度为L的有序序列。
排序性能
合并排序的平均时间复杂度为O(nlog n),其中n为待排序元素的数量,可以看到合并排序的性能更优异。
合并排序的平均时间复杂度的严格推导证明比较复杂,但我们同样可以换种角度将排序过程想象成一棵二叉树,该树的高度为log n,而每层需要比较的次数为n级别,所以整个平均时间复杂度就是O(nlog n)。
合并排序是一种稳定排序。
堆排序
它的主要思想是将待排序序列分为已排序序列和未排序序列两部分,然后每次从未排序序列中选出最大(小)的元素并将其加入到已排序序列中,不断执行此操作直到将未排序序列的所有元素都添加到已排序序列中。
所以堆排序其实可以看成是选择排序的升级版,通过引入堆来优化查找最大(小)元素的时间复杂度。
排序要点
堆排序的执行主要包含两大步骤。
第一步是根据未排序序列来构建一个堆(我们选择使用二叉堆),其中二叉堆可以使用数组来表示,这个在前面的第4章中有详细的介绍。
第二步是依次从二叉堆中提取出最大(小)元素并加入到已排序序列中,与此同时我们还要维护好二叉堆的性质。
① 初始时,整个待排序序列为未排序序列,而已排序序列为空,先根据未排序序列构建一个二叉堆。
② 从二叉堆中获取最大(小)元素,即数组中第一个元素,加入到已排序序列中。实际上我们并不需要额外创建新的未排序序列和已排序序列空间,只需将待排序数组分为未排序数组和已排序数组两部分,然后不断将最大(小)元素置换到已排序数组中,并且每次置换完需将未排序数组的范围缩小1。
③ 按照二叉堆的性质来调整未排序数组,调整后此轮的最大(小)元素处于未排序数组的首位。
④ 重复执行步骤②和步骤③,直到未排序数组只剩下一个元素。
排序性能
堆排序的平均时间复杂度为O(nlog n),其中n为待排序元素的数量。而选择排序的平均时间复杂度为O(n2),我们可以看到堆排序的性能比选择排序的更加优异。在整个排序过程中,构建二叉堆的平均时间复杂度为O(n),维护二叉堆性质的平均时间复杂度为O(log n),一共需要执行n次维护工作,所以整个平均时间复杂度就是O(n + nlog n),即O(nlog n)。
堆排序属于非稳定排序。