最近看了《算法导论》的排序第二部分,想写点什么,总结一下我的学习。
排序算法分两种:
一种是比较排序,时间复杂度最少达到O(n*lg n),主要有:插入排序,冒泡排序,选择排序,合并排序,堆排序,快速排序等。
另一种是非比较排序,时间复杂度可以达到O(n),也称为线性时间排序,主要有:计数排序,基数排序,桶排序等。
输入:n个数<a1,a2,a3.............an>
输出:输入序列的一个重排列<a'1,a'2,a'3........a'n>,使a'1<a'2<a'3<........<a'n
插入排序
当前项是第j项,j从2开始到len-1,循环插入前面已排好序的j-1项。
c++实现:
//INSERTION-SORT(A)
#include<iostream>
using namespace std;
void insertion_sort(int*,int*);
int main(){
int A[10]={97,3,2,4,54,45,25,23,44,99};
insertion_sort(A,A+10);
// 相当于STL中的 void __insertion_sort(RandomAccessIterator first,
// RandomAccessIterator last)函数 (头文件#include<algorithm>)
for(int i=0;i<10;i++)
cout<<A[i]<<" ";
}
void insertion_sort(int*first,int*last){
int len=last-first;
int key;
for(int j=1;j<len;j++){
key=*(first+j);
//将A[i]插入已排好序的A[0...j-1]
int i=j-1;
while(i>=0&&*(first+i)>key){
*(first+i+1)=*(first+i);
i--;
}
*(first+i+1)=key;
}
}
插入排序的最坏情况是刚好逆序,最坏情况时间代价是θ(n^2)
。这种算法效率不高。
合并排序
合并排序用到了分治模式,是分治模式的典型算法。
分治模式:1)分解 2)解决 3)合并
合并排序完全依照上述模式,直观操作如下:
1)分解 将n个元素分成各含n/2个元素的子序列
2)解决 用合并排序法对两个字序列递归的排序
3)合并 合并两个已排序的子序列以得到排序结果
C++实现:
//MERGE-SORT(A)
#include<iostream>
#define MAX 9999999
using namespace std;
void merge_sort(int A[],int p,int r);
void merge(int A[],int p,int q,int r);
int main(){
int A[10]={97,3,2,4,54,45,25,23,44,99};
merge_sort(A,0,9);
for(int i=0;i<10;i++)
cout<<A[i]<<" ";
}
void merge(int A[],int p,int q,int r){
int n1=q-p+1;
int n2=r-q;
int L[1000],R[1000];
for(int i=1;i<=n1;i++) L[i]=A[p+i-1];
for(int i=1;i<=n2;i++) R[i]=A[q+i];
L[n1+1]=MAX;
R[n2+1]=MAX;
int i=1,j=1;
for(int k=p;k<=r;k++){
if(L[i]<=R[j]){
A[k]=L[i];
i++;
}
else{
A[k]=R[j];
j++;
}
}
}
void merge_sort(int A[],int p,int r){
int q;
if(p<r){
q=(p+r)/2;
merge_sort(A,p,q);
merge_sort(A,q+1,r);
merge(A,p,q,r);
}
}
合并排序时间复杂度为θ(nlgn)。达到了比较排序算法的最佳,是渐进最优的。
堆排序
二叉堆是完全二叉树或者是近似完全二叉树。
二叉堆满足:
1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。
当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。
堆排序过程:
首先要建最大堆build_max_heap(),在建堆的时候需要用到保持堆的性质的函数max_heapify()。
然后就是利用了最大堆的特性,即根节点的值始终是最大值。在完成最大堆之后,把根节点的值与数组最后一个数相互交换swap(A[0],A[i]),再令堆的大小减一heap_size--,则最后一个数A[i]就是减一之前的堆得最大值。
buid_max_heap过程图示:
heap_sort过程图示:
(图片来自http://images.cnblogs.com/cnblogs_com/kkun/201111/201111231437383012.png)
c++代码:
//HEAPSORT-SORT(A)
#include<iostream>
using namespace std;
int heap_size; //堆的大小
int len; //数组A的大小
void max_heapify(int[],int);
void buid_max_heap(int []);
void heap_sort(int []);
int main(){
int A[10]={97,3,2,4,54,45,25,23,44,99};
len=sizeof(A)/4;
heap_sort(A);
for(int i=0;i<10;i++)
cout<<A[i]<<" ";
}
void max_heapify(int A[],int i){
int l=i*2+1;
int r=i*2+2;
int largest;
if(l<heap_size&&A[l]>A[i])
largest=l;
else largest=i;
if(r<heap_size&&A[r]>A[largest])
largest=r;
if(largest!=i){
swap(A[i],A[largest]);
max_heapify(A,largest);
}
}
void buid_max_heap(int A[]){
heap_size=len;
for(int i=(len-1)/2;i>=0;i--)
max_heapify(A,i); //堆从(len-1)/2到0都是非叶子结点
}
void heap_sort(int A[]){
buid_max_heap(A);
for(int i=len-1;i>=1;i--){
swap(A[0],A[i]);
//现在A[i]就是最大值
heap_size--;
max_heapify(A,0);
}
}
快速排序
基于分治模式,
对一个典型子数组A[p...r]排序的分治过程为三个步骤:
1.分解:
A[p..r]被划分为俩个(可能空)的子数组A[p ..q-1]和A[q+1 ..r],使得
A[p ..q-1] <= A[q] <= A[q+1 ..r]
2.解决:通过递归调用快速排序,对子数组A[p ..q-1]和A[q+1 ..r]排序。
3.合并。
快速排序算法的时间复杂度为O(n*lgn),最坏为O(n^2),不稳定。
快排通常是用于排序的最佳选择。因为,排序最快,也只能达到O(nlgn)。
快排中,递归是重点。
还需要一个子函数partition,把原数组分成如下形式:
Partition过程图解:
C++代码:
//QUICK-SORT(A)
#include <iostream>
using namespace std;
void quick_sort(int[],int,int);
int partition(int[],int,int);
int main()
{
int A[]={12,23,44,32,122,345,454,2,211,21};
quick_sort(A,0,9);
for(int i=0;i<10;i++)
cout<<A[i]<<" ";
cout<<endl;
return 0;
}
void quick_sort(int A[],int p,int r){
if(p<r){
int q=partition(A,p,r);
quick_sort(A,p,q-1);
quick_sort(A,q+1,r);
}
}
int partition(int A[],int p,int r){
int x=A[r];
int i=p-1;
for(int j=p;j<=r-1;j++){
if(A[j]<=x){
i++;
swap(A[i],A[j]);
}
}
swap(A[i+1],A[r]);
return i+1;
}
计数排序
计数排序是线性时间排序,运行时间θ(n),是稳定不是原地排序,需要占用很大的内存空间,以空间换时间。但是计数排序有个缺点,要排序的数必须是一个小范围内的数,在实践中,当k=O(n)时,常采用计数排序。
它对输入的数据的限制条件:
1、输入的线性表的元素属于有限偏序集S;
2、设输入的线性表的长度为n,|S|=k(表示集合S中元素的总数目为k),则k=O(n)。
在这两个条件下,计数排序的复杂性为O(n)。
在我看来,计数排序比较类似于hash-table,哈希表,过程类似哈希表的直接寻址法,计数排序算法没有用到元素间的比较,它利用元素的实际值来确定它们在输出数组中的位置。例如,如果输入序列中只有7个元素的值小于a的值,则a可以直接存放在输出序列的第8个位置上。
计数排序图示:
由于算法使用了downto语句(如上图c所示,A从后向前),经计数排序,输出序列中值相同的元素之间的相对次序与他们在输入序列中的相对次序相同,换句话说,计数排序算法是一个稳定in-place的排序算法。
c++代码:
//COUNTING-SORT(A)
#include<iostream>
#define LENGTH 10
#define MAX 101
using namespace std;
void counting_sort(int[],int[]);
int main(){
int A[10]={12,3,13,32,43,46,23,23,2,12}; //全部都是100以内的非负数
int B[10];
counting_sort(A,B);
for(int i=0;i<10;i++)
cout<<B[i]<<" ";
cout<<endl;
}
void counting_sort(int A[],int B[]){
int c[MAX]; //从0~100
for(int i=0;i<MAX;i++)
c[i]=0;
for(int j=0;j<LENGTH;j++)
c[A[j]]++;
//c[i]包含等于i的元素个数
for(int i=1;i<MAX;i++)
c[i]=c[i]+c[i-1];
//c[i]包含小于等于i的元素个数
for(int j=LENGTH-1;j>=0;j--){
B[c[A[j]]-1]=A[j];
c[A[j]]--;
}
}
java代码:
public class Main {
static int A[]={12,3,13,32,43,46,23}; //全部都是100以内的非负数
static int B[]=new int[A.length];
public static void main(String[]args){
countingSort(A,B);
for(int i=0;i<B.length;i++)
System.out.println(B[i]);
}
public static void countingSort(int A[],int B[]){
int k=101; //从0~100
int c[]=new int [k];
for(int i=0;i<k;i++)
c[i]=0;
for(int j=0;j<A.length;j++){
c[A[j]]=c[A[j]]+1;
}
//c[i]包含等于i的元素个数
for(int i=1;i<k;i++)
c[i]=c[i]+c[i-1];
//c[i]包含小于等于i的元素个数
for(int j=A.length-1;j>=0;j--){
B[c[A[j]]-1]=A[j];
c[A[j]]=c[A[j]]-1;
}
}
}
基数排序
将整数按位数切割成不同的数字,然后按每个位数分别比较。
设待排序列为n个记录,d个关键码,关键码的取值范围为[0,k-1],则进行链式基数排序的时间复杂度为O(d(n+k)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(k),共进行d趟分配和收集。
桶排序
(未完待续)