第2章 递归与分治策略
2.1.递归的概念
递归算法:直接或间接地调用自身的算法
递归函数:用函数自身给出定义的函数
!!!!递归函数的第一句一定是if语句作为边界条件,然后就是递归方程
如:阶乘函数的第一句就是if条件语句
1 int factorial(int n){ 2 if( n ==0) 3 return 1; 4 return n*factorial(n-1); 5 }
※※※递归函数中比较著名的是Hanoi塔问题
Hanoi塔问题。 设a,b,c是3个塔座。开始时,在塔座a上有一叠共n个圆盘,这些圆盘自下而上,由大到小地叠在一起。各圆盘从小到大编号为1,2,…,n,现要求将塔座a上的这一叠圆盘移到塔座c上,并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则: 规则1:每次只能移动1个圆盘; 规则2:任何时刻都不允许将较大的圆盘压在较小的圆盘之上; 规则3:在满足规则1和2的前提下,可将圆盘移至a,b,c中任一塔座上。
1 #include<iostream> 2 using namespace std; 3 void move(char p1,char p2){ 4 cout<<p1<<"->"<<p2<<endl; 5 } 6 7 //将a上的n个盘子经b移动到c上 8 void hanoi(int n,char a,char b,char c){ 9 if(n == 0) return;//当a上没有盘子的时候,直接返回不需要移动 10 if(n == 1) move(a,c);//当a上只有一个盘子的时候,直接将盘子从a上移动到c上 11 if(n>1){ 12 hanoi(n-1,a,c,b); 13 move(a,b); 14 hanoi(n-1,c,b,a); 15 } 16 } 17 18 int main(){ 19 char x,y,z; 20 x = 'a'; 21 y = 'b'; 22 z = 'c'; 23 hanoi(4,x,y,z); 24 return 0; 25 }
2.2分治法的基本思想
分治法的基本思想:将一个规模为n的问题分解为k个规模较小的子问题,这些子问题相互独立且与原问题相同
!!!在使用分治法设计算法的时候,最好使子问题的规模大致相同,即将一个问题分为大小相等的k个子问题,一般情况k取2.
※※※分治法中比较著名的是划分整数问题
1、整数划分问题 将一个正整数n表示为一系列正整数之和, n = n1 + n2 +…+nk 其中n1≥n2≥…≥nk≥1, k≥1。 例如 p(6) = 11 ,即整数6的划分数为11种: 6, 5+1, 4+2, 4+1+1, 3+3, 3+2+1, 3+1+1+1, 2+2+2, 2+2+1+1, 2+1+1+1+1, 1+1+1+1+1+1 2、有时候,问题本身具有比较明显的递归关系,因而容易用递归函数直接求解。 在本例中,如果设p(n)为正整数n的划分数,则难以直接找到递归关系。 因此考虑增加一个自变量:在正整数的所有不同划分中,将最大加数n不大于m的划分个数记为q(n, m)
递归关系:
1 #include<iostream> 2 using namespace std; 3 4 int q(int n,int m){ 5 if((n<1)||(m<1)) return 0; 6 if((n==1)||(m==1)) return 1; 7 if(n<m) return q(n,n); 8 if(n == m) return 1+q(n,n-1); 9 if(n>m) return q(n,m-1)+q(n-m,m); 10 } 11 12 int main(){ 13 int n,m; 14 cin >> n >> m; 15 cout << q(n,m); 16 return 0; 17 }
2.3.二分搜索技术
1 #include<iostream> 2 using namespace std; 3 4 int bsearch(int x,int a[],int left,int right){ 5 if(left>right) return -1; 6 int middle = (left+right)/2; 7 if(x == a[middle]) 8 return middle; 9 if(x < a[middle]) 10 return bsearch(x,a,left,middle-1); 11 else 12 return bsearch(x,a,middle+1,right); 13 } 14 15 int main(){ 16 int x; 17 cin >> x;//要找的特定元素 18 int n; 19 cin >> n; 20 int a[n]; 21 for(int i=0;i<n;i++){ 22 cin >> a[i]; 23 } 24 cout << bsearch(x,a,a[0],a[x-1]); 25 return 0; 26 }
时间复杂度分析:
1.前面两个if+int 一共三个语句 时间复杂度为3
2.后面两个if 时间复杂度是T(n/2)如果继续划分下去将会是T(n/4),T(n/8).....
T(n)= 3+ T(n/2) = 3+3+T(n/4) =........= 4log n + 4 =O(log n)
1 #include<iostream> 2 using namespace std; 3 4 int bsearch(int x,int a[],int left,int right){ 5 while(left <= right){ 6 int middle = (left+right)/2; 7 if(x == a[middle]) 8 return middle; 9 if(x < a[middle]) 10 right = middle -1; 11 else 12 left = middle +1; 13 } 14 return -1; 15 } 16 17 int main(){ 18 int a[10] = {1,2,3,4,5,6,7,8,9,10}; 19 cout << bsearch(3,a,1,10); 20 return 0; 21 }
时间复杂度分析:
每执行一次算法的while循环,待搜索数组的大小减小一半。因此,在最坏情况下,while循环执行了O(log n)次。循环体内运算需要O(1)时间,因此整个算法在最坏情况下时间复杂度为O(log n)
※※※※分治法的时间复杂度
2.4大整数的乘法
题目:设X和Y都是n位的十进制整数。如果用常规的乘法计算乘积XY,其时间复杂性为O(n2)。
分治法的做法:
将X和Y都分成2段,即 X = A10n/2 + B, Y = C10n/2 + D
于是: XY = (A10n/2 + B)(C10n/2 + D) = AC10n +(AD+BC)10n/2 + BD
分析:
最终的时间复杂度跟按常规方法是一样的,因此这种方法不可行
※※※改进:减少乘法的运算次数
大整数的乘法的方法:
将XY改写成: XY = AC10n +((A–B)(D–C)+AC+BD)10n/2 + BD
仅需做3次n/2位整数的乘法(AC,BD,((A-B)(D-C))
T(n) = 3T(n/2) + O(n) = O(nlog23) = O(n1.59)
2.5.Strassen矩阵乘法
常规方法:
两个n×n矩阵乘法的时间复杂性为 O(n3)
分治法:
※※※ 改进:
※※※※※※
分治法和大整数乘法,Strassen矩阵乘法的区别
分治法是将问题分为两个子问题通过递归来降低时间复杂度,而解决大整数乘法和Strassen矩阵乘法如果也用分治的思想的话就时间复杂度依旧很大,因此我们需要继续降低时间复杂度,方法是减少乘法的次数,因此我们把问题分为3,4..个子问题来减少乘法的次数,从而降低时间复杂度
2.6.棋盘覆盖(不是重点)
2.7.合并排序
基本思想:用分治策略实现对n个元素进行排序的算法,将待排序元素分成大小大致相同的两个子集合,分别对两个子集合进行排序,最终将排好序的子集合合并成要求的排好序的集合
参考:(1条消息)归并排序算法 C++ - summerlq的博客 - CSDN博客 https://blog.csdn.net/summerlq/article/details/81284928对合并排序进行逻辑分析
1 #include<iostream> 2 using namespace std; 3 4 void merge(int a[],int l,int mid,int r) { 5 int b[1000]; 6 int i = l; 7 int j = mid+1; 8 int k = 0; 9 /*两段数组分别进行排序将排好序的数组放入数组b中*/ 10 while(i <= mid && j<=r){ 11 if(a[i]<a[j]){ 12 b[k] = a[i]; 13 i++; 14 } 15 else{ 16 b[k] = a[j]; 17 j++; 18 } 19 k++; 20 } 21 if(i<=mid){ 22 //如果i<=m则证明第一段数组没有全部放入数组b中,即第二段数组已全部放入数组b中,而第一段数组已经排好序只需逐一放入数组b即可 ,else情况同理 23 for(int p =i;p<=mid;p++){ 24 b[k++] = a[p]; 25 } 26 } else{ 27 for(int p =j;p<=r;p++){ 28 b[k++] = a[p]; 29 } 30 } 31 for(int p =l;p<=r;p++){ 32 a[p] = b[p-l];//将排好序的b数组复制到a数组中 33 } 34 } 35 36 /*将数组a分成两个部分,对每个部分递归调用mergesort进行排序,然后将两段排好序的数组进行合并到另一个数组b中*/ 37 void mergesort(int a[],int l,int r){ 38 if(r<=l) return;//如果数组中少于两个元素,则直接返回 39 int mid = (l+r)/2; 40 mergesort(a,l,mid); 41 mergesort(a,mid+1,r); 42 merge(a,l,mid,r); 43 } 44 45 46 int main(){ 47 int a[10] = {10,4,9,33,88,34,78,66,56,43}; 48 cout<<"原数组序列是:"; 49 for(int i=0;i<10;i++){ 50 cout << a[i]<<" "; 51 } 52 cout<<endl; 53 cout<<"排序后数组序列是:" ; 54 mergesort(a,0,9); 55 for(int i=0;i<10;i++){ 56 cout << a[i]<<" "; 57 } 58 return 0; 59 }
时间复杂度分析:
2.8快速排序
2.9线性时间选择
2.10最接近点问题
2.11循环赛日程表
(以上的8,9,10,11节都非本章重点)
---恢复内容结束---
第2章 递归与分治策略
目录
2.1.递归的概念
递归算法:直接或间接地调用自身的算法
递归函数:用函数自身给出定义的函数
!!!!递归函数的第一句一定是if语句作为边界条件,然后就是递归方程
如:阶乘函数的第一句就是if条件语句
1 int factorial(int n){ 2 if( n ==0) 3 return 1; 4 return n*factorial(n-1); 5 }
※※※递归函数中比较著名的是Hanoi塔问题
Hanoi塔问题。 设a,b,c是3个塔座。开始时,在塔座a上有一叠共n个圆盘,这些圆盘自下而上,由大到小地叠在一起。各圆盘从小到大编号为1,2,…,n,现要求将塔座a上的这一叠圆盘移到塔座c上,并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则: 规则1:每次只能移动1个圆盘; 规则2:任何时刻都不允许将较大的圆盘压在较小的圆盘之上; 规则3:在满足规则1和2的前提下,可将圆盘移至a,b,c中任一塔座上。
1 #include<iostream> 2 using namespace std; 3 void move(char p1,char p2){ 4 cout<<p1<<"->"<<p2<<endl; 5 } 6 7 //将a上的n个盘子经b移动到c上 8 void hanoi(int n,char a,char b,char c){ 9 if(n == 0) return;//当a上没有盘子的时候,直接返回不需要移动 10 if(n == 1) move(a,c);//当a上只有一个盘子的时候,直接将盘子从a上移动到c上 11 if(n>1){ 12 hanoi(n-1,a,c,b); 13 move(a,b); 14 hanoi(n-1,c,b,a); 15 } 16 } 17 18 int main(){ 19 char x,y,z; 20 x = 'a'; 21 y = 'b'; 22 z = 'c'; 23 hanoi(4,x,y,z); 24 return 0; 25 }
2.2分治法的基本思想
分治法的基本思想:将一个规模为n的问题分解为k个规模较小的子问题,这些子问题相互独立且与原问题相同
!!!在使用分治法设计算法的时候,最好使子问题的规模大致相同,即将一个问题分为大小相等的k个子问题,一般情况k取2.
※※※分治法中比较著名的是划分整数问题
1、整数划分问题 将一个正整数n表示为一系列正整数之和, n = n1 + n2 +…+nk 其中n1≥n2≥…≥nk≥1, k≥1。 例如 p(6) = 11 ,即整数6的划分数为11种: 6, 5+1, 4+2, 4+1+1, 3+3, 3+2+1, 3+1+1+1, 2+2+2, 2+2+1+1, 2+1+1+1+1, 1+1+1+1+1+1 2、有时候,问题本身具有比较明显的递归关系,因而容易用递归函数直接求解。 在本例中,如果设p(n)为正整数n的划分数,则难以直接找到递归关系。 因此考虑增加一个自变量:在正整数的所有不同划分中,将最大加数n不大于m的划分个数记为q(n, m)
递归关系:
1 #include<iostream> 2 using namespace std; 3 4 int q(int n,int m){ 5 if((n<1)||(m<1)) return 0; 6 if((n==1)||(m==1)) return 1; 7 if(n<m) return q(n,n); 8 if(n == m) return 1+q(n,n-1); 9 if(n>m) return q(n,m-1)+q(n-m,m); 10 } 11 12 int main(){ 13 int n,m; 14 cin >> n >> m; 15 cout << q(n,m); 16 return 0; 17 }
2.3.二分搜索技术
1 #include<iostream> 2 using namespace std; 3 4 int bsearch(int x,int a[],int left,int right){ 5 if(left>right) return -1; 6 int middle = (left+right)/2; 7 if(x == a[middle]) 8 return middle; 9 if(x < a[middle]) 10 return bsearch(x,a,left,middle-1); 11 else 12 return bsearch(x,a,middle+1,right); 13 } 14 15 int main(){ 16 int x; 17 cin >> x;//要找的特定元素 18 int n; 19 cin >> n; 20 int a[n]; 21 for(int i=0;i<n;i++){ 22 cin >> a[i]; 23 } 24 cout << bsearch(x,a,a[0],a[x-1]); 25 return 0; 26 }
时间复杂度分析:
1.前面两个if+int 一共三个语句 时间复杂度为3
2.后面两个if 时间复杂度是T(n/2)如果继续划分下去将会是T(n/4),T(n/8).....
T(n)= 3+ T(n/2) = 3+3+T(n/4) =........= 4log n + 4 =O(log n)
1 #include<iostream> 2 using namespace std; 3 4 int bsearch(int x,int a[],int left,int right){ 5 while(left <= right){ 6 int middle = (left+right)/2; 7 if(x == a[middle]) 8 return middle; 9 if(x < a[middle]) 10 right = middle -1; 11 else 12 left = middle +1; 13 } 14 return -1; 15 } 16 17 int main(){ 18 int a[10] = {1,2,3,4,5,6,7,8,9,10}; 19 cout << bsearch(3,a,1,10); 20 return 0; 21 }
时间复杂度分析:
每执行一次算法的while循环,待搜索数组的大小减小一半。因此,在最坏情况下,while循环执行了O(log n)次。循环体内运算需要O(1)时间,因此整个算法在最坏情况下时间复杂度为O(log n)
※※※※分治法的时间复杂度
2.4大整数的乘法
题目:设X和Y都是n位的十进制整数。如果用常规的乘法计算乘积XY,其时间复杂性为O(n2)。
分治法的做法:
将X和Y都分成2段,即 X = A10n/2 + B, Y = C10n/2 + D
于是: XY = (A10n/2 + B)(C10n/2 + D) = AC10n +(AD+BC)10n/2 + BD
分析:
最终的时间复杂度跟按常规方法是一样的,因此这种方法不可行
※※※改进:减少乘法的运算次数
大整数的乘法的方法:
将XY改写成: XY = AC10n +((A–B)(D–C)+AC+BD)10n/2 + BD
仅需做3次n/2位整数的乘法(AC,BD,((A-B)(D-C))
T(n) = 3T(n/2) + O(n) = O(nlog23) = O(n1.59)
2.5.Strassen矩阵乘法
常规方法:
两个n×n矩阵乘法的时间复杂性为 O(n3)
分治法:
※※※ 改进:
※※※※※※
分治法和大整数乘法,Strassen矩阵乘法的区别
分治法是将问题分为两个子问题通过递归来降低时间复杂度,而解决大整数乘法和Strassen矩阵乘法如果也用分治的思想的话就时间复杂度依旧很大,因此我们需要继续降低时间复杂度,方法是减少乘法的次数,因此我们把问题分为3,4..个子问题来减少乘法的次数,从而降低时间复杂度
2.6.棋盘覆盖(不是重点)
2.7.合并排序
基本思想:用分治策略实现对n个元素进行排序的算法,将待排序元素分成大小大致相同的两个子集合,分别对两个子集合进行排序,最终将排好序的子集合合并成要求的排好序的集合
参考:(1条消息)归并排序算法 C++ - summerlq的博客 - CSDN博客 https://blog.csdn.net/summerlq/article/details/81284928对合并排序进行逻辑分析
1 #include<iostream> 2 using namespace std; 3 4 void merge(int a[],int l,int mid,int r) { 5 int b[1000]; 6 int i = l; 7 int j = mid+1; 8 int k = 0; 9 /*两段数组分别进行排序将排好序的数组放入数组b中*/ 10 while(i <= mid && j<=r){ 11 if(a[i]<a[j]){ 12 b[k] = a[i]; 13 i++; 14 } 15 else{ 16 b[k] = a[j]; 17 j++; 18 } 19 k++; 20 } 21 if(i<=mid){ 22 //如果i<=m则证明第一段数组没有全部放入数组b中,即第二段数组已全部放入数组b中,而第一段数组已经排好序只需逐一放入数组b即可 ,else情况同理 23 for(int p =i;p<=mid;p++){ 24 b[k++] = a[p]; 25 } 26 } else{ 27 for(int p =j;p<=r;p++){ 28 b[k++] = a[p]; 29 } 30 } 31 for(int p =l;p<=r;p++){ 32 a[p] = b[p-l];//将排好序的b数组复制到a数组中 33 } 34 } 35 36 /*将数组a分成两个部分,对每个部分递归调用mergesort进行排序,然后将两段排好序的数组进行合并到另一个数组b中*/ 37 void mergesort(int a[],int l,int r){ 38 if(r<=l) return;//如果数组中少于两个元素,则直接返回 39 int mid = (l+r)/2; 40 mergesort(a,l,mid); 41 mergesort(a,mid+1,r); 42 merge(a,l,mid,r); 43 } 44 45 46 int main(){ 47 int a[10] = {10,4,9,33,88,34,78,66,56,43}; 48 cout<<"原数组序列是:"; 49 for(int i=0;i<10;i++){ 50 cout << a[i]<<" "; 51 } 52 cout<<endl; 53 cout<<"排序后数组序列是:" ; 54 mergesort(a,0,9); 55 for(int i=0;i<10;i++){ 56 cout << a[i]<<" "; 57 } 58 return 0; 59 }
时间复杂度分析:
2.8快速排序
参考文献:(1条消息)快速排序算法的C++实现 - xuezhu1的博客 - CSDN博客 https://blog.csdn.net/xuezhu1/article/details/81944875
(1条消息)算法之快速排序(C++实现) - lyl771857509的博客 - CSDN博客 https://blog.csdn.net/lyl771857509/article/details/78845221
思想:是基于分治策略的另一种排序算法。其基本思想是,对于输入的子数组a[p:r],按一下三个步骤进行排序:
①分解:以a[p]为基准元素将a[p:r]划分为3段a[p:q-1],a[q]和a[q+1:r],使a[p:q-1]中任何一个元素小于等于a[q],而a[q+1:r]中任何一个元素大于等于a[q]。下标q在划分过程中确定
②递归分解:通过递归调用快速排序算法,分别对a[p:q-1],a[q+1:r]进行排序
③合并:由于对a[p:q-1]和a[q+1:r]的排序是就地进行的,因此在a[p:q-1]和a[q+1:r]都排好序后,不需要执行任何算法,a[p:r]已经排好序
1 #include<iostream> 2 using namespace std; 3 4 void Swap(int x,int y){ 5 int t; 6 t = x; 7 x = y; 8 y = t; 9 } 10 11 int Partition(int a[],int p,int r){ 12 int i = p,j = r; 13 int x = a[p];// x 为基准数 14 //将小于x的元素交换到左边区域,将大于x的元素交换到右边区域 15 while(true){ 16 /*左边的数小于基准数,右边的数大于基准数的时候 不作处理*/ 17 while(a[++i] < x && i<r); 18 while(a[--j] > x && i<r); 19 if(i >= j) 20 break; 21 Swap(a[i],a[j]); 22 } 23 a[p] = a[j]; 24 a[j] = x; 25 return j; 26 } 27 28 void QuickSort(int a[],int p,int r){ 29 if(p<r){ 30 int q =Partition(a,p,r); 31 QuickSort(a,p,q-1);//对左半段进行排序 32 QuickSort(a,q+1,r);//对右半段进行排序 33 } 34 } 35 36 int main(){ 37 int a[5] = {1,2,9,7,0}; 38 cout << "原序列是:"; 39 for(int i=0;i<5;i++){ 40 cout <<a[i]<<" "; 41 } 42 QuickSort(a,0,4); 43 cout << endl << "快排后的序列是:"; 44 for(int i=0;i<5;i++){ 45 cout << a[i]<<" "; 46 } 47 return 0; 48 }
2.9线性时间选择
2.10最接近点问题
2.11循环赛日程表
(以上的9,10,11节都非本章重点)