一、冒泡排序
冒泡排序(Bubble Sort)是一种简单的排序算法,它通过重复地遍历待排序的列表,比较相邻的元素并交换它们的位置来实现排序。该算法的名称来源于较小的元素会像"气泡"一样逐渐"浮"到列表的顶端。
算法步骤:
-
比较相邻元素:从列表的第一个元素开始,比较相邻的两个元素。
-
交换位置:如果前一个元素比后一个元素大,则交换它们的位置。
-
重复遍历:对列表中的每一对相邻元素重复上述步骤,直到列表的末尾。这样,最大的元素会被"冒泡"到列表的最后。
-
缩小范围:忽略已经排序好的最后一个元素,重复上述步骤,直到整个列表排序完成。
假设有一个待排序的列表 [5, 3, 8, 4, 6],冒泡排序的过程如下:
-
第一轮遍历:
-
比较 5 和 3,交换位置,列表变为
[3, 5, 8, 4, 6]
。 -
比较 5 和 8,不交换。
-
比较 8 和 4,交换位置,列表变为
[3, 5, 4, 8, 6]
。 -
比较 8 和 6,交换位置,列表变为
[3, 5, 4, 6, 8]
。 -
第一轮结束后,最大的元素 8 已经"冒泡"到列表的最后。
-
-
第二轮遍历:
-
比较 3 和 5,不交换。
-
比较 5 和 4,交换位置,列表变为
[3, 4, 5, 6, 8]
。 -
比较 5 和 6,不交换。
-
第二轮结束后,第二大的元素 6 已经"冒泡"到列表的倒数第二位置。
-
-
第三轮遍历:
-
比较 3 和 4,不交换。
-
比较 4 和 5,不交换。
-
第三轮结束后,列表已经有序。
-
-
第四轮遍历:
-
比较 3 和 4,不交换。
-
列表已经完全有序。
-
代码实现: 
外层循环控制进行多少轮比较,内层循环进行具体的元素排序操作,通常情况下当有n个元素时,外层循环次数设置为n-1次,上述代码还使用标志变量来判断元素是否有序并提前结束循环避免不必要的浪费,即在内层循环完成后没有发生交换操作,需要注意的是标志变量flag设置在每次外层循环开始处。
二、选择排序
选择排序(Selection Sort)是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
选择排序基本思想是每次从待排序的数据中选择最小(或最大)的元素,放到已排序序列的末尾,直到全部数据排序完成。
算法步骤:
-
初始化:将列表分为已排序部分和未排序部分。初始时,已排序部分为空,未排序部分为整个列表。
-
查找最小值:在未排序部分中查找最小的元素。
-
交换位置:将找到的最小元素与未排序部分的第一个元素交换位置。
-
更新范围:将未排序部分的起始位置向后移动一位,扩大已排序部分的范围。
-
重复步骤:重复上述步骤,直到未排序部分为空,列表完全有序。
假设有一个待排序的列表 [64, 25, 12, 22, 11],选择排序的过程如下:
-
第一轮:
-
未排序部分:
[64, 25, 12, 22, 11]
。 -
找到最小值
11
,将其与第一个元素64
交换。 -
列表变为
[11, 25, 12, 22, 64]
。 -
已排序部分:
[11]
,未排序部分:[25, 12, 22, 64]
。
-
-
第二轮:
-
未排序部分:
[25, 12, 22, 64]
。 -
找到最小值
12
,将其与第一个元素25
交换。 -
列表变为
[11, 12, 25, 22, 64]
。 -
已排序部分:
[11, 12]
,未排序部分:[25, 22, 64]
。
-
-
第三轮:
-
未排序部分:
[25, 22, 64]
。 -
找到最小值
22
,将其与第一个元素25
交换。 -
列表变为
[11, 12, 22, 25, 64]
。 -
已排序部分:
[11, 12, 22]
,未排序部分:[25, 64]
。
-
-
第四轮:
-
未排序部分:
[25, 64]
。 -
找到最小值
25
,它已经在正确的位置,无需交换。 -
列表保持不变:
[11, 12, 22, 25, 64]
。 -
已排序部分:
[11, 12, 22, 25]
,未排序部分:[64]
。
-
-
第五轮:
-
未排序部分:
[64]
。 -
只有一个元素,无需操作。
-
列表完全有序:
[11, 12, 22, 25, 64]
。
-
代码实现:
外层循环控制进行多少轮元素交换,内层循环寻找最小元素,每次外层循环开始处设置当前i值为最小元素,经内层循环更新最小元素下标,随后执行交换操作。
三、快速排序
快速排序(Quick Sort)是一种高效的排序算法,基于分治法(Divide and Conquer)的思想。它的核心是通过选择一个基准元素(pivot),将列表分为两部分:一部分小于基准元素,另一部分大于基准元素,然后递归地对这两部分进行排序。快速排序的平均时间复杂度为 O(n log n),在实际应用中性能优异。
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
算法步骤
-
选择基准元素:从列表中选择一个元素作为基准(pivot)。选择方式可以是第一个元素、最后一个元素、中间元素或随机元素。
-
分区:将列表重新排列,使得所有小于基准元素的元素都在基准的左侧,所有大于基准元素的元素都在基准的右侧。基准元素的位置在分区完成后确定。
-
递归排序:对基准元素左侧和右侧的子列表分别递归地进行快速排序。
-
合并:由于分区操作是原地进行的,递归结束后整个列表已经有序。
代码实现:
代码的主要思想是通过递归不断缩小待排序元素的范围,首先确定基准元素的最终位置,本代码选取最后一个元素为基准元素,且选取其他元素为基准元素的方法与此又略有不同,在partition函数中j用来遍历元素,i用来标记小于基准元素的位置,循环完毕后i+1的位置自然成为基准元素的最终位置,随后将元素分为左右两部分,依次进行递归排序,直至排序完成。
四、归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代;
归并排序的核心思想是将一个大问题分解成若干个小问题,分别解决这些小问题,然后将结果合并起来,最终得到整个问题的解。具体到排序问题,归并排序的步骤如下:
- 分解(Divide):将待排序的数组分成两个子数组,每个子数组包含大约一半的元素。
- 解决(Conquer):递归地对每个子数组进行排序。
- 合并(Combine):将两个已排序的子数组合并成一个有序的数组。
通过不断地分解和合并,最终整个数组将被排序。
算法步骤:
-
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
-
设定两个指针,最初位置分别为两个已经排序序列的起始位置;
-
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
-
重复步骤 3 直到某一指针达到序列尾;
-
将另一序列剩下的所有元素直接复制到合并序列尾。
代码实现:
代码采用的方法是先将数组分为两个部分,随后对各部分排序,最后合并,简单实现了归并排序算法,在MergeSort方法中通过数组res来按大小依次填入两个子数组的元素,并通过现有方法实现对子数组的遍历即不断截取子数组,从而实现归并排序。
五、桶排序
桶排序(Bucket Sort)是一种分布式排序算法,它将待排序的元素分配到若干个桶(Bucket)中,然后对每个桶中的元素进行排序,最后将所有桶中的元素按顺序合并。桶排序的核心思想是将数据分到有限数量的桶中,每个桶再分别排序(可以使用其他排序算法或递归地使用桶排序)。
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
算法步骤:
-
初始化桶:根据数据的范围和分布,创建若干个桶。
-
分配元素:遍历待排序的列表,将每个元素分配到对应的桶中。
-
排序每个桶:对每个桶中的元素进行排序(可以使用插入排序、快速排序等)。
-
合并桶:将所有桶中的元素按顺序合并,得到最终排序结果。
代码实现:
代码中先确定桶的数量,随后将元素分配进桶里,分别对各个桶里元素排序,最后合并桶内元素。桶排序的关键是处理好桶的个数和桶中元素个数的关系,算法的效率与元素分配密切相关。