算法导论(1)排序算法(二)
接着之前的来:算法导论(1)排序算法(一)
前面我们已经介绍了两种排序算法,插入排序和归并排序。插入排序最坏情况下可以在Θ(
n
2
n^2
n2)时间内将n个数排好序,对于小规模输入,插入排序是一种非常快的原址排序算法(如果输入数组中仅有常数个元素需要在排序过程中存储在数组之外,则称算法是原址的)。归并排序有更好的渐进运行时间
Θ
(
n
lg
n
)
\Theta (n \lg n)
Θ(nlgn),但它所使用的MERGE过程不是原址的。
接下来我们会介绍几种新的排序算法,如 堆排序、快速排序、计数排序、基数排序、桶排序。他们的运行时间的比较如下:
算法 | 最坏情况运行时间 | 平均情况/期望运行时间 |
---|---|---|
插入排序 | Θ ( n 2 ) \Theta (n^2) Θ(n2) | Θ ( n 2 ) \Theta (n^2) Θ(n2) |
归并排序 | Θ ( n lg n ) \Theta (n \lg n) Θ(nlgn) | Θ ( n lg n ) \Theta (n \lg n) Θ(nlgn) |
堆排序 | O ( n lg n ) O(n \lg n) O(nlgn) | – |
快速排序 | Θ ( n 2 ) \Theta (n^2) Θ(n2) | Θ ( n lg n ) \Theta (n \lg n) Θ(nlgn) |
计数排序 | Θ ( k + n ) \Theta (k+n) Θ(k+n) | Θ ( n lg n ) \Theta (n \lg n) Θ(nlgn)(期望) |
基数排序 | Θ ( d ( n + k ) ) \Theta (d(n+k)) Θ(d(n+k)) | Θ ( d ( n + k ) ) \Theta (d(n+k)) Θ(d(n+k)) |
桶排序 | Θ ( n 2 ) \Theta (n^2) Θ(n2) | Θ ( n ) \Theta (n) Θ(n)(平均情况) |
顺序统计量:一个n个数的集合的第i个顺序统计量就是集合中第i小的数。
1.堆排序
堆排序的时间复杂度与归并排序一样,但不同的是堆排序具有空间原址性。所以堆排序集合了插入排序与归并排序二者的优点。
首先要弄懂堆的概念。堆是一个数组,可以把他看成一个近似的完全二叉树,书上的每个节点对应数组中的应给元素。二叉堆可以分为两种形式:最大堆和最小堆。把一个堆中的节点的高度定义为该节点到叶结点最长简单路径上边的数目,这样堆的高度就可以定义为根节点的高度。那么一个包含n个元素的堆的高度是 Θ ( lg n ) \Theta (\lg n) Θ(lgn)。接下来我们将介绍一些基本过程:
- MAX-HEAPIFY过程:时间复杂度为 O ( lg n ) O(\lg n) O(lgn),维护最大堆性质的关键
- BUILD-MAX-HEAP过程:线性时间复杂度,功能是从无序的输入数据数组中构造一个最大堆
- HEAPSORT过程:时间复杂度为 O ( n lg n ) O(n\lg n) O(nlgn),功能是对一个数组进行原址排序
- MAX-HEAP-INSERT、HEAP-EXTRACT-MAX、HEAP-INCREASE-KET和HEAP-MAXIMUM过程:时间复杂度为 O ( lg n ) O(\lg n) O(lgn),利用堆实现一个优先队列
维护堆的性质
MAX-HEAPIFY是用于维护最大堆性质的重要过程。它输入一个数组A和一个下标i,假定根节点为LEFT(i)和RIGHT(i)的二叉树都是最大堆,但A[i]有可能小于其孩子,所以这个过程中让A[i]的值在最大堆中逐级下降是下标为i的根节点的子树重新遵循最大堆的性质。
MAX-HEAPIFY(A,i)
l=LEFT(i)//2i
r=RIGHT(i)//2i+1
if l<=A.heap-size and A[l]>A[i]
largest=l
else largest=i
if r<=A.heap-size and A[r]>A[largest]
largest=r
if largest!=i
exchange A[i] with A[largest]
MAX-HEAPIFY(A,largest)
建堆
用自底向上的方法利用过程MAX-HEAPIFY把一个大小为n=A.length的数组A[1…n]转换为最大堆。
BULID-MAX-HEAP(A)
A.heap-size=A.length
for i=[A.length//2] downto 1
MAX-HEAPIFY(A,i)
堆排序算法
好了,终于到了我们的堆排序算法。先用前面的算法建堆,此时我们最大值为A[1],让A[1]与A[n]互换并去掉节点n,将其放到正确的位置。不断这么做。直到堆的大小降到2.
HEAPSORT(A)
BUILD-MAX-HEAP(A)
for i=A.length downto 2
exchange A[1] with A[i]
A.heap-size=A.heap-size-1
MAX-HEAPIFY(A,1)
C++版本,容器还是用的vector,C++的数组真有点用不习惯,所以能用vector的我基本都用vector好了。完全按照这些步骤来的话程序还是不容易出现bug的
#include<iostream>
#include<vector>
using namespace std;
void max_heapify(vector<int>& A, int i) {
int l = 2 * (i + 1) - 1, r = 2 * (i + 1);
int largest = (l<A.size() && A[l]>A[i]) ? l : i;
largest = (r<A.size() && A[r]>A[largest]) ? r : largest;
if (largest != i) {
int temp = A[largest];
A[largest] = A[i];
A[i] = temp;
max_heapify(A, largest);
}
}
void build_max_heap(vector<int>& A) {
for (int i = A.size() / 2; i >= 0; --i) {
max_heapify(A, i);
}
}
vector<int> heap_sort(vector<int>& A) {
vector<int>B;
build_max_heap(A);
for (int i = A.size() - 1; i >= 1; --i) {
int temp = A[0];
A[0] = A[i];
A.erase(A.begin() + i);
B.insert(B.begin(),temp);
max_heapify(A, 0);
}
B.insert(B.begin(), A[0]);
return B;
}
int main() {
vector<int>A= { 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2 };
A = heap_sort(A);
for (const auto c : A)
cout << c << endl;
}
Java版本出现了一个小问题,由于Java的数组初始化以后是不可以直接删除或添加元素的,但可以通过创建新数组赋值的方式来删除添加元素,利用这一点我们还是可以实现算法,但这样一来感觉又不如C++版本的简洁了,不过Java中有其他容器,可能用其他容器会好点。
import org.omg.PortableInterceptor.SYSTEM_EXCEPTION;
public class algo_heapsort {
public int[] max_heapify(int[] A,int i){
int l = 2 * (i + 1) - 1, r = 2 * (i + 1);
int largest = (l<A.length && A[l]>A[i]) ? l : i;
largest = (r<A.length && A[r]>A[largest]) ? r : largest;
if (largest != i) {
int temp = A[largest];
A[largest] = A[i];
A[i] = temp;
A=max_heapify(A, largest);
}
return A;
}
public int[] build_max_heap(int[] A){
for (int i = A.length / 2; i >= 0; --i) {
A=max_heapify(A, i);
}
return A;
}
public int[] heap_sort(int[] A){
int[] B=new int[A.length];
A=build_max_heap(A);
int k=A.length-1;
for (int i = A.length - 1; i >= 1; --i) {
int temp = A[0];
A[0] = A[i];
B[k--]=temp;
int[] a=new int[A.length-1];
int ind=0;
for(int j=0;j<A.length;++j){
if (j!=i){
a[ind++]=A[j];
}
}
A=max_heapify(a, 0);
}
// for(int c:A){
//System.out.println(c);
//}
B[k]=A[0];
return B;
}
public static void main(String[] args) {
int A[]={ 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2 };
A=new algo_heapsort().heap_sort(A);
for(int c:A){
System.out.println(c);
}
}
}
python版本,整体没太大变化,但要时刻注意python里面的range(a,b)是从a到b包括a 不包括b的,这一点非常容易忽视,C++和Java的话没有这样的问题,区间的开闭完全自己控制,但python语言上的简化使得这个地方直接默认了区间开闭了,稍不注意就会犯错。
def max_heapify(A,i):
l,r=2*(i+1)-1,2*(i+1)
largest=l if l<len(A) and A[l]>A[i] else i
largest=r if r<len(A) and A[r]>A[largest] else largest
if(largest!=i):
temp=A[largest]
A[largest]=A[i]
A[i]=temp
A=max_heapify(A,largest)
return A
def build_max_heap(A):
for i in range(len(A)//2,-1,-1):
A=max_heapify(A,i)
return A
def heap_sort(A):
B=[]
A=build_max_heap(A)
print(A)
for i in range(len(A)-1,0,-1):
temp=A[0]
print(temp)
B.insert(0,temp)
A[0]=A[i]
A.pop(i)
A=max_heapify(A,0)
B.insert(0,A[0])
return B
A=[1,2,3,42,3,4 ,1,2,4,22,44,22,123,2]
B=heap_sort(A)
print(B)
优先队列(priority queue)
优先队列是一种用来维护由一组元素构成的集合S的数据结构,其中每个元素都有一个相关的值,称为关键字。一个最大优先队列支持一下操作:
- INSERT(S,x):把元素插入集合S中
- MAXIMUM(S):返回S中具有最大关键字的元素
- EXTRACT-MAX(S):去掉并返回S中的具有最大关键字的元素
- INCREASE-KEY(S,x,k):将元素x的关键字值增加到k,假设k的值不小于x的原关键字值
HEAP-MAXIMUM(A)
return A[1]
HEAP-EXTRACT-MAX(A)
if A.heap-size<1
error "heap underflow"
max=A[1]
A[1]=A[A.heap-size]
A.heap-size=A.heap-size-1
MAX-HEAPIFY(A,1)
return max
HEAP-INCREASE-KEY(A,i,key)
if key<A[i]
error" new key is smaller than current key"
A[i]=key
while i>1 and [PARENT(i)]<A[i]
exchange A[i] with A[PARENT(i)]
i=PARENT(i)
2.快速排序
快速排序最坏情况时间复杂度为 Θ ( n 2 ) \Theta (n^2) Θ(n2),但它通常是实际排序应用中最好的选择,因为它的平均性能很好,它的期望时间复杂度为 Θ ( n lg n ) \Theta (n \lg n) Θ(nlgn)(而且其中隐含的常数因子非常小),它还能原址排序。
与归并排序一样,快速排序使用了分治思想。下面是快速排序的三步分治过程:
- 分解:数组A[p…r]划分为两个子数组A[p…q-1]和A[q+1…r],使得A[p…q-1]中的每一个元素都小于等于A[q],而A[q]小于等于A[q+1…r]中的每个元素
- 解决:通过递归调用快速排序,对子数组A[q…q-1]和A[q+1…r]进行排序
- 合并:因为子数组都是原址排序的所以不需要合并操作
QUICKSORT(A,p,r)
if p<r
q=PARTITION(A,p,r)
QUICKSORT(A,p,q-1)
QUICKSORT(A,q+1,r)
算法的关键是数组的划分过程
PARTITION(A,p,r)
x=A[r]
i=p-1
for j=p to r-1
if A[j]<=x
i=i+1
exchange A[i] with A[j]
exchange A[i+1] with A[r]
return i+1
C++版本
#include<iostream>
#include<vector>
using namespace std;
int partition(vector<int>& A, int p, int r) {
int x = A[r],i = p - 1;
for (int j = p; j < r; ++j) {
if (A[j] <= x) {
int temp = A[++i];
A[i] = A[j];
A[j] = temp;
}
}
int temp = A[i + 1];
A[i + 1] = A[r];
A[r] = temp;
return i + 1;
}
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);
}
}
int main() {
vector<int>A = { 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2 };
quickSort(A, 0, A.size() - 1);
for (const auto c : A)
cout << c << endl;
}
快速排序的性能分析
先讨论快速排序的性能,在最坏情况下时间复杂度为 Θ ( n 2 ) \Theta (n^2) Θ(n2),此时快速排序并不比插入排序好。而在最好情况下快速排序的时间复杂度为 Θ ( n l g n ) \Theta (n \\lg n) Θ(nlgn)。好在快速排序的平均运行时间更接近于其最好情况。
快速排序的随机化版本
通过在算法中引入随机性可以使算法对于任何输入都能获得较好的期望性能。对于快速排序我们采用一种称为随机抽样的随机化计数。
如下,在前面的版本稍微修改
RANDOMIZED-PARTITION(A,p,r)
i=RANDOM(p,r)
exchange A[r] with A[i]
return PARTITION(A,p,r)
RANDOMIZED-QUICKSORT(A,p,r)
if p<r
q=RANDOMIZED-PARTITION(A,p,r)
RANDOMIZED-QUICKSORT(A,p,q-1)
RANDOMIZED-QUICKSORT(A,q+1,r)
C++本版,在前面的基础上稍微修改一下就行了,这两个版本表面上差别不大,但实际上是很不一样的,随机化版本不那么容易受输入的影响,所以具有更好的性能。
#include<iostream>
#include<vector>
#include<ctime>
using namespace std;
int partition(vector<int>& A, int p, int r) {
int x = A[r],i = p - 1;
for (int j = p; j < r; ++j) {
if (A[j] <= x) {
int temp = A[++i];
A[i] = A[j];
A[j] = temp;
}
}
int temp = A[i + 1];
A[i + 1] = A[r];
A[r] = temp;
return i + 1;
}
void quickSort(vector<int>& A, int p, int r) {
if (p < r) {
srand(time(0));
int i = rand() % (r - p) + p;
int temp = A[i];
A[i] = A[r];
A[r] = temp;
int q = partition(A, p, r);
quickSort(A, p, q - 1);
quickSort(A, q + 1, r);
}
}
int main() {
vector<int>A = { 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2 };
quickSort(A, 0, A.size() - 1);
for (const auto c : A)
cout << c << endl;
}
Java版本
public class algo_quick_sort {
public int partition(int[] A, int p, int r) {
int x = A[r], i = p - 1;
for (int j = p; j < r; ++j) {
if (A[j] <= x) {
int temp = A[++i];
A[i] = A[j];
A[j] = temp;
}
}
int temp = A[i + 1];
A[i + 1] = A[r];
A[r] = temp;
return i + 1;
}
public int[] quickSort(int[] A, int p, int r) {
if (p < r) {
int i = (int) (p + Math.random() * (r - p - 1));
int temp = A[i];
A[i] = A[r];
A[r] = temp;
int q = partition(A, p, r);
A = quickSort(A, p, q - 1);
A = quickSort(A, q + 1, r);
}
return A;
}
public static void main(String[] args) {
int A[] = {1, 2, 3, 42, 3, 4, 1, 2, 4, 22, 44, 22, 123, 2};
A = new algo_quick_sort().quickSort(A, 0, A.length - 1);
for (int c : A) {
System.out.println(c);
}
}
}
python版本
from random import randint as ra
def partition(A,p,r):
x,i=A[r],p-1
for j in range(p,r):
if A[j]<=x:
i+=1
temp=A[i]
A[i]=A[j]
A[j]=temp
temps=A[i+1]
A[i+1]=A[r]
A[r]=temps
return i+1
def quickSort(A,p,r):
if(p<r):
i=ra(p,r)
temp=A[i]
A[i]=A[r]
A[r]=temp
q=partition(A,p,r)
A=quickSort(A,p,q-1)
A=quickSort(A,q+1,r)
return A
A=[1, 2, 3, 42, 3, 4, 1, 2, 4, 22, 44, 22, 123, 2]
A=quickSort(A,0,len(A)-1)
print(A)
后面的是线性时间排序,作为一个整体,包括计数排序、基数排序、桶排序,这次就到此为止,剩下的放到下次。