第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节都非本章重点)
