前言:递归、分治、字符串匹对算法。个人感觉似懂非懂,先总结下来吧。开始之前,先说一下我对上面几个的基本理解:
1)递归:相同步骤,不断调用自身,但是需要设置终止条件(常规套路,先假设步骤成立,然后直接调用);
2)分治:递归时候,可能单纯一个步骤不足以解决这个问题,那么,我们就把这一解决方法,分成几个子步骤,不断递归来各个子步骤来解决问题,这种分拆解决问题的思路,就是分治;
3)字符串匹对算法:如何快速查找下次匹配开始点,就是匹配算法的关键点。
好了,下面开始,骚挠环节:
20201210
1、递归和迭代简单对比
答:
1)一般情况下,指导了检索范围,就应该使用迭代,如for循环;但是,如果检索范围未知,就应该选择递归;
2)递归效率相对比较低,而且需要使用寄存器所带来的资源浪费。
3)总之,能用迭代就迭代,递归尽量少用。
2、递归例子1_裴波拉契数列计算
答:通俗理解,兔子对数计算问题,假设兔兔不会死,从第三个月开始(终止条件),兔兔对数是前面的两个月对数之和(重复步骤,可递归)。
//裴波拉契数列递归方式实现
#include <stdio.h>
int Fib(int i) {
if (i < 2)
return i == 0 ? 0 : 1;
return Fib(i - 1) + Fib(i - 2);//重复的步骤
}
int main() {
for (int i = 0; i < 40; i++) {
printf("%d ", Fib(i));//打印兔子的对数
}
return 0;
}
3、递归相关
答:
1)递归定义:把一个直接调用自己或通过一系列的调用语句间接调用自己的函数,称为递归函数。(在高级语言中,函数调用自己和调用其他函数并没有本质的不同)
2)递归一定要设置终止条件;
3)递归的使用,可以使得程序结构看起来简洁易懂(如何写成这样子,就难了),可以减少读懂代码时间;但是大量递归会建立函数副本,会消耗大量的时间和内存,而迭代则不需要这种付出;
4)递归函数分为调用和回退阶段,递归的回退顺序就是它调用的顺序的逆序。(这句话看了,还是觉得很迷惑,调用理解简单,但是回退,指的是类似兔兔回调前面数据意思吗?)
5)递归的关键在于寻找相同点和结束点。
4、分治思想是啥?
答:
当一个问题规模较大且不易求解时候,可以考虑将问题分为几个小的模块,逐一解决,从而解决问题的思想。
5、分治与递归关系。
答:
分治思想和递归常常配合使用,因为采用了分治思想处理问题,其各个小模块通常与大问题有相同的结构,这种特性也使得递归技术有了用武之地。
6、分治与递归案例1_汉诺塔问题
答:
1)问题来源:64个盘子组成的,大盘在下小盘在上组成的塔放在一个位置X,还有两个位置Y、Z,现在需要把盘子塔从X位置移动到Z位置上,三个位置随意使用,但移动过程中,一定要保证大盘子在下小盘子在上原则。
2)问题分析:这个问题大情况来看,每个步骤都是借助第三位置,把盘子塔移动到指定位置,但是不同的是每次借助的第三位置是不固定的。(直接导致一步骤递归形式失败)
3)但是,通过分析发现,调用有规律的:
–先将前63个盘子移动到Y上,确保大盘在小盘的下面;
–再将最底下的第64个盘子移动到Z上;
–最后将Y上的63个盘子移动到Z上。
分析可以知道,由于每次只能移动一个圆盘,所以在移动的过程中显然要借助另外一位置才行。
也就是说,第1步将1~63个盘子借助Z移动到Y上,第2步将最底下的第64个盘子移动到Z上,第3步将Y针上的63个盘子借助X移动到Z。那么,把所有思路归结为以下两个问题:
–问题一:将X上的63个盘子借助Z移动到Y上;
–问题二:将Y上的63个盘子借助X移动到Z上
解决上述两个问题依然用相同方法:
问题一的圆盘移动步骤为:
–先将前62个盘子移动到Z上面,确保大盘在小盘下面
–再将最底下的第63个盘子移动到Y上
–最后将Z上的62个盘子移动到Y上
问题二的圆盘移动步骤为:
–先将前62个盘子移动到X上,确保大盘在小盘下
–再将最底下的第63个盘子移动到Z上;
–最后将X上的62个盘子移动到Y上
然后,每次都是重复问题一、二过程。
简单理解,开始位置,过渡位置,目标位置,而三者会随目标位置不同而角色不同。
//汉诺塔问题
#include<stdio.h>
//将n个盘子从x借助y移动到z
void move(int n, char x, char y, char z) {
if (1 == n) {
printf("%c-->%c\n", x, z);
}
else{
move(n - 1, x, z, y);//这里将n-1个盘子从x借助z移动到y上
printf("%c-->%c\n", x, z);//将第n个盘子移动到z上
move(n - 1, y, x, z);//将n-1个盘子从y借助x移动到z上
}
}
int main() {
int n;
printf("请输入汉诺塔的层数:");
scanf_s("%d", & n);
printf("移动步骤如下:\n");
move(n, 'X', 'Y', 'Z');
}
//很简洁的,但是看起来还是很晕,虽然分析里面原理都理解
7、递归和分治案例2_八皇后问题
答:
八皇后问题:国际棋盘上有摆放八个皇后,使其不能相互攻击,也就是说任意两个皇后不能处于同一行、同一列、同一斜线上,请问有多少种摆法。
#include<stdio.h>
int count = 0;//使用全局变量,记录八皇后摆放的方法个数
//判断棋子合适位置
int notDanger(int row, int j, int (*chess)[8]) //表示当前棋子所在的row行,j列
{
int i,k,flag1=0, flag2 = 0, flag3 = 0, flag4 = 0,flag5=0;
//判断列方向
for (i = 0; i < 8; i++) {
if (*(*(chess + i) + j) != 0) {
flag1=1;
break;
}
}
//判断左上方
for (i = row, k = j; i >= 0 && k >= 0; i--, k--) {
if (*(*(chess + i) + k) != 0) {
flag2 = 1;
break;
}
}
//判断右下方
for (i = row, k = j; i < 8 && k < 8; i++, k++) {
if (*(*(chess + i) + k) != 0) {
flag3 = 1;
break;
}
}
//判断右上方
for (i = row, k = j; i >= 0 && k < 8; i--, k++) {
if (*(*(chess + i) + k) != 0) {
flag4 = 1;
break;
}
}
//判断左下方
for (i = row, k = j; i < 8 && k >=0; i++, k--) {
if (*(*(chess + i) + k) != 0) {
flag5 = 1;
break;
}
}
if (flag1 || flag2 || flag3 || flag4 || flag5)//表示只要有一处为1,也就是说有冲突,就可以终止了
{
return 0;
}
else
{
return 1;//如果该位置属于安全位置,那么就返回
}
}
//row表示本次起始行;
//n表示列数
//(*chess)[8]表示指向棋盘每一行的指针
void EightQueue(int row,int n,int (*chess)[8]) //(*chess)[8]表示指向棋盘每一行指针
{
int chess2[8][8],i,j;
for (i = 0; i < 8; i++) {
for (j = 0; j < 8; j++) {
chess2[i][j] = chess[i][j];//利用传入的棋盘元素初始化这里的棋盘
}
}
if (8 == row) //如果row到达最后一行,那么表示棋盘王后摆放位置出来了
{
printf("第%d种\n", count+1);
for (i = 0; i < 8; i++) {
for (j = 0; j < 8; j++) {
printf("%d ", *(*(chess2 + i) + j));//打印棋盘
}
printf("\n");
}
printf("\n");
count++;//方法+1
}//结束条件
else {
//判断这个位置是否有冲突
//没有冲突,继续往下
for (j = 0; j < n; j++) //遍历每一行中的每一个位置可能性
{
if (notDanger(row,j,chess)) //判断位置是否冲突
{
for (i = 0; i < 8; i++)
{
*(*(chess2 + row) + i) = 0;
}//把棋盘的row行所在的元素全部重置为0
*(*(chess2 + row) + j) = 1;//把该行中放王后的位置重置为1
EightQueue(row + 1, n, chess2);//这里表示下一行查询合适位置;这里的row使得行不断推进;
//chess2会不断保留上一次的棋子摆放情况,直到row==8时;
}
}
}
}
//这里调用时候,我一开始会产生这么一个疑问,row到8了,不需要重置吗?后面想了想,其实在for遍历行中每一个位置时候,
//使用递归,不断开辟了副本的,所以,并不存在需要重置,然后保存新的成功方法的问题。这就是递归的恐怖地方。
int main() {
int chess[8][8], i, j;
for (i = 0; i < 8; i++) {
for (j = 0; j < 8; j++) {
chess[i][j] = 0;//初始化棋盘所有位置都为0
}
}
EightQueue(0,8,chess);
printf("总共有 %d 种方法\n", count);
return 0;
}
8、字符串种的“串”以及字符串相关知识。
答:
1)“串”定义:串(string)是由零个或多个字符串组成的有限序列,又称字符串;
2)计算机一开始都是处理数值工作,后来引入字符串的概念;计算机开始可以处理非数值概念(原理是,基于ASCII码表,采用数值来模拟非数值);
3)串可以是空串,即没有字符,直接由 ”” 表示,这里面是没有空格的,或者可以用希腊字母∅表示;
4)子串与主串,例如”fish”是”fishccc”的子串;
5)字符串的存储结构:
–其存储结构与线性表相同,也分为顺序存储结构和链式存储结构。
–字符串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列的。
–按照预定义的大小,为每个定义的字符串变量分配一个固定长度的存储区,一般用定长数组来定义
–通常直接定义一个足够长度的存储区来存储字符串的。
9、字符串匹配算法之_BF算法
答:
1)
–BF算法属于朴素的模式匹配算法,核心思想是:
–有两个字符串S和T,长度为N和M。首先S[1]和T[2]比较,若相等,则在比较S[2]和T[2],一直到为T[M]止;若S[1]和T[1]不等,则T向右移动一个字符的位置,再依次进行比较。
–该算法的最坏情况下要进行M*(N-M+1)比较,时间复杂度为O(M*N)
–在这里S是主串,T是子串,这种子串的定位操作称作串的模式匹配。
2)简单理解,有两个字符串,主串与子串,子串从第一个字符跟主串的第一个字符匹对,若出现不匹对情况,则将子串后移主串下一位开始,依次重新匹对,直到找到或者达到主串末端。
3)这种匹对方式比较简单粗暴,但是效率较低。
10、字符串匹配算法_KMP算法
答:
1)这个算法作用就是,下次子串开始匹配更合适的位置查询也就是回溯更高效。这里直接从代码原理来解析。
2)KMP算法的结构感性认识:
从左到右k值得出:
0 第一号元素规定填写0
1 第二号元素,前面就一个元素,不存在匹配情况,故填1
1 第三号元素,前面a,b不匹配,所以填1
2 第四号元素,前面元素,前缀a,和后缀a匹配,有一个字符匹配的,所以填2
3 第五号元素,前面,前缀ab,和后缀ab匹配,有两个字符匹配的,所以填写3
4 第六号元素,前面,前缀aba和后缀aba匹配,有三个字符匹配(虽然a重复了),所以填写4
2 第七号元素,前面,前缀a和后缀a匹配,只有一个字符匹配,所以填写2
2 第八号元素,前面,前缀a和后缀a匹配,只有一个字符匹配,所以填写2
3 第九号元素,前面,前缀ab和后缀ab匹配,有两个字符匹配,所有填写3
从上面看出k数组数值的填写,而k数组里面的数值就是指明了模式匹配串下一步该怎么移动。
3)KMP算法数组代码原理分析:
模式匹配串添加一个k数组(也就是KMP算法中非著名的next数组)
K数组属一种“智能”的数组,因为它指导着模式匹配串中下一步改用第几号元素去匹配。
其实就是根据子串的数据特征来决定回溯的位置,也就是kmp算法的核心。
匹配时,只是考虑子串T。
//T 9 a b a b a a a b a
//下标 0 1 2 3 4 5 6 7 8 9
//next x 0 1 1 2 3 4 2 2 3
//next也就是K数组;根据代码,可以把上面的next填写出来的;下面的前后缀值也是代码流程得出
//后缀i = 1 2 2 3 4 5 6 6 6 7 7 8 9
//前缀j = 0 1 0 1 2 3 4 2 1 2 1 2 3
//-匹配时,只管子串T串,不用管主串S;在T串中,下标为1时,next的k值就为1
//前后缀都是表示下标的
void get_next(std::string T, int* next) {
//kmp算法核心部分,next数组教你怎么进行下次
//回溯(体现在前缀j的下次位置在哪)
int j = 0;//前缀
int i = 1;//后缀
next[1] = 0;
while (i < T[0])//T[0]数值是保存字符串的长度
{
if (j == 0 || T[i] == T[j]) //这里添加j==0判断,使得前后缀都从T串的保存
//字符处开始;前缀后缀的元素值相等时,那么
//进行下一组前后缀的匹对
{
i++;
j++;
next[i] = j; //下次后缀作为下标的next数组的量值,保留
//着下次匹配的前缀下标
}
else
{
j = next[j];//j回溯:当模式匹配串T失配的时候,
//NEXT数组对应的元素指导用T串的哪个元素进行下一轮匹配。
}
}
}
//因为前缀是固定的,后缀是相对的
//前缀固定是由于,下标为1的元素一直作为前缀的开始
//关键理解的是next数组里面的前缀跳动点在哪里了
//巧记:
//后前缀为i,j,初始i、j、next100分
//若j0或串Tij等,ij+,nexti/j;
//否则回溯,j/nextj
11、字符串匹配算法_KMP算法改进版
答:
1)缘由:–有人发现,kmp算法是有缺陷的,比如主串S=”aaabcde”,子串 T=”aaaaax”,其中很容易得到next数组为012345
这里可以发现,子串中的前面5个都是相同的,应该直接回溯到第一个a位置可以了,不应该经过这么多步的(next数组回到第一个a有多个值)
2)代码:
这里的T表示的是子串,S表示主串。
typedef char* string;
//kmp算法的改进
void get_next(string T, int* next) {
int i = 1;
int j = 0;
next[i] = 0;
while (i < T[0])
{
if (j == 0 || T[i] == T[j]) {
i++;
j++;
//next[i] = j;//未改进版本的kmp写法
//改进部分:当子串中出现保存字符一样情况,不做无用功回溯
if (T[i] != T[j]) {
next[i] = j;
}
else {
next[i] = next[j];
}
}
else
{
j = next[j];//j回溯
}
}
}
//返回子串T在主串S第pos个字符之后的位置
//若不存在,则返回0
int index_kmp(string s, string T, int pos) {
int i = pos;
int j = 1;
int next[255];
get_next(T, next);
while (i <= s[0] && j <= T[0]) //这里表示若任意一个串比对已经达到末尾时
//则查询结束
{
if (j==0||s[i] == T[j]) {
i++;
j++;
}
else {
j = next[j];
}
}
if (j > T[0]) {
return i - T[0];
}
else {
return 0;
}
}
感觉这一章虽然内容貌似不多,但是很绕脑子,感觉还是多看几次,也需要实战多次,才能消化。
#########################
不积硅步,无以至千里
好记性不如烂笔头
致谢授课老师的付出