前记:
说会写递归的是神,写迭代的是人。虽然不想当神,但还是题目它不允许呀,只能乖乖学习递归。本博客的内容学习自bilibili:https://www.bilibili.com/video/av43466895?from=search&seid=13312556908231953217
递归的三个步骤:
1.递归函数的意义及参数和返回值
2.递归公式
3.递归函数的结束条件,即最小的返回条件
例题:
题目1:计算 n!
当然既可以使用迭代又可以使用递归,思路却有不同:迭代是先从1开始逐渐累乘,得到最后的结果;递归是从n开始,通过递推公式:,即n! = n * (n - 1)!,下面给出两种方案:
①迭代法:
#include <iostream>
using namespace std;
int JieCheng(int n)
{
int num = 1;
for(int i = 1;i <= n;i++)
{
num *= i;
}
return num;
}
int main()
{
int n = 5;
cout<<JieCheng(n)<<endl;
return 0;
}
②递归法:
#include <iostream>
using namespace std;
int JieCheng(int n)
{
if(n == 0)
{
return 1;
}
return n * JieCheng(n - 1);
}
int main()
{
int n = 5;
cout<<JieCheng(n)<<endl;
return 0;
}
题目二:辗转相除法
①迭代法:
#include <iostream>
using namespace std;
int MaxGongYueShu(int a,int b)
{
int c = a % b;
while(c)
{
a = b;
b = c;
c = a % b;
}
return b;
}
int main()
{
int a = 45,b = 25;
cout<<MaxGongYueShu(a,b)<<endl;
return 0;
}
②递归法:
#include <iostream>
using namespace std;
int MaxGongYueShu(int a,int b)
{
if(a % b == 0)
return b;
MaxGongYueShu(b,a%b);
}
int main()
{
int a = 45,b = 25;
cout<<MaxGongYueShu(a,b)<<endl;
return 0;
}
分治算法
分治法的设计思想:
1.分——将问题分解为规模更小的子问题;
2.治——将这些规模更小的子问题逐个击破;
3.合——将已解决的子问题合并,最终得出“母”问题的解;
·减而治之(每次让问题的规模减1)
·分而治之(每次让问题的规模减半)(归并排序的思想)
题目三:走楼梯
题目描述:
一个台阶总共有n级,如果一次可以跳1级,也可以跳2级。求总共有多少种跳法。
第一行输入T,表示有多少个测试数据。接下来T行,每行输入一个数n,表示台阶的阶数。输出时每一行对应一个输出。
样例输入:
3
5
8
10
样例输出:
8
34
89
解析:
#include <iostream>
using namespace std;
int JumpStep(int n)
{
if(n == 0)
return 0;
if(n == 1)
return 1;
return JumpStep(n - 1) + JumpStep(n - 2);
}
int main()
{
int n = 5;
cout<<JumpStep(n)<<endl;
return 0;
}
题目四:归并排序
归并排序做两件事,一件是把一个数组分成两个数组,然后把排好序的两个数组再合并成一个数组。
递归停止的条件:如果只剩一个元素,那么此时就是有序的,就可以返回了。
#include <iostream>
using namespace std;
void merge(int* base,int n)
{
int L = 0,R = n - 1,M = (L + R) / 2 + 1;
int LEFT_SIZE = M - L,RIGHT_SIZE = R - M + 1;
int left[LEFT_SIZE],right[RIGHT_SIZE];
for(int t = 0;t < LEFT_SIZE;t++)
{
left[t] = base[t];
}
for(int t = 0;t < RIGHT_SIZE;t++)
{
right[t] = base[M + t];
}
int i = 0,j = 0,k = 0;
while(i < LEFT_SIZE && j < RIGHT_SIZE)
{
if(left[i] < right[j])
{
base[k] = left[i];
k++;
i++;
}
else
{
base[k] = right[j];
k++;
j++;
}
}
while(i < LEFT_SIZE)
{
base[k] = left[i];
k++;
i++;
}
while(j < RIGHT_SIZE)
{
base[k] = right[j];
k++;
j++;
}
}
void display(int* base,int n)
{
for(int t = 0;t < n;t++)
{
cout<<base[t]<<'\t';
}
cout<<endl;
}
void mergeSort(int* base,int n)
{
if(n == 1)
return ;
mergeSort(base,n / 2);
mergeSort(base + n / 2,n / 2);
merge(base,n);
}
int main()
{
int base[] = {8,7,6,5,4,3,2,1};
int n = sizeof(base) / sizeof(base[0]);
mergeSort(base,n);
display(base,n);
return 0;
}
二叉树、树:
二叉树的遍历
遍历:每个节点都访问,且都仅访问一次;
struct Node{
int data;
Node* lchild, Node* rchild;
/*构造函数*/
Node(int x = 0) {data = x; lchild = NULL; rchild = NULL};
};
先序遍历
void preOrderTrav(Node* root)
{
if(root == NULL)
{
return ;
}
cout<<root->data;
preOrderTrav(root->lchild);
preOrderTrav(root->rchild);
}
中序遍历
略
后序遍历
略
求二叉树的高度
求以1为根的树的高度,可以求以2为根的子树的高度和以3为根的子树的高度的最大值,然后再加1,就得到了这颗树的高度。
int getTreeHeight(Node* root)
{
if(root == NULL)
{
return 0;
}
int left = getTreeHeight(root->lchild);
int right = getTreeHeight(root->rchild);
return max(left,right) + 1;
}
表达式树的输出与求值
前缀表达式举例:- + 4 * 1 - 5 2 / 6 3,前缀表达式与后缀表达式运算开始的方向正好相反是从给出的表达式的右边向左边推导,运算符在前,运算数在后,即一个运算符对应后面紧邻的两个运算数。
中缀表达式举例:就是我们正常的顺序(4+(1*(5-2)))-(6/3)。中缀表达式不能直接根据树的中序遍历得出,要注意优先级,比如图中的5 - 2的优先级就比较高,不能直接与1相乘,而是把它们的结果与1相乘。
后缀表达式举例:4 1 5 2 - * + 6 3 / -》首先从左向右读取,直到遇到第一个运算符,取其前两个运算数,做相应的计算,比如“5 2 -”代表5 - 2 = 3,那么式子就成为 4 1 3 * + 6 3 / ,再向后读取,读到*,再向前取两个运算数:1 3 *,代表1*3 = 3,那么后缀表达式此时为4 3 + 6 /,再读取到+,4 + 3 = 7,后缀表达式为7 6 /,最后得到结果为7/6 = 1。
所以,如果题目给你一个后缀表达式应该会计算它的结果,并可以将其还原为一颗表达式树。
而也就是说代码是与树的前序中序遍历完全相同的。但是中序遍历特殊,需要考虑括号的问题,即为"a 运算符 b"变成了“(a 运算符 b)”,于是中缀遍历的代码如下:
void inOrder(Node* root)
{
if(root == NULL)
return;
cout<<'(';
inOrder(root->lchild);
cout<<root->value;
inOrder(root->rchild);
cout<<')';
}
输出结果如下:
发现问题,在叶子结点也加了中括号,所以需要解决这个问题,如果判断是叶子结点那我们就不输出()了。所以优化代码如下:
void inOrder(Node* root)
{
if(root == NULL)
return ;
if(root->lchild == NULL && root->rchild == NULL)
{
cout<<root->value;
return ;
}
cout<<'(';
inOrder(root->lchild);
cout<<root->value;
inOrder(root->rchild);
cout<<')';
}
输出结果如下:
发现,在最外层也有括号,于是要判断如果当前循环是最后一层的话,即如果是根结点的话就不要输出了,那么就可以在函参中加入一个新的层数变量,如果是零层,那么就代表是根结点:
void inOrder(Node* root,int layer)
{
if(root == NULL)
return ;
if(root->lchild == NULL && root->rchild == NULL)
{
cout<<root->value;
return ;
}
if(layer > 0) cout<<'(';
inOrder(root->lchild,layer + 1);
cout<<root->value;
inOrder(root->rchild,layer + 1);
if(layer > 0) cout<<')';
}
//main函数调用传参layer是0
优化后输出结果为:
计算表达式树
递归规律:先计算左子树,再计算右子树,然后再根据根结点来做对应的计算。
停止条件:如果是叶子结点,直接将对应的数返回即可。
double calculateExprTree(Node* root)
{
if(root->lchild == NULL && root->rchild == NULL)
{
//由于题目限定说是0-9,即这个字符串都一个字符对应一个操作符/操作数
return root->data - '0';
}
double left = calculateExprTree(root->lchild);
double right = calculateExprTree(root->rchild);
return calc(left,right,root->data);
}
如何将字符转化为对应的操作,所以设计了calc函数如下所示:
double calc(double a,double b,char op)
{
switch(op)
{
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/': return a / b;
}
}
求某结点到根结点的路径
首先考虑如何找到对应的节点,回忆链表/数组,就是通过从头到尾扫描来查找的,对于树可以选择一种遍历方式来查找,代码采用前序遍历。再考虑如何获得层数,因为只查找单一节点的层数,所以可以使用一个全局变量来存储。再思考查找的过程来调整前序遍历的代码,在root节点向下查找时,从0 layer,增长了一层,然后计算左子树的层数,同时查找有没有查找的结点,当根结点的左右子树都查找完,就返回到根结点,此时的层数就又应回到起初的0层。而在查找过程中,如果查找成功,就输出layer并结束查找。
int layer = 0;
void getNodeLayer(Node* root, int x)
{
if(root == NULL) return ;
layer++;
if(root->data == x)
{
cout<<layer<<endl;
return ;
}
getNodeLayer(root->lchild,x);
getNodeLayer(root->rchild,x);
layer--;
}
当然也可以将这个全局变量换成引用来做参数,也可以保证其值的改变。方法二,如下:
void getNodeLayer(Node* root, int x, int& layer)
{
if(root == NULL) return ;
layer++;
if(root->data == x)
{
cout<<layer<<endl;
return ;
}
getNodeLayer(root->lchild,x);
getNodeLayer(root->rchild,x);
layer--;
}
int main()
{
// ......
int layer = 0;
getNodeLayer(root, 7, layer);
}
- 问题:如何求最大的层数?
if(root->lchild == NULL && root->rchild == NULL)
{
int temp = layer;
vec.push_back(temp);
}
求结点路径
思路:求结点的路径也是一个遍历的过程,比如找结点7的路径,我们首先从根出发,查找其左右子树来寻找其路径,进入左子树后,首先进入2结点,就将2压入向量中,此时向量为1,2,2结点又向下搜索4结点,并压入向量,发现4下无子嗣,就想要回溯,到2,那就需要将向量首元素4剔除掉,以回到2的位置,继续扫描右子树,进入5结点,压入向量,再压入7,发现与查找的内容一致,达到最后的搜索目的,此时就可以将整个向量的内容逐一输出即为路径长度。如果没有找到,就会不断的遍历,压入向量,回溯弹出向量顶元素,直至回溯到根,并将根也弹出,此时向量为空。代码实现如下:
vector<int> path;
void getNodeLayer(Node* root, int x)
{
if(root == NULL)
return ;
path.pash_back(root->data);
if(root->data == x)
{
for(int x : path)
{
cout<<x<<'\t';
}
return ;
}
getNodePath(root->lchild, x);
getNodePath(root->rchild, x);
path.pop_back();
}
普通树的遍历
与二叉树不同,普通树的每个节点的分叉数是不一定的,那么如何来存储每个孩子节点呢?通过向量来存储每个孩子节点。普通树有前序遍历(先访问根,再依次访问每个孩子节点)、后序遍历(先依次访问每个孩子节点,再访问根)、层序遍历(),无中序遍历。
#include <iostream>
#include <vector>
#include <queue>
#include <initializer_list>
using namespace std;
struct Node
{
int data;
vector<Node* > child;
Node(int x = 0) {data = x;}
void setChildNode(initializer_list<Node* > il)
{
//c++11特性:initializer_list
for(Node* x : il)
{
child.push_back(x);
}
}
};
void preOrder(Node* root)
{
if(root == NULL) return ;
cout<<root->data<<endl;
for(Node* x : root->child)
{
preOrder(x);
}
}
void postOrder(Node* root)
{
if(root == NULL) return ;
for(Node* x : root->child)
{
postOrder(x);
}
cout<<root->data<<endl;
}
void levelOrder(Node* root)
{
if(root == NULL) return ;
queue<Node* > Q;
Q.push(root);
while(Q.empty() == false)
{
Node* t = Q.front();
Q.pop();
cout<<t->data<<endl;
for(Node* x : t->data)
{
Q.push(x);
}
}
cout<<endl;
}
DFS/回溯算法
如果某问题的解可以由多个步骤得到,而每个步骤都有若干种选择(这些候选方案集可能会依赖之前作出的选择),且可以用递归枚举法实现,则它的工作方式可以用解答树来描述。
全排列问题
输出数字1-N所能组成的所有全排列
这部分视频讲的感觉不如之前我的那篇博客内容简单明了,所以还是推荐大家去看上篇博客,嘿嘿。