≤ x x i j x > x x i j 交换 x x x 交换 x 我们需要的第一趟排序结果如图最后一行所示,使得序列中大于x x x 和小于x x x 的序列被分割开来。
建立双指针i , j i,j i , j . 初始时分别在数组的左右两端,j j j 不断向左移动,i i i 不断向右移动。 移动过程中,j j j 主要目的是从右向左找到第一个不满足最终要求的位置,即A [ j ] < x A[j]\lt x A [ j ] < x ,i i i 类似,找到这样的i i i 使得A [ i ] ≥ x A[i]\geq x A [ i ] ≥ x ,为了得到目标排序,对两个元素进行交换,然后继续。
【注】 一趟排序的搜索与交换方法有很多种,这里展示的只是其中一种
下面是对子数组A [ p . . r ] A[p..r] A [ p .. r ] 进行一趟排序的搜索交换算法的伪码。其中,为了方便我们假设把首元素作为一个基准/枢纽 进行排序。
Algorithm: Partition ( A , p , r ) 1. x ← A [ p ] 2. i ← p ; j ← r 3. w h i l e i < j d o 4. w h i l e i < j and A [ j ] > x d o 5. j ← j − 1 6. w h i l e i < j and A [ i ] ≤ x d o 7. i ← i + 1 8. A [ i ] ↔ A [ j ] 9. A [ i ] ↔ A [ p ] 10. r e t u r n i \begin{aligned} &\text{Algorithm: }\;\text{Partition}(A,p,r)\\\\ 1.&\;x\leftarrow A[p]\\ 2.&\;i\leftarrow p;\;j\leftarrow r\\ 3.&\;\mathbf{while}\;i\lt j\;\mathbf{do}\\ 4.&\;\qquad\mathbf{while}\;i\lt j\;\text{and}\;A[j]\gt x\;\mathbf{do}\\ 5.&\;\qquad\qquad j\leftarrow j-1\\ 6.&\;\qquad\mathbf{while}\;i\lt j\;\text{and}\;A[i]\leq x\;\mathbf{do}\\ 7.&\;\qquad\qquad i\leftarrow i+1\\ 8.&\;\qquad A[i]\;\leftrightarrow\;A[j]\\ 9.&\; A[i]\leftrightarrow A[p]\\ 10.&\;\mathbf{return}\;i \end{aligned} 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. Algorithm: Partition ( A , p , r ) x ← A [ p ] i ← p ; j ← r while i < j do while i < j and A [ j ] > x do j ← j − 1 while i < j and A [ i ] ≤ x do i ← i + 1 A [ i ] ↔ A [ j ] A [ i ] ↔ A [ p ] return i
得到一趟排序的算法之后,我们便可以根据分治法的思想,递归 地分解子问题然后调用函数实现快排,伪代码如下:
Algorithm: QuickSort ( A , p , r ) 1. i f p < r t h e n 2. q ← Partition ( A , p , r ) 3. QuickSort ( A , p , q − 1 ) 4. QuickSort ( A , q + 1 , r ) \begin{aligned} &\text{Algorithm: }\;\text{QuickSort}(A,p,r)\\\\ 1.&\;\mathbf{if}\;p\lt r\;\mathbf{then}\\ 2.&\;\qquad q\leftarrow \text{Partition}(A,p,r)\\ 3.&\;\qquad \text{QuickSort}(A,p,q-1)\\ 4.&\;\qquad \text{QuickSort}(A,q+1,r)\\ \end{aligned} 1. 2. 3. 4. Algorithm: QuickSort ( A , p , r ) if p < r then q ← Partition ( A , p , r ) QuickSort ( A , p , q − 1 ) QuickSort ( A , q + 1 , r )
算法性能 当每一次划分(Partition)时,都把数组分别划分为长度分别是n , 0 n,0 n , 0 的两个数组时,算法一共要执行n n n 次时间为Θ ( n ) \Theta(n) Θ ( n ) 的划分操作,从而快排的最坏时间复杂度 为O ( n 2 ) O(n^2) O ( n 2 ) .
当每次都正好能够平分数组 ,即每次都能将数组划分为A [ 1.. n / 2 ] , A [ n / 2 + 1.. n ] A[1..n/2],\;A[n/2+1..n] A [ 1.. n /2 ] , A [ n /2 + 1.. n ] 时,算法运行时间的递推方程为:
T ( n ) = 2 T ( n / 2 ) + Θ ( n ) T(n)=2T(n/2)+\Theta(n) T ( n ) = 2 T ( n /2 ) + Θ ( n )
可利用主定理 解得T ( n ) = Θ ( n log n ) T(n)=\Theta(n\log n) T ( n ) = Θ ( n log n ) . 这是快排的最优时间复杂度 .
当然这是1 : 1 1:1 1 : 1 的情况。 《算法导论》给出,如果每次递归下都是固定的某一常数比例的划分(例如1 : 9 1:9 1 : 9 ),那么快排的时间复杂度仍是O ( n log n ) O(n\log n) O ( n log n ) .
而 平均时间复杂度 已在本站文章【算法设计基础与综述】中的【差消法】部分介绍,此处不再赘述。 可以得出假设 枢轴/基准/首元素 在一趟排序后的位置服从于均匀分布 U ( p , r ) U(p,r) U ( p , r ) . 即其在每一个位置的概率相等且为1 / n 1/n 1/ n . 那么快排的平均实际复杂度为Θ ( n log n ) \Theta(n\log n) Θ ( n log n ) .
对于空间复杂度,由于需要利用到递归工作栈,所以:
分配均匀时达到最好情况,此时空间复杂度为⌊ log 2 ( n + 1 ) ⌋ \lfloor\log_2(n+1)\rfloor ⌊ log 2 ( n + 1 )⌋ 分配不平衡时到达最坏情况,此时为O ( n ) O(n) O ( n ) 平均情况就是O ( log 2 n ) O(\log_2n) O ( log 2 n ) 快速排序的元素比较次数与序列的初始序列无关 ,始终是n ( n − 1 ) / 2 n(n−1)/2 n ( n − 1 ) /2 快速排序当待排序表已经基本有序 时,反而属于最坏情况
随机化快速排序| Randomized QuickSort 在快排的性能分析时,我们假设数组元素在每个位置的概率相同且为1 / n 1/n 1/ n 然而在实际工程中这种假设却往往不成立。
因此,对于大数据的排列问题时,往往引入 随机性 来优化算法的性能。这样的快排被称为快速排序的随机化版本,或直接叫随机快速排序。
算法分析 引入一种随机抽样 (random sampling )的概念,区别于直接将首元作为枢轴,我们每一次划分时都随机地从数组A [ p . . r ] A[p..r] A [ p .. r ] 中选取一个元素x x x 将其作为枢轴来进行划分,因枢轴是等概率的随机选取,所以这个划分也是尽量均衡的。
算法很简单,只需在P a r t i t i o n ( ) Partition() P a r t i t i o n ( ) 时,将首元和数组中随机选择出的任意元素进行交换即可。
Algorithm: Randomized-Partition ( A , p , r ) 1. i ← Random ( p , r ) 2. A [ p ] ↔ A [ i ] 3. x ← A [ p ] 4. i ← p ; j ← r 5. w h i l e i < j d o 6. w h i l e i < j and A [ j ] > x d o 7. j ← j − 1 8. w h i l e i < j and A [ i ] ≤ x d o 9. i ← i + 1 10. A [ i ] ↔ A [ j ] 11. A [ i ] ↔ A [ p ] 12. r e t u r n i \begin{aligned} &\text{Algorithm: }\;\text{Randomized-Partition}(A,p,r)\\\\ 1.&\;i\leftarrow \text{Random}(p,r)\\ 2.&\;A[p]\leftrightarrow A[i]\\ 3.&\;x\leftarrow A[p]\\ 4.&\;i\leftarrow p;\;j\leftarrow r\\ 5.&\;\mathbf{while}\;i\lt j\;\mathbf{do}\\ 6.&\;\qquad\mathbf{while}\;i\lt j\;\text{and}\;A[j]\gt x\;\mathbf{do}\\ 7.&\;\qquad\qquad j\leftarrow j-1\\ 8.&\;\qquad\mathbf{while}\;i\lt j\;\text{and}\;A[i]\leq x\;\mathbf{do}\\ 9.&\;\qquad\qquad i\leftarrow i+1\\ 10.&\;\qquad A[i]\;\leftrightarrow\;A[j]\\ 11.&\; A[i]\leftrightarrow A[p]\\ 12.&\;\mathbf{return}\;i \end{aligned} 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. Algorithm: Randomized-Partition ( A , p , r ) i ← Random ( p , r ) A [ p ] ↔ A [ i ] x ← A [ p ] i ← p ; j ← r while i < j do while i < j and A [ j ] > x do j ← j − 1 while i < j and A [ i ] ≤ x do i ← i + 1 A [ i ] ↔ A [ j ] A [ i ] ↔ A [ p ] return i
编程实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 int Partition (vector <int > &A, int p, int r) { int i, j; i = p+1 ; j = r; int x = A[p]; while (i < j){ while (i < j && A[j] >= x) j--; while (i < j && A[i] < x) i++; swap(A[i],A[j]); } swap(A[p],A[i]); return i; } void QuickSort (vector <int > &A, int p, int r) { if (p < r){ int q = Partition(A, p, r); QuickSort(A, p, q-1 ); QuickSort(A, q+1 , r); } }
选择排序 选择排序的基本思想是:每一趟(如第i i i 趟)在后面的n − i + 1 n-i+1 n − i + 1 个待排序元素中,选择最小的元素放入第i i i 个位置。直到n − 1 n-1 n − 1 趟做完。
根据对不同的数据结构操作,对数组或链表操作的叫简单选择排序,对堆进行操作的叫堆排序。
简单选择排序| Selection Sort 根据上面选择排序的算法思想,可以很直观得出简单选择排序的算法步骤:
初始化变量 min = 0
作为序列中最小元素的下标; 第 1 趟排序,依次判断L [ j ] < L [ m i n ] L[j]\lt L[min] L [ j ] < L [ min ] ,若是则更新 min
的值,找到最小值后,将其与L [ 0 ] L[0] L [ 0 ] 交换位置,即最小值放在最前面,其位置固定; 以此类推,第i i i 趟排序,从L [ i ] ∼ L [ n ] L[i]\sim L[n] L [ i ] ∼ L [ n ] 中选出最小元素,与L [ i ] L[i] L [ i ] 交换,以固定第i i i 小元素的位置。 动图演示
算法分析 1 2 3 4 5 6 7 8 9 10 void SelectSort (vector<int > &nums) { int min, n = nums.size (); for (int i = 0 ; i < n-1 ; i++){ min = i; for (int j = i+1 ; j < n; j++){ if (nums[min] > nums[j]) min = j; } if (min != i) swap (nums[min],nums[i]); } }
不难得出,简单选择排序元素移动操作的次数最少,不会超过3 ( n − 1 ) 3(n-1) 3 ( n − 1 ) 次,但元素间的比较次数与初始序列无关,始终为n ( n − 1 ) / 2 n(n-1)/2 n ( n − 1 ) /2 次。 即:空间复杂度O ( 1 ) O(1) O ( 1 ) ,时间复杂度O ( n 2 ) O(n^2) O ( n 2 ) 。
堆排序 堆排序是利用堆 这种数据结构而设计的一种排序算法,堆排序是一种选择排序 。
它的最坏,最好,平均时间复杂度均为O ( n log 2 n ) O(n\log_2n) O ( n log 2 n ) ,它也是不稳定排序。
堆的定义 堆 (heap )是计算机科学中一类特殊的数据结构的统称,可以把它视为利用数组 L [ 1.. n ] L[1..n] L [ 1.. n ] 存储的完全二叉树 ,并且满足下列条件中的其中一条(即要么是大根堆要么是小根堆)。
大根堆/大顶堆 :满足L [ i ] ≥ L [ 2 i ] L[i]\geq L[2i] L [ i ] ≥ L [ 2 i ] 且L [ i ] ≥ L [ 2 i + 1 ] L[i]\geq L[2i+1] L [ i ] ≥ L [ 2 i + 1 ] ,即父结点的值始终大于或等于左右孩子结点的值。小根堆/小顶堆 :满足L [ i ] ≤ L [ 2 i ] L[i]\leq L[2i] L [ i ] ≤ L [ 2 i ] 且L [ i ] ≤ L [ 2 i + 1 ] L[i]\leq L[2i+1] L [ i ] ≤ L [ 2 i + 1 ] ,即父结点的值始终小于或等于左右孩子结点的值。一个大根堆的示例如下图所示。
大根堆结构 数组
STL
中的堆堆在 C++ 标准库 STL 中支持以下的基本操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <algorithm> #include <vector> int main (void ) { vector<int > nums = {9 , 43 , -54 , 4 , -13 , 10 , 36 }; make_heap (nums.begin (),nums.end ()); make_heap (nums.begin (),nums.end (),less <int >()); make_heap (nums.begin (),nums.end (),greater <int >()); pop_heap (nums.begin (),nums.end (),less <int >());
堆的维护 当一个堆在下标i i i 处的结构遭到破坏时,我们需要对其进行维护,以使得其通过我们的调整后再次符合堆的定义。
我们定义Max-Heapify ( ) \text{Max-Heapify}() Max-Heapify ( ) 函数是一个用于维护最大堆 性质的过程。输入为一个需要调整的最大堆,即数组A A A 和一个用于开始调整的下标i i i 。
我们假定A [ i ] A[i] A [ i ] 的左右子树代表的二叉树已经都是最大堆,而A [ i ] A[i] A [ i ] 本身有可能小于其孩子,这样就违背了最大堆的性质。于是我们可以调用Max-Heapify ( ) \text{Max-Heapify}() Max-Heapify ( ) 函数,让A [ i ] A[i] A [ i ] 的值在最大堆中“逐级下降 ”,从而使得以下标i i i 为根结点的子树重新遵循最大堆的性质。
Max-Heapify ( ) \text{Max-Heapify}() Max-Heapify ( ) 堆结点i i i 调整时,若其子结点的值大于父结点(即它本身),则进行交换;若本次交换破坏了下一级堆,则再递归地过去调整下一级的堆。
下图是一个调用Max-Heapify ( A , i = 2 ) \text{Max-Heapify}(A,i=2) Max-Heapify ( A , i = 2 ) 的示例。
因为结点i = 2 i=2 i = 2 的左孩子的值A [ 2 i ] > A [ i ] A[2i]\gt A[i] A [ 2 i ] > A [ i ] ,于是将二者交换,此交换可能破坏了左子树代表的堆,于是递归地对结点2 i = 4 2i=4 2 i = 4 进行新的调整。发现此时结点2 i + 1 = 9 2i+1=9 2 i + 1 = 9 有A [ 2 i + 1 ] > A [ i ] A[2i+1]\gt A[i] A [ 2 i + 1 ] > A [ i ] ,所以二者交换。
算法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void MaxHeapify (vector<int > &nums, int i) { int max; int left = 2 *i+1 ; int right = 2 *i+2 ; if (left < nums.size () && nums[left] > nums[i]) max = left; else max = i; if (right < nums.size () && nums[right] > nums[max]) max = right; if (max != i){ swap (nums[i], nums[max]); MaxHeapify (nums, max); }
可以证明,对于树高为h h h 的堆,其维护所需时间复杂度为O ( h ) O(h) O ( h ) .
堆的创建 堆的创建通常是指对于一个给定的无序序列L L L ,将其调整为符合堆定义的顺序。
而这个创建过程一般有两种方式。 方式一是利用在堆中插入元素 的思路。 尽管数组中包含n n n 个元素,也可以假设起初堆中只包含一个元素,然后不断调用插入操作,将后续2 ∼ n 2\sim n 2 ∼ n 的元素依次插入到堆中,这样就将包含n n n 个元素的数组,组织成堆。
方式二是先按照完全二叉树的存储方法对其进行存储,然后再从下往上地进行元素交换调整,使得数组排序满足堆的定义,这个过程也叫“堆化 ”。
这里我们主要介绍方式二。
对于有n n n 个结点的完全二叉树,利用完全二叉树的特性,我们知道最后一个结点的父结点下标是⌊ n / 2 ⌋ \lfloor n/2\rfloor ⌊ n /2 ⌋ ,即L [ ⌊ n / 2 ⌋ ] L[\lfloor n/2\rfloor] L [⌊ n /2 ⌋] 是最后一个分支结点/非终端结点。
自底向上的堆化过程要求:先对⌊ n / 2 ⌋ \lfloor n/2\rfloor ⌊ n /2 ⌋ 为根的子树进行调整 ,然后向前依次对⌊ n / 2 ⌋ − 1 → 1 \lfloor n/2\rfloor-1\to1 ⌊ n /2 ⌋ − 1 → 1 为根的子树进行调整,每次调整时若子结点的值大于父结点,则进行交换;若本次交换导致破坏了下一级堆,则再转过去调整该堆,然后回来继续比对上一个子树,直到根结点结束。
从描述上不难看出,这就是一个从最后一个非终端结点开始不断在调用Max-Heapify ( ) \text{Max-Heapify}() Max-Heapify ( ) 的过程。因此其算法实现也很简单:
1 2 3 4 5 void BuildMaxHeap (vector<int > &nums) { for (i = nums.size ()/2 -1 ; i >= 0 ; i--){ MaxHeapify (nums, i); } }
结合Max-Heapify ( ) \text{Max-Heapify}() Max-Heapify ( ) 的时间复杂度,可以证明,堆的建立可以直接在线性时间内完成,即其时间复杂度为O ( n ) O(n) O ( n ) .
上图是一个建堆的示例,根据前面的描述,i i i 分别取{ ⌊ 10 / 2 ⌋ = 5 , 4 , 3 , 2 , 1 } \{\lfloor 10/2\rfloor=5,4,3,2,1\} {⌊ 10/2 ⌋ = 5 , 4 , 3 , 2 , 1 } (与代码实现不同,默认下标从1开始)
堆的插入 堆的插入问题是指:假设数组中从0 0 0 到i − 1 i-1 i − 1 位置的元素是一个大根堆 ,然后把第i i i 个位置的元素插入大根堆中以便构造一个新的大根堆。
堆的创建有三种方法:交换法、下移法、哨兵法。
交换法 交换法的思想是:将要插入的结点插入末尾,此时其下标为i i i ,则从第i i i 个结点开始,依次和它的父结点进行比较,如果父结点的值小于它就进行交换,依次从下往上比较,直到父结点的值大于它或者到了大根堆的最顶端的根结点时,彻底结束。
下移法 哨兵法 堆的删除 堆排序| Heap Sort 归并排序 归并排序是建立在归并操作 的基础上的一种有效的排序算法。 算法核心是采用分治思想:将已经有的子序列排序后合并,得到完全有序的序列;即先使每个子序列有序,在使子序列段间有序。
二分归并排序| 2-Route Merge Sort 若归并排序中,递归地将待排序表分为两个表进行排序,然后再将两个有序表合并成一个有序表,则称为 2-路归并 ,也叫二分归并排序 。
算法分析 由于采用了分治策略 ,所以我们需要从分解子问题和子问题合并两个方向进行考虑,如下图所示。
先考虑将两个有序表进行合并 的问题。
为了尽可能降低辅助空间的开辟,我们可以考虑仅通过下标定界的方式进行“分割”,即采用 low, high
下标来界定当前子序列的边界,而不是真的开辟一个空间copy出一个子序列。 然而合并的比较过程以及最终元素的位置确定上,则不可避免地需要依赖新的空间,所以我们可以动态申请一个新的数组空间临时使用。
从而,得到如下的合并函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void merge (vector<int > &nums, int low, int mid, int high) { int i, j, k; int *tmp = new int [nums.size ()]; for (i = low; i <= high; i++) tmp[i] = nums[i]; k = low; i = low; j = mid+1 ; while (i <= mid && j <= high){ if (tmp[i] <= tmp[j]){ nums[k++] = tmp[i++]; }else { nums[k++] = tmp[j++]; } } while (i <= mid) { nums[k++] = tmp[i++];} while (j <= high){ nums[k++] = tmp[j++];} delete tmp; }
对于分解。假设我们已经实现了归并排序的算法 MergeSort()
,则一个序列的归并结果,就是其左子序列排序、右子序列排序,最后两个序列合并的过程。 从而可以得到二路归并排序的递归函数:
1 2 3 4 5 6 7 8 void MergeSort (vector<int > &nums, int low, int high) { if (low < high){ int mid = (low+high)/2 ; MergeSort (nums, low, mid); MergeSort (nums, mid+1 , high); merge (nums, low, mid, high) ; } }
算法性能 显然,归并排序与初始序列无关。
空间复杂度:O ( n ) O(n) O ( n ) (merge()
中占n n n 个辅助空间) 时间复杂度:O ( n log 2 n ) O(n\log_2n) O ( n log 2 n ) (每趟归并O ( n ) O(n) O ( n ) ,共进行⌈ log 2 n ⌉ \lceil\log_2n\rceil ⌈ log 2 n ⌉ 趟归并) 稳定性:稳定 其他排序 计数排序| Counting Sort 计数排序要求输入的数据必须是有明确的范围的整数 ,这一点尤为重要。
在此基础上,我们通过增加辅助空间,如数组C [ 1.. n ] C[1..n] C [ 1.. n ] 对每个元素出现的次数进行计数 。该计数过程十分类似于 Hash 表 的单一映射,若L [ i ] = k L[i]=k L [ i ] = k 则C [ k ] ← C [ k ] + 1 C[k]\leftarrow C[k]+1 C [ k ] ← C [ k ] + 1 。
计数完毕之后,再执行C [ k ] ← C [ k ] + C [ k − 1 ] C[k]\leftarrow C[k]+C[k-1] C [ k ] ← C [ k ] + C [ k − 1 ] ,这一步将C [ ⋅ ] C[·] C [ ⋅ ] 中的值更新为 原表L L L 中不大于k k k 的元素的个数,也就是说,此时C [ ⋅ ] C[·] C [ ⋅ ] 中的值就是元素k k k 最后的位置的下标。(针对L L L 中元素各不相同的情况)
于是,再覆盖回到L L L 中:L [ C [ k ] ] ← k L[C[k]]\leftarrow k L [ C [ k ]] ← k ,则得到最终结果。
若L L L 中有相同元素,则只需C [ k ] ← C [ k ] − 1 C[k]\leftarrow C[k]-1 C [ k ] ← C [ k ] − 1 ,然后再覆盖L [ C [ k ] ] ← k L[C[k]]\leftarrow k L [ C [ k ]] ← k 。相当于在原来的基础上将下标左移一位,并放入k k k 。
动图演示如下:
编程实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void CountingSort (vector<int > &nums, int min, int max) { int i, *C = new int [max+1 ]; vector<int > tmp (nums.size(),0 ) ; memset (C,0 ,sizeof (C)); for (i = 0 ; i < nums.size (); i++) C[nums[i]]++; for (i = min; i < max; i++) C[i+1 ] += C[i]; for (i = 0 ; i < nums.size (); i++){ tmp[C[nums[i]]-1 ] = nums[i]; C[nums[i]]--; } nums = tmp; }
可以得出,计数排序可以实现线性时间内 的排序。 时空复杂度均为O ( k + n ) O(k+n) O ( k + n ) 。
基数排序| Radix Sort 待更
桶排序| Bucket Sort 待更
🎲特典:洗牌算法综述