算法导论(1)排序算法(一)
排序问题:
输入:包含n个数的一个序列
<
a
1
,
a
2
,
.
.
.
,
a
n
>
<a_1,a_2,...,a_n>
<a1,a2,...,an>
输出:输出一个序列的排序
<
a
1
′
,
a
2
′
,
.
.
.
,
a
n
′
>
,
满
足
a
1
′
≤
a
2
′
≤
.
.
.
≤
a
n
′
<a_1',a_2',...,a_n'>,满足a_1'\le a_2'\le...\le a_n'
<a1′,a2′,...,an′>,满足a1′≤a2′≤...≤an′
1.插入排序(增量法)
对于插入排序,我们将其伪代码过程命名为INSERTION-SORT。对于数组A,
INSERTION-SORT(A)
for j=2 to A.length
key=A[j]
i=j-1
while i>0 and A[i]>key
A[i+1]=A[i]
i=i-1
A[i+1]=key
C语言版本
#include<stdio.h>
int sort(int A[],int len) {//传入的是数组的首地址
for (int j =1; j < len;++j) {
printf("%d ",A[j]);
int key = A[j];
int i = j - 1;
while (i > 0 && A[i] > key) {
A[i + 1] = A[i];
--i;
}
A[i + 1] = key;
}
return 0;
}
int main() {
int A[] = { 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2};
sort(A,sizeof(A)/sizeof(A[0]));
for (int i = 0; i < sizeof(A) / sizeof(A[0]); ++i) {
printf("%d \n", A[i]);
}
return 0;
}
C++版本
#include<iostream>
int sort(int A[], int len) {
for (int j = 1; j < len; ++j) {
int key = A[j];
int i = j - 1;
while (i > 0 && A[i] > key) {
A[i + 1] = A[i];
--i;
}
A[i + 1] = key;
}
return 0;
}
int main() {
int a[]= { 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2 };
sort(a,std::size(a));
for (const auto c : a) {
std::cout << c << std::endl;
}
}
C++ vector版本,用指向vector的指针
由于用C语言时函数中是传入数组的首地址,函数无法得知数组的大小,所以函数需要额外传入一个size参数,但对于C++,我们用vector的话就不用额外传入一个size参数,我们形参可以定义为指向vector的指针或引用。
#include<iostream>
#include<vector>
int sort(std::vector<int> *A, int len) {
for (int j = 1; j < A->size(); ++j) {
int key = (*A)[j];
int i = j - 1;
while (i > 0 && (*A)[i] > key) {
(*A)[i + 1] = (*A)[i];
--i;
}
(*A)[i + 1] = key;
}
return 0;
}
int main() {
std::vector<int> aa= { 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2 };
sort(&aa,std::size(aa));
for (const auto c : aa) {
std::cout << c << std::endl;
}
}
C++ vector版本,用vector的引用
#include<iostream>
#include<vector>
int sort(std::vector<int> &A) {
for (int j = 1; j < A.size(); ++j) {
int key = A[j];
int i = j - 1;
while (i > 0 && A[i] > key) {
A[i + 1] = A[i];
--i;
}
A[i + 1] = key;
}
return 0;
}
int main() {
std::vector<int> aa= { 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2 };
sort(aa);
for (const auto c : aa) {
std::cout << c << std::endl;
}
}
Java 版本
public class algo_sort_1 {
public void sort(int[] a){
for(int j=1;j<a.length;++j){
int key=a[j];
int i=j-1;
while(i>0 && a[i]>key){
a[i+1]=a[i];
--i;
}
a[i+1]=key;
}
}
public static void main(String[] args) {
int []a={ 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2 };
new algo_sort_1().sort(a);
for(int c:a){
System.out.println(c);
}
}
}
python版本
def sort(a):
for j in range(1, len(a)):
key=a[j]
i=j-1
while(i>0 and a[i]>key):
a[i+1]=a[i]
i-=1
a[i+1]=key
aa=[ 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2]
sort(aa)
print(aa)
可以看出python版本的最简洁,当然也没什么用。
本来还可以有Fortran版本的,但觉得还是算了,Fortran这门语言感觉就是旧时代的残党。
分析算法
详细的理论分析可以参考《算法导论》一书。这里我就简单地说明一下。
一般来说一个算法的运行时间是其输入规模的函数,比如我们这个插入排序算法,输入规模为输入序列的项数n,运行时间为n的函数。但这个函数是怎样的我们需要去分析(需要分情况讨论)。具体的分析过程我略了,直接来说结论:
- 最好的情况的运行时间为n的线性函数。(如果序列已经排好序了那自然是最好情况)
- 最坏的情况的运行时间为n的二次函数。(如果序列正好是反向排序的)
对于一个算法的分析我们往往将注意力集中在最坏的情况。
所以我们记插入排序具有最坏情况运行时Θ(n2)
2.归并排序(分治法)
首先先说说分治法:将原问题分解成几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。
归并排序遵循分治模式,操作如下:
- 分解:分解待排序的n个元素的序列成各具n/2个元素的两个子序列
- 解决:使用归并排序递归地排序两个子序列
- 合并:合并两个已排序的子序列以产生已排序的答案
归并排序的关键操作时“合并”步骤中已排序序列的合并。我们通过过程MERGE(A,p,q,r)来完成合并。此时数组A[p…q]和A[q+1…r]都已经排好序了。这个过程将合并两个子数组。
过程MERGE需要Θ(n)的时间,其中n=r-q+1为待合并的元素总数。
伪代码如下:
MERGE(A,p,q,r)
n_1=q-p+1
n_2=r-q
let L[1..n_1+1] and R[1..n_2+1] be new arrays
for i=1 to n_1
L[i]=A[p+i-1]
for j=1 to n_2
R[i]=A[q+j]
L[n_1+1]=INFTY
R[n_2+1]=INFTY
i=1
j=1
for k=p to r
if L[i]<=R[j]
A[k]=L[i]
i=i+1
else A[k]=R[j]
j=j+1
从伪代码我们可以容易的看出算法的过程,不过这个过程只是归并排序最重要的一步,看懂了这一过程其他步骤也不难。我们把上面过程作为一个子程序,整个过程如下:
MERGE-SORT(A,p,r)
if p<r
q=[(p+r)/2]
MERGE-SORT(A,p,q)
MERGE-SORT(A,q+1,r)
MERGE(A,p,q,r)
由于C语言定义数组时必须数组的大小必须是字面值导致要实现本算法存在难度,主要还是我对C语言不熟悉,只会一些简单的操作,所以这里就不用C语言实现了。
C++版本,这里完全按照伪代码的步骤来,容器是用的vector,vector还是要灵活很多,虽然这里用数组也是可以的。然后需要注意的一点是,程序中需要用到INT_MAX来代替伪代码中的无穷大(INFTY),INT_MAX在头文件climits。
#include<iostream>
#include<vector>
#include<climits>
void merge(std::vector<int>& A, int p, int q, int r) {
int n1 = q - p + 1;
int n2 = r - q;
std::vector<int> L, R;
for (int i = 1; i <= n1; ++i) {
L.push_back(A[p + i - 1]);
}
for (int j = 1; j <= n2; ++j) {
R.push_back(A[q + j]);
}
L.push_back(INT_MAX);
R.push_back(INT_MAX);
int i = 0, j = 0;
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(std::vector<int>& A, int p, int r) {
if (p < r) {
int q = (p + r) / 2;
merge_sort(A, p, q);
merge_sort(A, q + 1, r);
merge(A, p, q, r);
}
}
int main() {
int a[]= { 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2 };
std::vector<int> aa= { 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2 };
//sort(aa);
merge_sort(aa, 0, aa.size() - 1);
for (const auto c : aa) {
std::cout << c << std::endl;
}
}
java版本
public class algo_sort_1 {
public void merge(int[] a,int p,int q,int r){
int n1=q-p+1;
int n2=r-q;
int[] L=new int[n1+1],R=new int[n2+1];
for(int i=0;i<n1;++i){
L[i]=a[p+i];
}
for(int j=0;j<n2;++j){
R[j]=a[q+j+1];
}
L[n1]=Integer.MAX_VALUE;
R[n2]=Integer.MAX_VALUE;
int i=0,j=0;
for(int k=p;k<=r;++k){
if(L[i]<=R[j]){
a[k]=L[i];
++i;
}else{
a[k]=R[j];
++j;
}
}
}
public void merge_sort(int[] a,int p,int r){
if(p<r){
int q=(p+r)/2;
new algo_sort_1().merge_sort(a,p,q);
new algo_sort_1().merge_sort(a,q+1,r);
new algo_sort_1().merge(a,p,q,r);
}
}
public static void main(String[] args) {
int []a={ 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2 };
//new algo_sort_1().sort(a);
new algo_sort_1().merge_sort(a,0,a.length-1);
for(int c:a){
System.out.println(c);
}
}
}
python版本,由于列表推导式的存在使程序简洁了不少
import sys
def merge(a,p,q,r):
n1=q-p+1
n2=r-q
L=[x for x in a[p:q+1]]
R=[x for x in a[q+1:r+1]]
L.append(sys.maxsize)
R.append(sys.maxsize)
i,j=0,0
for k in range(p,r+1):
if L[i]<R[j]:
a[k]=L[j]
i+=1
else:
a[k]=R[j]
j+=1
def merge_sort(a,p,r):
if p<r:
q=(p+r)//2
merge_sort(a,p,q)
merge_sort(a,q+1,r)
merge(a,p,q,r)
aa=[ 1,2,3,42,3,4 ,1,2,4,22,44,22,123,2]
#sort(aa)
merge_sort(aa, 0, len(aa)-1)
print(aa)
分析算法
这个算法还是很有意思的,《算法导论》书上有对合并排序的运行时间的详细分析。结论是,最坏情况下运行时间为Θ(n lg n)。显然它在足够大输入时是优于插入排序的。
后面还有堆排序、快速排序、线性时间排序,内容非常多,所以还是分几次来学习。