Skip to the content.

Sorting

排序算法有很多很多种,而这些特定的基础算法还会衍生各种变体,教授挑选了几种最常见的算法进行讲解说明。下面先简要说一下将要介绍的算法的特点:

上述是几种常用的算法,也是接下来教授要介绍的算法。

Bogo Sort

通过不断地随机打乱数组中的元素来对数组进行碰运气式的排序,直到数组碰巧有序,否则不停地打乱他。

Big O Performance

时间复杂度:总元素个数的阶乘O(N!),这太糟糕了,根据概率论思想,最化的情况下,它需要尝试N!次可能的排序序列后才能拿到最终正确的排序结果。

这很考验我们编写的shuffle函数,需要shuffle函数每个元素同等的概率,稍有不慎就可能出现概率失衡。

Comp-Swap based Sort

Selection Sort

每次找到当前待排序数组中的最小元素,将其移动到头部。 不断重复直到待排序数组为空。

时间复杂度:基本上需要为每个最小元素遍历一次整个数组,因此是O(n^2^)。

下面是一种实现方式:

image-20240310145533464

Insertion Sort

首先构建一个存放有序元素的容器;之后取出待排序数组的第一个元素,将之插入到有序元素的容器中。当然我们可以就地排序,使用一个分割线将数组分为有序和无序部分。

就地排序的话,就是不断地将待排序元素与前面的元素进行compare_and_swap操作,直到找到合适位置。或者可以将有序部分的元素不断的后移,直到找到

时间复杂度:每取出一个元素时,最坏的情况下都需要与当前有序容器中所有元素比较后才能决定放入位置,因此时间复杂度是O(N^2^)。

在大数据量情况下,总体上来说比Selection Sort要快。示例代码如下:

image-20240310152522322

像selection Sort和Insertion Sort这两种排序都是基于compare and swap的,因此时间复杂度都是O(N^2^)。

Divide-and-Conquer based Sort

Merge Sort(Stable Sort)

将未排序的数组递归的分成两半,直到每个half的大小符合我们规定的sort block size,之后单独对每个half进行merge sort,然后将两个sorted half合并为一个sorted list(一般用双指针的方法合并两个sorted half),不断地重复该过程,直到只有一个sorted list。

时间复杂度:很明显,在算法的每个级别我们都需要进行双指针的排序操作,因此每个级别都把所有元素遍历了一遍,是O(N);当我们拆分数组时,一共有LogN层。因此,该算法的时间复杂度是O(NLogN)。

从下图中可以直观的感受该算法。

image-20240310153525814

runtime intuition

下图可以更直观的感受merge sort的时间复杂度:

image-20240310154041263

example

下面是一个实现的例子:

image-20240310155953370

image-20240310155921304

Quick Sort(not stable sort)

通过将待排序列表分为两部分分别进行排序,这个分割的标准取决于我们选择的pivot elem,小于pivot的元素放在pivot左侧,大于等于pivot的元素放在其右侧。

快排也是一种分治算法:

时间复杂度:平均为O(NLogN)。如果pivot选的不好,最坏情况下是O(N^2^)。结合二叉树结构很容易想明白。

Quick Sort的步骤大体上可以分为如下几步:

choosing a “pivot”

理论上,不管我们选择哪个元素作为pivot,算法都能正确的工作。只是运行效率不同。

但是为了效率,选择的pivot最好可以将元素分为大小相当的两个partition(当前元素结合的中位数)。

partition an array

暂时将pivot元素放到最后一个索引处。

partition行为通过重复以下操作进行while(i < j)

最后i,j相遇或者i+1=j,将pivot元素与a[i]元素交换,此时partition操作完成。

Stable Sort

稳定排序是一种排序算法,它在排序的过程中保持相等元素的相对顺序。与之相对的是不稳定排序,它不保证相等元素的相对顺序。

比如说,我们首先用S1策略排序数组,之后用S2策略排序数组,如果S2策略后数组仍然可以保持S1策略的相对顺序,那么S2就是稳定排序。