欢迎大家挑错
第一部分、算法基础
这一部分主要说了分治法和算法的时间分析。
一开始先介绍了插入法,这个方法就像我们玩扑克牌的时候,选一张排,插入前面已经排好的序列里面。为了实现这个算法,我们就要有个抽象的思维,假设前面的i个数字已经排好,那么我们就要把i+1位置上的数字和前i的逐个比较,找到i+1应该插入的位置。如果是用数组的话,那就意味着还要移位。算法时间复杂度是O(n^2),c代码如下:
for(i=1;i<n;++i){
int key = a[i];
for(j=i-1;j>=0 && key>a[j]; --j)
a[j+1]= a[j];
a[j+1]= key;
}
接着引入合并排序(mergesort),这个算法用的就是分治的思想,分而治之。把一个问题,分解成几个个子问题,对子问题求解完之后,再对它们的结果合并,如此一来,就解决了问题。对于排序这个例子,我们想象一下如果要分治的话要怎么分呢?既然是要分成子问题,把子问题解决掉之后合并解决大问题,那就是说我分完子问题之后,分开的地方都排好序了,然后把他们合并。怎么合并呢?既然子问题排好了,那么我们就可以每次比较子问题,选出它们最小的那个。在这里我们假设分成两个,那么可以想象成我有两堆牌,他们排好序了,然后每次比较牌面的两张,谁小就放进新的一堆那里。C代码如下:
void merge_sort(int*a,int left,int right)
{
if (left<right) {
int mid = (left+right)>>1;
merge_sort(a,left,mid);
merge_sort(a,mid+1,right);
merge(a,left,right);
}
}
void merge(int *a,int left,int right)
{//用b这个临时数组,把[left,mid]到[mid+1,right]这两部分合并到b那里,然 //后再把b覆盖到a的left到right这一段
int i=left,j=mid+1,k=0;
int *b = (int*)malloc(sizeof(int)*(right-left+1));
while(i<=mid&& j<=right)
if(a[i]>a[j])
b[k++]= a[i++];
else
b[k++]= a[j++];
//其中一堆还有剩余的话,就直接放上去。
while(i<=mid)
b[k++]= a[i++];
while(j<=right)
b[k++]= a[j++];
for(i=left,j=0;i<=right; ++i,++j)
a[i]= b[j];
free(b);
}
下面再展示另一个merge的算法,这个方法用了两个哨兵在临时数组末尾:
void merge(int*a,int left,int right)
{
int mid = (left+right)>>1;
//因为要放多一个哨兵,所以长度长了一个。左边是[left,mid]所以是mid-left+1
//的长度,加1就是mid-left+2。右边是rigt-(mid+1)+1,再加1就是
//right-mid+1了。
int b1[mid-left+2],b2[right-mid+1];
b1[mid-left+1]= b2[right-mid] = MAX;
int i,j,k;
//下面的第一个for和if是把复制两部分到临时数组那里去,因为右边部分的长度不会比 //左边的长,所以用右边的边界条件,然后再判断左边是不是还有没复制进去的。
for(j=0,i=mid+1;i<=hight; ++i,++j) {
b1[j]= a[low+j];
b2[j]= a[i];
}
if (low+j == mid)
b1[j]= a[mid];
for(i=j=0,k=low;k<=hight; )
a[k++]= b1[i] < b2[j] ? b1[i++] : b2[j++];
}
那么这个分支算法的时间复杂度是多少呢?如果设它是T(n),那么就是T(n)= 2T(n/2)+O(n)。所以就引出一个问题,怎么去解这递归式子。
用主方法T(n)= aT(n/b)+f(n),那么有:
所以我们可以算出合并排序的时间复杂度是nlgn。
另外还有两个方法可以分析时间复杂度,一个是万能的代换法,另一个是很直观的递归树法。
接下来,在介绍两个应用分治思想的算法,一个是二分查找,一个是求fibonacci。例如一组数,查找的时候,我们从前到后一个个的比较,找出那个数。如果数组的有序的呢?还这样顺序的找岂不是浪费了已知信息。既然是排好了,那么我们就可以跟这组数中间那个数比较,如果大了,就说明在右边,小了,就说明在左边,这样每次找都可以排除一半的选择。算法的c代码如下:
int binary_search(int x,int *a,int left,int right)
{
int mid;
while (left<=right){
mid= (left+right)>>1;
if(x<a[mid])
right= mid-1;
else if(x>a[mid])
left= mid+1;
else
return mid;
}
return FAIL;
}
通常我们在查找失败的时候,还可以返回一个数字i,使a[i-1]<x<a[i],代表如果x要插入的话,插在位置i上面,看看边界条件,在查找失败的时候,left>right,那么大多少了,因为mid是left和right之间的数,而且每次都只是+1、-1的,所以left最多只会比right大1,之前那次迭代必然有left=mid=right,如果是+1导致的,就是说x>a[mid],那么就应该返回mid+1,就是说返回left,同样如果是-1导致的,就是说x<a[mid],那么就返回mid就可以了,而这个时候mid=left,所以我们发现在查找失败的时候,只要返回left就可以了,所以把returnFAIL改成returnleft;就可以返回应该插入的位置了。
接下来,就说fibonacci数的求法,用分治的方法的话,我们就要用到快速幂乘和矩阵。先说说快速幂乘积。快速幂乘积就是分治的思想,假设a^n,那么就有a^n=a^(n/2)*a^(n/2),因为这这里是向下取整的,当n是偶数的时候,成立,当n是基数的时候,我们就少成了一个a,所以这个算法要分奇偶两种情况,c代码如下:
int power1(int x,int n)
{
if(n==1) return x;
if(n&0x1)//判断奇偶性
return power1(x,n>>1)*power1(x,n>>1)*x;
else
return power1(x,n>>1)*power1(x,n>>1);
}
对于上面的方法,其实我们还可以用二进制来优化的,因为a^(n1+n2)=a^n1*a^n2,那么我们把n分解成二进制,然后用位运算来把相应位置上的a的x次幂成上去就可以啦。C代码如下:
int power2(int a,int n)
{
int s=1;
while (n) {
if(n&0x1)
s*= x;
a*=a;
n>>=1;
}
return s;
}
上面这个算法还可以用来当n很大也不溢出的快速求a^nmod m,由数论的知识知道:
a^n/2 和 (a^n/2mod m) 同余m
那么按照同余的运算法则,那么就有:
a^n和 (a^n/2mod m)(a^n/2 mod m) 同余m
所以我们就可以用快速求幂的算法来求同余幂,c代码如下:
int mod(int a,int n,int m)
{
int s = 1;
while(n){
if(n&0x1)
s=s*a%m;
a=(a*a)%m;
n>>=1;
}
return s;
}
现在的话回到主题,怎么用分治法来求fibonacci数,我们知道fib(n)=fib(n-1)+fib(n-2)
那么如果我们用迭代的方法,时间复杂度是O(n),是这样写的:
int fib(int n)
{
if(!n) return 0;
if(n<3) return 1;
int f1=1,f2=1,f3=1;
for(inti=3; i<=n; ++i) {
f1= f2;
f2= f3;
f3= f1+f2;
}
return f3;
}
用分治法的话,我们可以把把时间复杂度降为O(lgn),用的方法是矩阵,构造一个小的矩阵,使用每次相乘的时候,都会有这个式子:fib(n)=fib(n-1)+fib(n-2),那么我们就可以用快速幂乘积的方法来把矩阵相乘了。先假定一下这样的矩阵
fib(n) fib(n-1)
fib(n-1) fib(n-2)
如果要它乘上另一个矩阵有那条式子的话,很容易就想到矩阵
1 1
1 0
用这个矩阵自己幂乘后,发现每次相乘之后,其实就等于上面那个矩阵,用归纳法可以严格证明。就是说如果要求f(n),那么就等于求这个矩阵的n次方,然后放回位置(0,0)上的值。有了这个发现,我们就可以写代码了。对于矩阵相乘,每次都是:
M1[0][0]*M2[0][0]+M1[0][1]*M2[1][0]
M1[0][0]*M2[0][1]+M1[0][1]*M2[1][1]
M1[1][0]*M2[0][0]+M1[1][1]*M2[1][0]
M1[1][0]*M2[0][1]+M1[1][1]*M2[1][1]
算法的c语言代码如下:
int fib(int n)
{
if(!n) return 0;
if(n<3) return 1;
n-=2;
int a[2][2]={{1,1},{1,0}};
int b[2][2]={{1,1},{1,0}};
while (n) {
if(n&0x1)
matrixMutiply(b,a);
matrixMutiply(a,a);
n>>=1;
}
return b[0][0];
}
void matrixMutiply(int m1[][2],int m2[][2])
{
int m3[2][2];
m3[0][0]= m1[0][0]*m2[0][0]+m1[0][1]*m2[1][0];
m3[0][1]= m1[0][0]*m2[0][1]+m1[0][1]*m2[1][1];
m3[1][0]= m1[1][0]*m2[0][0]+m1[1][1]*m2[1][0];
m3[1][1]= m1[1][0]*m2[0][1]+m1[1][1]*m2[1][1];
m1[0][0]= m3[0][0];
m1[0][1]= m3[0][1];
m1[1][0]= m3[1][0];
m1[1][1]= m3[1][1];
}
这一部分主要说的内容就是这样子的了。下面再把一些题目的解答写一下
2.3-7:
这题,就先用O(nlgn)的方法对数组进行排序,然后用二分查找,看看每个数有没有相对应的数,相加之后等于x,当然这里可以常数因子的优化,就是把x/2加入做哨兵,然后把数组分成左右两部分,左边<=x/2,右边>=x/2。具体要怎么做呢?就是把数组排序完之后,用插入排序的方法,把x/2插入里面,设它插在位置j,那么就对于[0,j-1)的每一个数,二分查找[j+1,n)有没有相加之后等于x的。
2.4:
提示我们修改一下合并排序的算法。求逆序对的数量,就是说求每个数后面比它小的数字有多少个。用合并排序的话,我们知道左边的那一堆的是在右边那一堆的前面,那么就是说在合并的时候,对于每放上一张左边的数,就看看它压住了多少张右边的数。那么总的一个流程,就应该是我先求出两个数都在左边那一堆的逆序对数,再求出两个数都在右边那堆的逆序对数,然后再加上一个数在左边,另一个数在右边的逆序对数,把它们都加起来,就是整个集合的逆序对数了。C代码如下:
int merge_sort_inversion(int *a,int low,int hight)
{
if(low<hight){
int mid = (low+hight)>>1;
int sum = merge_sort_inversion(a,low,mid);
sum+= merge_sort_inversion(a,mid+1,hight);
int b1[mid-low+2],b2[hight-mid+1];
b1[mid-low+1]= b2[hight-mid] = MAX;
int i,j,k;
for(j=0,i=mid+1;i<=hight; ++i,++j) {
b1[j]= a[low+j];
b2[j]= a[i];
}
if (low+j==mid)
b1[j]= a[mid];
for(i=j=0,k=low;k<=hight;) {
if(b1[i]<b2[j]){
a[k++]= b1[i++];
sum+=j;//j就等于右边有多少个数字放去了
}else
a[k++]= b2[j++];
}
return sum;
}else
return0;
}
int key = a[i];for(j=i-1;j>=0 && key>a[j]; --j)
a[j+1]= a[j];
a[j+1]= key;
}
接着引入合并排序(mergesort),这个算法用的就是分治的思想,分而治之。把一个问题,分解成几个个子问题,对子问题求解完之后,再对它们的结果合并,如此一来,就解决了问题。对于排序这个例子,我们想象一下如果要分治的话要怎么分呢?既然是要分成子问题,把子问题解决掉之后合并解决大问题,那就是说我分完子问题之后,分开的地方都排好序了,然后把他们合并。怎么合并呢?既然子问题排好了,那么我们就可以每次比较子问题,选出它们最小的那个。在这里我们假设分成两个,那么可以想象成我有两堆牌,他们排好序了,然后每次比较牌面的两张,谁小就放进新的一堆那里。C代码如下:
mergesort(int*a,int left,int right)
{
if(left<right){
intmid = (left+right)>>1;
mergesort(a,left,mid);
mergesort(a,mid+1,right);
merge(a,left,right);
}
}
merge(int*a,int left,int right)
{//用b这个临时数组,把[left,mid]到[mid+1,right]这两部分合并到b那里,然 //后再把b覆盖到a的left到right这一段
inti=left,j=mid+1,k=0;
int*b = (int*)malloc(sizeof(int)*(right-left+1));
while(i<=mid&& j<=right)
if(a[i]>a[j])
b[k++]= a[i++];
else
b[k++]= a[j++];
//其中一堆还有剩余的话,就直接放上去。
while(i<=mid)
b[k++]= a[i++];
while(j<=right)
b[k++]= a[j++];
for(i=left,j=0;i<=right; ++i,++j)
a[i]= b[j];
free(b);
}
下面再展示另一个merge的算法,这个方法用了两个哨兵在临时数组末尾:
merge(int*a,int left,int right)
{
intmid = (left+right)>>1;
//因为要放多一个哨兵,所以长度长了一个。左边是[left,mid]所以是mid-left+1
//的长度,加1就是mid-left+2。右边是rigt-(mid+1)+1,再加1就是
//right-mid+1了。
intb1[mid-left+2],b2[right-mid+1];
b1[mid-left+1]= b2[right-mid] = MAX;
inti,j,k;
//下面的第一个for和if是把复制两部分到临时数组那里去,因为右边部分的长度不会比 //左边的长,所以用右边的边界条件,然后再判断左边是不是还有没复制进去的。
for(j=0,i=mid+1;i<=hight; ++i,++j) {
b1[j]= a[low+j];
b2[j]= a[i];
}
if(low+j==mid)
b1[j]= a[mid];
for(i=j=0,k=low;k<=hight; )
a[k++]= b1[i] < b2[j] ? b1[i++] : b2[j++];
}
那么这个分支算法的时间复杂度是多少呢?如果设它是T(n),那么就是T(n)= 2T(n/2)+O(n)。所以就引出一个问题,怎么去解这递归式子。
用主方法T(n)= aT(n/b)+f(n),那么有:
所以我们可以算出合并排序的时间复杂度是nlgn。
另外还有两个方法可以分析时间复杂度,一个是万能的代换法,另一个是很直观的递归树法。
接下来,在介绍两个应用分治思想的算法,一个是二分查找,一个是求fibonacci。例如一组数,查找的时候,我们从前到后一个个的比较,找出那个数。如果数组的有序的呢?还这样顺序的找岂不是浪费了已知信息。既然是排好了,那么我们就可以跟这组数中间那个数比较,如果大了,就说明在右边,小了,就说明在左边,这样每次找都可以排除一半的选择。算法的c代码如下:
int binary_search(int x,int *a,int left,int right)
{
int mid;
while (left<=right){
mid= (left+right)>>1;
if(x<a[mid])
right= mid-1;
else if(x>a[mid])
left= mid+1;
else
return mid;
}
return FAIL;
}
通常我们在查找失败的时候,还可以返回一个数字i,使a[i-1]<x<a[i],代表如果x要插入的话,插在位置i上面,看看边界条件,在查找失败的时候,left>right,那么大多少了,因为mid是left和right之间的数,而且每次都只是+1、-1的,所以left最多只会比right大1,之前那次迭代必然有left=mid=right,如果是+1导致的,就是说x>a[mid],那么就应该返回mid+1,就是说返回left,同样如果是-1导致的,就是说x<a[mid],那么就返回mid就可以了,而这个时候mid=left,所以我们发现在查找失败的时候,只要返回left就可以了,所以把returnFAIL改成returnleft;就可以返回应该插入的位置了。
接下来,就说fibonacci数的求法,用分治的方法的话,我们就要用到快速幂乘和矩阵。先说说快速幂乘积。快速幂乘积就是分治的思想,假设a^n,那么就有a^n=a^(n/2)*a^(n/2),因为这这里是向下取整的,当n是偶数的时候,成立,当n是基数的时候,我们就少成了一个a,所以这个算法要分奇偶两种情况,c代码如下:
int power1(int x,int n)
{
if(n==1) return x;
if(n&0x1)//判断奇偶性
return power1(x,n>>1)*power1(x,n>>1)*x;
else
return power1(x,n>>1)*power1(x,n>>1);
}
对于上面的方法,其实我们还可以用二进制来优化的,因为a^(n1+n2)=a^n1*a^n2,那么我们把n分解成二进制,然后用位运算来把相应位置上的a的x次幂成上去就可以啦。C代码如下:
int power2(int a,int n)
{
int s=1;
while (n) {
if(n&0x1)
s*= x;
a*=a;
n>>=1;
}
return s;
}
上面这个算法还可以用来当n很大也不溢出的快速求a^nmod m,由数论的知识知道:
a^n/2和 (a^n/2mod m)同余m
那么按照同余的运算法则,那么就有:
a^n和(a^n/2mod m)(a^n/2 mod m)同余m
所以我们就可以用快速求幂的算法来求同余幂,c代码如下:
int mod(int a,int n,int m)
{
int s = 1;
while(n){
if(n&0x1)
s=s*a%m;
a=(a*a)%m;
n>>=1;
}
return s;
}
现在的话回到主题,怎么用分治法来求fibonacci数,我们知道fib(n)=fib(n-1)+fib(n-2)
那么如果我们用迭代的方法,时间复杂度是O(n),是这样写的:
int fib(int n)
{
if(!n) return 0;
if(n<3) return 1;
int f1=1,f2=1,f3=1;
for(inti=3; i<=n; ++i) {
f1= f2;
f2= f3;
f3= f1+f2;
}
return f3;
}
用分治法的话,我们可以把把时间复杂度降为O(lgn),用的方法是矩阵,构造一个小的矩阵,使用每次相乘的时候,都会有这个式子:fib(n)=fib(n-1)+fib(n-2),那么我们就可以用快速幂乘积的方法来把矩阵相乘了。先假定一下这样的矩阵
fib(n) fib(n-1)
fib(n-1) fib(n-2)
如果要它乘上另一个矩阵有那条式子的话,很容易就想到矩阵
1 1
1 0
用这个矩阵自己幂乘后,发现每次相乘之后,其实就等于上面那个矩阵,用归纳法可以严格证明。就是说如果要求f(n),那么就等于求这个矩阵的n次方,然后放回位置(0,0)上的值。有了这个发现,我们就可以写代码了。对于矩阵相乘,每次都是:
M1[0][0]*M2[0][0]+M1[0][1]*M2[1][0]
M1[0][0]*M2[0][1]+M1[0][1]*M2[1][1]
M1[1][0]*M2[0][0]+M1[1][1]*M2[1][0]
M1[1][0]*M2[0][1]+M1[1][1]*M2[1][1]
算法的c语言代码如下:
int fib(int n)
{
if(!n) return 0;
if(n<3) return 1;
n-=2;
int a[2][2]={{1,1},{1,0}};
int b[2][2]={{1,1},{1,0}};
while (n) {
if(n&0x1)
matrixMutiply(b,a);
matrixMutiply(a,a);
n>>=1;
}
return b[0][0];
}
void matrixMutiply(int m1[][2],int m2[][2])
{
int m3[2][2];
m3[0][0]= m1[0][0]*m2[0][0]+m1[0][1]*m2[1][0];
m3[0][1]= m1[0][0]*m2[0][1]+m1[0][1]*m2[1][1];
m3[1][0]= m1[1][0]*m2[0][0]+m1[1][1]*m2[1][0];
m3[1][1]= m1[1][0]*m2[0][1]+m1[1][1]*m2[1][1];
m1[0][0]= m3[0][0];
m1[0][1]= m3[0][1];
m1[1][0]= m3[1][0];
m1[1][1]= m3[1][1];
}
这一部分主要说的内容就是这样子的了。下面再把一些题目的解答写一下
2.3-7:
这题,就先用O(nlgn)的方法对数组进行排序,然后用二分查找,看看每个数有没有相对应的数,相加之后等于x,当然这里可以常数因子的优化,就是把x/2加入做哨兵,然后把数组分成左右两部分,左边<=x/2,右边>=x/2。具体要怎么做呢?就是把数组排序完之后,用插入排序的方法,把x/2插入里面,设它插在位置j,那么就对于[0,j-1)的每一个数,二分查找[j+1,n)有没有相加之后等于x的。
2.4:
提示我们修改一下合并排序的算法。求逆序对的数量,就是说求每个数后面比它小的数字有多少个。用合并排序的话,我们知道左边的那一堆的是在右边那一堆的前面,那么就是说在合并的时候,对于每放上一张左边的数,就看看它压住了多少张右边的数。那么总的一个流程,就应该是我先求出两个数都在左边那一堆的逆序对数,再求出两个数都在右边那堆的逆序对数,然后再加上一个数在左边,另一个数在右边的逆序对数,把它们都加起来,就是整个集合的逆序对数了。C代码如下:
int merge_sort_inversion(int *a,int low,int hight)
{
if(low<hight){
int mid = (low+hight)>>1;
int sum = merge_sort_inversion(a,low,mid);
sum+= merge_sort_inversion(a,mid+1,hight);
int b1[mid-low+2],b2[hight-mid+1];
b1[mid-low+1]= b2[hight-mid] = MAX;
int i,j,k;
for(j=0,i=mid+1;i<=hight; ++i,++j) {
b1[j]= a[low+j];
b2[j]= a[i];
}
if (low+j==mid)
b1[j]= a[mid];
for(i=j=0,k=low;k<=hight;) {
if(b1[i]<b2[j]){
a[k++]= b1[i++];
sum+=j;//j就等于右边有多少个数字放去了
}else
a[k++]= b2[j++];
}
return sum;
}else
return0;
}