Chapter 6 Sorting
约 796 个字 25 行代码 23 张图片 预计阅读时间 3 分钟
Chapter 6¶
Sorting
Insertion Sort 插入排序¶
-
类似于抓牌时,边拿边理牌
-
步骤:
- 对于新抓的第p个元素,去和前面理好的p-1个元素相比较,比它大就交换
- 直到找到恰比它小的数字
void InsertionSort(int a[],int n){
int temp;
for (int i=1;i<n;i++){
temp = a[i];
for (int j=i-1;j>=0&&a[j]>temp;j--){
a[j+1]=a[j];
}
//错误代码:a[j+1]=temp;
a[j+2]=temp;
//问题就在于for条件不满足后仍会j--,可以把j--挪至循环内部
}
}
-
worst case: \(T(N)=O(N^2)\)
-
best case: \(T(N)=O(N)\)
- 因为每次相邻元素的交换只会使逆序对减1,那么插入排序的总操作次数,就是N个元素过一把,加上I次swap
\(T(N,I)=O(I+N)\)
两个定理:
- 对于一个N元素数组,它逆序对的个数为, \(I=C_n^2 * 0.5 = N(N-1)/4\)
- 那么凡是通过交换相邻元素实现排序的算法,一定是有平均复杂度 \(\Omega (N^2)\)
Shell sort 希尔排序¶
-
每间隔n来取数,并把取的数字进行
insertion sort
(这种非相邻元素的交换,可以保证逆序对的纠正 ≥1) -
间隔依次减小,最后减小到1
- 而且进行 \(h_{k-1}\)-sorted时,会保证 \(h_k\)-sorted
- increment sequence 的取法
- 希尔本人是 除2来取,但实际上并不好
Hibbard’s Increment Sequence:¶
- \(h_k=2^k-1\)
- 这样子 \(h_i\)之间彼此互质
int k=log2(n);
for (;k>0;k--){
int incremtnent=pow(2,k)-1;
for (int i=increment;i<n;i++){
int temp=a[i];
int j;
for (j=i-increment;j>=0&&a[j]>temp;){
a[j+increment]=a[j];
j-=increment
}
a[j+increment]=temp;
}
}
In-place 与 stable¶
-
in-place:
如果一个算法是就地的,那么它在处理数据时不需要或只需要固定的额外空间。
例如,许多排序算法(如冒泡排序、插入排序、选择排序和快速排序)可以被实现为就地排序,这意味着它们可以在原数组上进行排序,而不需要创建一个新的等大小的数组。
就地算法的优点是它们通常比非就地算法更节省内存。然而,就地算法可能会修改输入数据,这在某些情况下可能是不可接受的。
-
stable:
等值元素的相对位置没有发生改变
in-place | stable | |
---|---|---|
shell sort | \(\checkmark\) | \(\times\) |
insertion sort | \(\checkmark\) | \(\times\) |
Heap sort 堆排序¶
- 记得选择排序 \(O(N^2)\)就在于每次遍历n元素才能确定最小值,于是构建最小堆,就可以用 \(O(logN)\)来找到最小值
- 算法一有点浪费空间,
- 我们会发现每次deletemin 完,数组末尾会有空余位置,于是有了算法二的想法
-
我们采用
MaxHeap
-
每次 DeleteMax 都是在把 H[0] 与 H[tail]交换
-
然后pecolatedown(0);
- \(T(N)=O(NlogN)\)
-
无论best、worst、average case 都是很好的NlogN
-
这个版本是
in-place, 非 stable
Merge sort 归并排序¶
-
前置思想:Merge两个有序list,就是和合并链表一样,从头比较
-
于是
MergeSort(分治思想)
要点解析
if(Left < Right)
就代表了递归停止的时候,只剩一个元素(Left=Right) 先递归Msort(也就是分半),再调用Mergevoid Mergesort
就是个TmpArray
启动器, 记得给TmpArray分配空间,调用Msort时是从0~N-1
Analysis¶
-
\(T(N)=O(NlogN)\)
-
不是
in-place
但是stable
Quick sort 快速排序¶
- 先找到一个
pivot
然后整个序列进行一个partition,一部分比pivot小,一部分比它大;然后递归进行
Picking the Pivot¶
- 问题在于 如何选择一个好的
pivot
Abstact
综上:选取位置上的第一个,最后一个和中间数,选择其中这三个里的中间大小的数
Partition strategy¶
- 把选取的
pivot
移至末尾 - 给定 i,j 两个指针分别指向 a[0]和a[n-2]
- i负责比pivot大的部分,j负责比pivot小的部分
- 一旦有一方不符合,直接互换a[i]和a[j]
- 最后 j会在i前面,且两者相邻,于是我们交换末尾a[n-1]的pivot 和 a[i]
注意
- 当 a[i] or a[j] == pivot 时,我们就互换一下 a[i]和a[j]就好了,然后让i和j继续往前走
Small Array¶
- 当数据量 \(N \leq 10\)时,QuickSort实际没有insertion sort来的快,于是我们可以在partition到10以内的数据量时调用insertion sort
Implementation¶
Tip
- 注意细节:
pivot
被放在了A[Right-1]
- 是因为我们真正需要 管理排序的部分 其实是 A[Left+1] 到 A[Right-1]
Tip
- 注意细节:为了配合
++i,—-j
i和j 分别初始化为 Left和Right-1
Analysis¶
Kth largest¶
-
因为在quicksort的partition环节,我们可以确定pivot是数组中第i大的数(从0开始记),于是
-
k≥i,去右边找;k≤i,去左边找
Table Sort¶
-
Problem:交换两个特别大的结构是十分expensive的
-
Solution:只是挪动 指针
General Lower Bound for Sorting¶
-
排序算法的下界
-
任何用比较实现的算法只能有 \(\Omega(NlogN)\)的最坏情况时间复杂度
Bucket Sort and Radix Sort¶
Bucket Sort¶
-
我们划分出M个桶来分装 N个元素
-
当 N>>M时,是极好的 \(T(N,M)=O(N)\)
- 当M>>N时
- 我们观察到虽然 M很大,但是1000这个数字实际上位数不多,于是便有了 Radix Sort 基数排序
Radix Sort¶
- 依次基于位数字进行的桶排序
-
利用上一轮排序的结果,可以快速得到下一个桶的排序
-
\(T(N)=O(P(N+B))\)
-
P是Pass数也就是位数,N+B就是每次pass的桶排序