文章目录
一、串
1.串的基本概念
- 串:由零个或多个字符组成的有限序列。
- 空串:含零个字符的串,用∅表示。
- 通常将一个串表示成"a1a2···an“的形式,其中最外边的双引号(或单引号)不是串的内容,它们是串的标志,用于将串与标识符(例如变量名等)加以区别。每个ai(1≤i≤n)代表一个字符,不同的计算机和编程语言对合法字符(即允许使用的字符)有不同的规定。但在一般情况下,英文字母、数字(0,1,···,9)和常用的标点符号以及空格符等都是合法的字符。
- 两个串相等当且仅当这两个串的长度相等并且各对应位置上的字符都相同。一个串中任意个连续字符组成的序列称为该串的子串,例如串"abcde"的子串有“a”、"ab"、"abc"和"abcd"等。为了表述清楚,在串中空格字符用“□”符号表示,例如"a□□b"是一个长度为4的串,其中含有两个空格字符。空串是不包含任何字符的串,其长度为0,空串是任何串的子串。
- 串的抽象数据类型描述如下:
ADT String
{
数据对象:
D={ ai | 1≤i≤n,n≥0,ai为char类型}
数据关系:
R={<ai,ai+1> | ai,ai+1∈D,i=1,···,n-1}
基本运算:
StrAssign(&s,cstr):将字符串常量cstr赋给串s,即生成其值等于cstr的串s。
DestroyStr(&s):销毁串,释放为串s分配的存储空间。
StrCopy(&s,t):串复制,将串t赋给串s。
StrEqual(s,t):判断串是否相等,若两个串s与t相等则返回真;否则返回假。
StrLength(s):求串长,返回串s中字符的个数。
Concat(s,t):串连接,返回由两个串s和t连接在一起形成的新串。
SubStr(s,i,j):求子串,返回串s中从第i((1≤i≤n)个字符开始的由连续j个字符组成的子串。
InsStr(s1,i,s2):子串的插入,将串s2插入串s1的第i(1≤i≤n+1)个位置,并返回产生的新串。
DelStr(s,i,j):子串的删除,从串s中删去从第ii(1≤i≤n))个字符开始的长度为j的子串,并返回产生的新串。
RepStr(s,i,j,t):子串的替换,在串s中将从第i(1≤i≤n)个字符开始的j个字符构成的子串用串t替换,并返回产生的新串。
DispStr(s):串的输出,输出串s的所有字符值。
2.串的存储结构
和线性表一样,串也有顺序存储结构和链式存储结构,前者称为顺序串,后者称为链串。
1)串的顺序存储结构——顺序串
typedef struct
{ char data[MaxSize]; //串中字符
int length; //串长
} SqString; //声明顺序串类型
void StrAssign(SqString &s,char cstr[]) //字符串常量赋给串s
{
int i;
for (i=0;cstr[i]!='\0';i++)
s.data[i]=cstr[i];
s.length=i;
}
void DestroyStr(SqString &s) //销毁串
{ }
void StrCopy(SqString &s,SqString t) //串复制
{
for (int i=0;i<t.length;i++)
s.data[i]=t.data[i];
s.length=t.length;
}
bool StrEqual(SqString s,SqString t) //判串相等
{
bool same=true;
if (s.length!=t.length) //长度不相等时返回0
same=false;
else
for (int i=0;i<s.length;i++)
if (s.data[i]!=t.data[i]) //有一个对应字符不相同时返回假
{ same=false;
break;
}
return same;
}
int StrLength(SqString s) //求串长
{
return s.length;
}
SqString Concat(SqString s,SqString t) //串连接
{
SqString str;
int i;
str.length=s.length+t.length;
for (i=0;i<s.length;i++) //s.data[0..s.length-1]→str
str.data[i]=s.data[i];
for (i=0;i<t.length;i++) //t.data[0..t.length-1]→str
str.data[s.length+i]=t.data[i];
return str;
}
SqString SubStr(SqString s,int i,int j) //求子串
{
SqString str;
int k;
str.length=0;
if (i<=0 || i>s.length || j<0 || i+j-1>s.length)
return str; //参数不正确时返回空串
for (k=i-1;k<i+j-1;k++) //s.data[i..i+j]→str
str.data[k-i+1]=s.data[k];
str.length=j;
return str;
}
SqString InsStr(SqString s1,int i,SqString s2) //插入串
{
int j;
SqString str;
str.length=0;
if (i<=0 || i>s1.length+1) //参数不正确时返回空串
return str;
for (j=0;j<i-1;j++) //s1.data[0..i-2]→str
str.data[j]=s1.data[j];
for (j=0;j<s2.length;j++) //s2.data[0..s2.length-1]→str
str.data[i+j-1]=s2.data[j];
for (j=i-1;j<s1.length;j++) //s1.data[i-1..s1.length-1]→str
str.data[s2.length+j]=s1.data[j];
str.length=s1.length+s2.length;
return str;
}
SqString DelStr(SqString s,int i,int j) //串删去
{
int k;
SqString str;
str.length=0;
if (i<=0 || i>s.length || i+j>s.length+1) //参数不正确时返回空串
return str;
for (k=0;k<i-1;k++) //s.data[0..i-2]→str
str.data[k]=s.data[k];
for (k=i+j-1;k<s.length;k++) //s.data[i+j-1..s.length-1]→str
str.data[k-j]=s.data[k];
str.length=s.length-j;
return str;
}
SqString RepStr(SqString s,int i,int j,SqString t) //子串替换
{
int k;
SqString str;
str.length=0;
if (i<=0 || i>s.length || i+j-1>s.length) //参数不正确时返回空串
return str;
for (k=0;k<i-1;k++) //s.data[0..i-2]→str
str.data[k]=s.data[k];
for (k=0;k<t.length;k++) //t.data[0..t.length-1]→str
str.data[i+k-1]=t.data[k];
for (k=i+j-1;k<s.length;k++) //s.data[i+j-1..s.length-1]→str
str.data[t.length+k-j]=s.data[k];
str.length=s.length-j+t.length;
return str;
}
void DispStr(SqString s) //输出串s
{
if (s.length>0)
{ for (int i=0;i<s.length;i++)
printf("%c",s.data[i]);
printf("\n");
}
}
- 将子串插入的算法设计:
功能:将顺序串s2插入插入顺序串s1的第i个位置上,并返回产生的结果串,如果参数不正确则返回一个空串。
SqString InsStr(SqString s1,int i,SqString s2)
{ int j;
SqString str; //定义结果串
str.length=0; //设置str为空串
if(i<=0||i>s1.length+1) //参数不正确时返回空串
return str;
for(j=0;j<i-1;j++) //s1.data[0..i-2]→str
str.data[j]=s1.data[j]
for(j=0;j<s2.length;j++) //s2.data[0..s2.length-1]→>str
str.data[i+j-1]=s2.data[j];
for(j=i-1;j<sl.length;j++) //s1.data[i-1..s1.length-1]->str
str.data[s2.length+j]=s1.data[j];
str.length=sl.length+s2.length;
return str;
}
2)串的链式存储结构——链串
- 链串的组织形式宇一般的单链表类似,主要区别在于链串中的一个结点可以存储多个字符,通常将链串中每个结点所存储的字符个数称为结点大小。
- 当结点大小大于1(例如结点大小为4)时,链串的尾结点的各个数据域不一定总能全被字符占满。此时应在这些未占用的数据域里补上不属于字符集的特殊符号(例如'#'字符),以示区别。显然结点大小越大,存储密度越大,但相关算法设计越麻烦,因为可能引起大量字符的移动。当结点大小为1时,每个结点存放一个字符,相关算法设计十分方便,但存储密度较低。
- 为了简便,这里规定链串的结点大小均为1,相应的链串结点类型LinkStrNode的声明如下:
typedef struct snode
{
char data;
struct snode *next;
} LinkStrNode;
void StrAssign(LinkStrNode *&s,char cstr[]) //字符串常量cstr赋给串s
{
LinkStrNode *r,*p;
s=(LinkStrNode *)malloc(sizeof(LinkStrNode));
r=s; //r始终指向尾节点
for (int i=0;cstr[i]!='\0';i++)
{ p=(LinkStrNode *)malloc(sizeof(LinkStrNode));
p->data=cstr[i];
r->next=p;r=p;
}
r->next=NULL;
}
void DestroyStr(LinkStrNode *&s) //销毁串
{ LinkStrNode *pre=s,*p=s->next; //pre指向节点p的前驱节点
while (p!=NULL) //扫描链串s
{ free(pre); //释放pre节点
pre=p; //pre、p同步后移一个节点
p=pre->next;
}
free(pre); //循环结束时,p为NULL,pre指向尾节点,释放它
}
void StrCopy(LinkStrNode *&s,LinkStrNode *t) //串t复制给串s
{
LinkStrNode *p=t->next,*q,*r;
s=(LinkStrNode *)malloc(sizeof(LinkStrNode));
r=s; //r始终指向尾节点
while (p!=NULL) //将t的所有节点复制到s
{ q=(LinkStrNode *)malloc(sizeof(LinkStrNode));
q->data=p->data;
r->next=q;r=q;
p=p->next;
}
r->next=NULL;
}
bool StrEqual(LinkStrNode *s,LinkStrNode *t) //判串相等
{
LinkStrNode *p=s->next,*q=t->next;
while (p!=NULL && q!=NULL && p->data==q->data)
{ p=p->next;
q=q->next;
}
if (p==NULL && q==NULL)
return true;
else
return false;
}
int StrLength(LinkStrNode *s) //求串长
{
int i=0;
LinkStrNode *p=s->next;
while (p!=NULL)
{ i++;
p=p->next;
}
return i;
}
LinkStrNode *Concat(LinkStrNode *s,LinkStrNode *t) //串连接
{
LinkStrNode *str,*p=s->next,*q,*r;
str=(LinkStrNode *)malloc(sizeof(LinkStrNode));
r=str;
while (p!=NULL) //将s的所有节点复制到str
{ q=(LinkStrNode *)malloc(sizeof(LinkStrNode));
q->data=p->data;
r->next=q;r=q;
p=p->next;
}
p=t->next;
while (p!=NULL) //将t的所有节点复制到str
{ q=(LinkStrNode *)malloc(sizeof(LinkStrNode));
q->data=p->data;
r->next=q;r=q;
p=p->next;
}
r->next=NULL;
return str;
}
LinkStrNode *SubStr(LinkStrNode *s,int i,int j) //求子串
{
int k;
LinkStrNode *str,*p=s->next,*q,*r;
str=(LinkStrNode *)malloc(sizeof(LinkStrNode));
str->next=NULL;
r=str; //r指向新建链表的尾节点
if (i<=0 || i>StrLength(s) || j<0 || i+j-1>StrLength(s))
return str; //参数不正确时返回空串
for (k=0;k<i-1;k++)
p=p->next;
for (k=1;k<=j;k++) //将s的第i个节点开始的j个节点复制到str
{ q=(LinkStrNode *)malloc(sizeof(LinkStrNode));
q->data=p->data;
r->next=q;r=q;
p=p->next;
}
r->next=NULL;
return str;
}
LinkStrNode *InsStr(LinkStrNode *s,int i,LinkStrNode *t) //串插入
{
int k;
LinkStrNode *str,*p=s->next,*p1=t->next,*q,*r;
str=(LinkStrNode *)malloc(sizeof(LinkStrNode));
str->next=NULL;
r=str; //r指向新建链表的尾节点
if (i<=0 || i>StrLength(s)+1) //参数不正确时返回空串
return str;
for (k=1;k<i;k++) //将s的前i个节点复制到str
{ q=(LinkStrNode *)malloc(sizeof(LinkStrNode));
q->data=p->data;
r->next=q;r=q;
p=p->next;
}
while (p1!=NULL) //将t的所有节点复制到str
{ q=(LinkStrNode *)malloc(sizeof(LinkStrNode));
q->data=p1->data;
r->next=q;r=q;
p1=p1->next;
}
while (p!=NULL) //将节点p及其后的节点复制到str
{ q=(LinkStrNode *)malloc(sizeof(LinkStrNode));
q->data=p->data;
r->next=q;r=q;
p=p->next;
}
r->next=NULL;
return str;
}
LinkStrNode *DelStr(LinkStrNode *s,int i,int j) //串删去
{
int k;
LinkStrNode *str,*p=s->next,*q,*r;
str=(LinkStrNode *)malloc(sizeof(LinkStrNode));
str->next=NULL;
r=str; //r指向新建链表的尾节点
if (i<=0 || i>StrLength(s) || j<0 || i+j-1>StrLength(s))
return str; //参数不正确时返回空串
for (k=0;k<i-1;k++) //将s的前i-1个节点复制到str
{ q=(LinkStrNode *)malloc(sizeof(LinkStrNode));
q->data=p->data;
r->next=q;r=q;
p=p->next;
}
for (k=0;k<j;k++) //让p沿next跳j个节点
p=p->next;
while (p!=NULL) //将节点p及其后的节点复制到str
{ q=(LinkStrNode *)malloc(sizeof(LinkStrNode));
q->data=p->data;
r->next=q;r=q;
p=p->next;
}
r->next=NULL;
return str;
}
LinkStrNode *RepStr(LinkStrNode *s,int i,int j,LinkStrNode *t) //串替换
{
int k;
LinkStrNode *str,*p=s->next,*p1=t->next,*q,*r;
str=(LinkStrNode *)malloc(sizeof(LinkStrNode));
str->next=NULL;
r=str; //r指向新建链表的尾节点
if (i<=0 || i>StrLength(s) || j<0 || i+j-1>StrLength(s))
return str; //参数不正确时返回空串
for (k=0;k<i-1;k++) //将s的前i-1个节点复制到str
{ q=(LinkStrNode *)malloc(sizeof(LinkStrNode));
q->data=p->data;q->next=NULL;
r->next=q;r=q;
p=p->next;
}
for (k=0;k<j;k++) //让p沿next跳j个节点
p=p->next;
while (p1!=NULL) //将t的所有节点复制到str
{ q=(LinkStrNode *)malloc(sizeof(LinkStrNode));
q->data=p1->data;q->next=NULL;
r->next=q;r=q;
p1=p1->next;
}
while (p!=NULL) //将节点p及其后的节点复制到str
{ q=(LinkStrNode *)malloc(sizeof(LinkStrNode));
q->data=p->data;q->next=NULL;
r->next=q;r=q;
p=p->next;
}
r->next=NULL;
return str;
}
void DispStr(LinkStrNode *s) //输出串
{
LinkStrNode *p=s->next;
while (p!=NULL)
{ printf("%c",p->data);
p=p->next;
}
printf("\n");
}
- 求子串的算法设计:
功能:返回链串s中从第i个字符开始的,由连续j个字符组成的子串,当参数不正确时返回一个空串。
LinkStrNode *SubStr(LinkStrNode *s,int i,int j)
{
int k;
LinkStrNode *str,*p=s->next,*q,*r;
str=(LinkStrNode*)malloc(sizeof(LinkStrNode));
str->next=NULL; //置结果串str为空串
r=str; //r指向结果串的尾结点
if(i<=0 || i>StrLength(s) || j<0 || i+j-1>StrLength(s))
return str; //参数不正确时返回空串
for(k=1;k<i;k++) //让p指向链串s的第i个结点
p=p->next;
for(k=1;k<=j;k++) //将s的从第i个结点开始的j个结点复制到str
{ q=(LinkStrNode *)malloc(sizeof(LinkStrNode));
q->data=p->data;
r->next=q;
r=q;
p=p->next;
}
r->next=NULL; //将尾结点的next域置为空
return str;
}
3.串的模式匹配
设有两个串s和t,s称为目标串,t称为模式串,在串s中找到一个与串t相等的子串称为模式匹配
1)Brute-Force算法
Brute-Force(暴力)简称为BF算法,也称简单匹配算法,采用穷举方法,其基本思路是从目标串s="s0 s1···sn-1”的第一个字符开始和模式串t="t0t1···tm-1”中的第一个字符比较,若相等,则继续逐个比较后续字符;否则从目标串s的第二个字符开始重新与模式串t的第一个字符进行比较。以此类推,若从目标串s的第i个字符开始,每个字符依次和模式串t中的对应字符相等,则匹配成功,该算法返回位置i(表示此时t的第一个字符在s中出现的下标)。如果从s的每个字符开始均匹配失败,则t不是s的子串,算法返回-1。
int BF(SqString s,SqString t)
{
int i=0,j=0;
while (i<s.length &&j<t.length) //两个串都没有遍历完时循环
{ if(s.data[i]==t.data[j]) //当前比较的两个字符相同
{
i++;
j++;
} //依次比较后续的两个字符
else //当前比较的两个字符不相同
{
i=i-j+1;
j=0;
} //遍历s的i回退,遍历t的j从0开始
if (j>=t.length) //j超界,表示t是s的子串
return(i-t.length); //返回t在s中的位置
else //模式匹配失败
return(-1); //返回-1
}
易于理解,但效率不高
2)KMP算法
与Brute-Force算法相比,KMP算法主要是消除了主指针的回溯
(1)从模式串t中提取加速匹配的信息
在KMP算法中,通过分析模式串t从中提取出加速匹配的有用信息。这这种信息是对于t的每个字符tj(0 ≤ j ≤ m-1)存在一个整数k(k<j),使得模式串t中开头的k个字符(t0 t1…tk-1)依次与tj,的前面k个字符(tj-ktj-k+1…tj-1,这里第一个字符tj-k最多从t1开始,所以k<j)相同。如果这样的k有多个,取其中最大的一个。模式串t中每个位置j的字符都有这种信息,采用next数组表示,即next[j]=MAX{h}。
void GetNext(SqString t,int next[]) //由模式串t求出next数组
{
int j,k;
j=0;
k=-1; //j遍历t,k记录t[j]之前与t开头相同的字符个数
next[0]=-1; //设置next[0]值
while(j<t.length-1) //求t所有位置的next值
{ if(k==-1 lldata[j]==t.data[k] //k为-1或比较的字符相等时
{ j++;
k++; //j、k依次移到下一个字符
next[j]=k; //设置 next[j]为k
}
else k=next[k]; //k回退
}
}
(2)KMP算法的模式匹配过程
int KMPIndex(SqString s,SqString t) //KMP算法
{
int next[MaxSize],i=0,j=0;
GetNext(t,next);
while (i<s.length && j<t.length)
{
if(j==-1|| s.data[i]==t.data[j])
{
i++;
j++; //i、j各增1
}
else
j=next[j]; //i不变,j后退
}
if(j>=t.length) //匹配成功
return(i-t.length); //返回子串的位置
else //匹配不成功
return(-1); //返回-1
}
(3)改进的KMP算法
void GetNextval(SqString t, int nextval[])//由模式串t求出 nextval值
{
int j=0,k=-1;
nextval[0]=-1;
while (j< t.length-1)
{ if (k==-1 || t.data[j] == t.data[k])
{ j++;
k++;
if (t.data[j]!=t.data[k])
nextval[j]=k;
else
nextval[j]=nextval[k];
}
else
k=nextval[k];
}
}
int KMPIndex1(SqString s, SqString t)//改进后的KMP算法
{
int nextval[MaxSize],i=0,j=0;
GetNextval(t, nextval);
while (i< s.length && j<t.length)
{ if(j==-1 || s.data[i]==t.data[j])
{ i++;
j++;
}
else
j=nextval[j];
}
if(j>=t.length)
return(i-t.length);
else
return(-1);
}
二、递归
1.什么是递归
(1)递归的定义
- 递归:在定义一个过程或函数时出现调用本过程或本函数的成分。
- 直接递归:调用自身
间接递归:若过程或函数p调用过程或函数q,而q又调用p
任何间接递归算法郁可以转换为直接递归算法来实现。 - 一些与递归有关的概念
(1)递归数列指的是由递归关系所确定的数列。
(2)递归过程指的是直接或间接调用自身的过程。
(3)递归算法指的是包含递归过程的算法。
(4)递归程序指的是直接或间接调用自身的程序。
(5)递归方法指的是一种在有限步骤内根据特定的法则或公式对一个或多个前面的元素进行运算,以确定一系列元素(例如数或函数)的方法。 - 一般来说,能够用递归解决的问题应该满足以下3个条件:
(1)需要解决的问题可以转化为一个或多个子问题来求解,而这些子问题的求解方法与原问题完全相同,只是在数量规模上不同。
(2)递归调用的次数必须是有限的。
(3)必须有结束递归的条件来终止递归。 - 递归算法通常把一个大的复杂问题层层转化为一个或多个与原问题相似的规模较小的问题来求解。
- 递归算法的优点是结构简单、清晰,易于阅读,方便其正确性证明,缺占具管法执行中占用的内存空间较多,执行效率低,不容易优化。
(2)何时使用递归
经常要用到递归的3种情况
1.定义是递归的
2.数据结构是递归的
3.问题的求解方法是递归的
(3)递归模型
- 一般情况下,一个递归模型由递归出口和递归体两部分组成。
递归出口:确定递归到何时结束,即指出明确的递归结束条件。
递归体:确定递归求解时的递推关系。 - 递归出口的一般格式: f(s1)=m1
这里的s1与m1,均为常量,有些递归问题可能有几个递归出口。 - 递归体的一般格式:f (sn)=g(f (si) ,f (si+1),…,f (sn-1),cj, cj+1,…,cm )
- 递归思路是把一个不能或不好直接求解的大问题转化成一个或几个小问题来解决。
- 设简化·的递归模型为
f(s1) = m1
f(sn) = g(f(sn-1) , cn-1)
分解过程:f(sn) -> f(sn-1) -> … -> f(s2) -> f(s1)
求值过程:f(s1) = m1 -> f(s2) = g( f(s1) , c1) -> f(s3) = g( f(s2) , c2 ) -> … ->f(sn) = g(f(sn-1) , cn-1)
举例:
对于求Fibonacci数列的Fib1算法,求Fib1(6)的构成的递归树:
(4)递归与数学归纳法
上述过程相当于数学归纳法,可以说递归的思想来自于数学归纳法。
2.栈和递归
1)函数调用栈
- 大多数CPU上的程序实现使用栈来支持函数调用操作。
- 栈帧结构:单个函数调用操作所使用的函数调用栈。
- 每次函数调用时都会相应地创建一帧,保存返回地址、函数实参和局部变量值等,并将该帧压入调用栈。若在该函数返回之前又发生了新的调用,则同样要将与新函数对应的一帧进栈,成为栈顶。函数一旦执行完毕,对应的帧便出栈,控制权交还给该函数的上层调用函数,并按照该帧中保存的返回地址确定程序中继续执行的位置。
int main(){
int m, n;
…
f(m,n);//后面第一个语句的地址为d1
…
return 1;
}
void f(int s,int t){
int i;
…
g(i);//后面第一个语句的地址为d2
…
}
void g(int d){
int x,y;
…
}
在执行上述程序时,假设main函数的返回地址为d0。当执行main 函数时,将栈帧①进栈。在main函数中调用f函数时,将栈帧②进栈。在f函数中调用g 函数时﹐将栈帧③进栈,如下图所示。当g函数执行完毕,将栈帧③退栈,控制权交回到f函数,转向其中的d2地址继续执行,其余执行过程类似。
2)递归调用的实现
递归是函数调用的一种特殊情况.即它是调用自身代码。但这些调用在内部实现时并不是每次调用真正去复制一个复制件存放到内存中,而是采用代码共享的方式,也就是它们都是调用同-个函数的代码,系统为每一次调用开辟一组存储单元,用来存放本次调用的返回地址以及被中断的函数的参数值。这些单元以栈的形式存放,每调用一次进栈一次,当返回时执行出栈操作,把当前栈顶保留的值送回相应的参数中进行恢复,并按栈顶中的返回地址从断点继续执行。
3)递归算法的时空性能分析
1.递归算法的时间复杂度分析
- 不同于非递归算法分析的方法,递归算法属于变长时空分析,非递归算法属于定长时空分析。
- 在递归算法分析中,首先应写出对应的递归式,然后求解递推式得出算法的执行时间或者空间。
- 例如,对于Hanoi问题的递归算法,分析其时间复杂度的过程如下。设Hanoil(n,x,y,z)的执行时间为T(n),则两个问题规模为n-1的子问题的执行时间均为T(n-1),总时间是累加关系。对应的递推式如下:
T(n)=1当n=1时
T(n)=2T(n-1)+1当n>1时
则:T(n)=2T(n-1)+1
=2(2T(n-2)+1)+1
=2²T(n−2)+2+1
=2²(2T(n−3)+1)+2+1
=2³T(n−3)+2²+2+1
=…
=2n−1T(1)+2n−2+⋯+22+2+1
=2ⁿ-1
=O(2ⁿ)
2.递归算法的空间复杂度分析
- 递归算法执行中使用了系统栈空间,因此需要根据递归调用的深度来分析递归算法的空间复杂度,其过程与递归算法的时间复杂度分析类似。
- 例如,对于求Hanoi 问题的递归算法,分析其空间复杂度的过程如下。
设Hanoil(n,x,y,z)的临时空间为S(n),则两个问题规模为n-1的子问题的临时空间均为S(n-1),总空间是最大值关系,因为前一个子问题执行完毕其空间被释放,释放的空间被后一个子问题重复使用。对应的递推式如下:
S(n)=1当n=1时
S(n)=S(n-1)+1当n>1时
则:S(n)=S(n−1)+1
=(S(n−2)+1)+1
=…
=S(1)+1+⋯+1
=1+1+⋯+1
=O(n)
4)递归到非递归的转换
通常递归算法的执行效率较差,当递归调用层次较深时容易出现“栈溢出”,可以将递归算法转换为等效的非递归算法,主要有两种方法,即直接转换法和间接转换法。
1.直接转换法
- 直接转换法就是用迭代方式或者循环语言替代多次重复的递归调用。直接转换法通常用来消除尾递归和单向递归。
- 尾递归:一个递归函数中的递归调用语句是最后一条执行语句,且把当前运算结果放在参数里传给下层函数
- 例如
int factl (int n,int ans){ //求n!的尾递归算法
if(n==1)
return ans;
else
return fact1(n-1,n* ans);
}
- 有些编译器针对尾递归的特点通过优化将返回地址不保存在系统栈中,从而节省栈空间开销。单向递归是指递归的求值过程总是朝着一个方向进行的,例如,前面求Fibonacci数列的递归算法Fib1就属于单向递归,因为求值过程是1,1,2,3,5,8,··,即单向生长的。可以采用迭代方式将Fib1转换为如下非递归算法:
int Fib2(int n){//求 Fibonacci数列的第n项
int a=1,b=1,i,s;
if (n==1 || n==2)
return 1;
else{
for(i=3;i<=n;i++)
s=a+b;
a=b;
b=s;
}
return s;
}
2.间接转换法
- 其他相对复杂的递归算法不能直接求值,在执行中需要回溯。可以在理解递归调用实现过程的基础上用栈来模拟递归执行过程,即使用栈保存中间结果,从而将其转换为等效的非递归算法,这称为间接转换法。
- 例如,在将前面求解Hanoi问题的递归算法Hanoi1转换为等价的非递归算法时,需要使用一个栈暂时存放还不能直接移动盘片的任务/子任务。
typedef struct{
int n; //盘片的个数
char x,y,z; //3个塔座
boal flag; //可直接移动盘片时为true,否则为false
}ElemType; //顺序栈中元素的类型
typedef struct{
ElemType data[MaxSize]; //存放元素
int top; //栈顶指针
}StackType; //顺序栈的类型
void Hanoi2(int n,char x, char y, char z){
StackType *st; //定义顺序栈指针
ElemType e,el,e2,e3;
if (n<=0) return; //参数错误时直接返回
InitStack(st); //初始化栈
e.n=n;
e.x=x;
e.y=y;
e.z=z;
e.flag = false;
Push(st,e); //元素e进栈
while (!StackEmpty(st)){ //栈不空时循环
Pop(st,e); //出栈元素e
if (e.flag==false){ //当不能直接移动盘片时
e1.n=e.n-1;
e1.x=e.y;
e1.y=e.x;
e1.z=e.z;
if (e1.n==1) //只有一个盘片时可直接移动
e1.flag = true;
else //有一个以上盘片时不能直接移动
e1.flag = false;
Push(st,e1); //处理Hanoi(n-l,y,x,z)步骤
e2.n=e.n;
e2.x=e.x;
e2.y=e.y;
e2.z=e.z;
e2.flag = true;
Push(st,e2); //处理move(n,x,z)步骤
e3.n=e.n-1;
e3.x=e.x;
e3.y=e.z;
e3.z=e.y;
if (e3.n==1) //只有一个盘片时可直接移动
e3.flag=true;
}
else
e3.flag=false; //有一个以上盘片时不能直接移动
Push(st,e3); //处理Hanoi(n-1,x,z,y)步骤
else //当可以直接移动时
printf("\t将第%d个盘片从%c移动到%c\n”,e.n,e.x,e.z);
}
DestroyStack(st); //销毁栈
}
3.递归算法的设计
1)递归算法的设计步骤
递归算法设计的基本步骤是先确定求解问题的递归模型,再转换成对应的C/C++语言函数。
求解问题递归模型(简化递归模型)的步骤如下:
(1)对原问题f(sn)进行分析,假设出合理的小问题f(sn-1)。
(2)假设小问题f(sn-1)是可解的,在此基础上确定大问题f(sn)的解,即给出f(sn)
与f(sn-1)之间的关系,也就是确定递归体(与数学归纳法中假设i=n-1时等式成立,再求证i=n时等式成立的过程相似)。
(3)确定一个特定情况(例如f(1)或f(0))的解,由此作为递归出口(与数学归纳法中求证i=1或i=0时等式成立相似)。
2)基于递归数据结构的递归算法设计
- 具有递归特性的数据结构称为递归数据结构。递归数据结构通常是采用递归方式定义的。在一个递归数据结构中总会包含一个或者多个递归运算。
- 例如,正整数的定义为1是正整数,若n是正整数(n≥1),则n+1也是正整数。从中可以看出,正整数就是一种递归数据结构。显然,若n是正整数(n>1),m=n-1也是正整数,也就是说,对于大于1的正整数n,n-1是一种递归运算。所以在求n!的算法中,递归体f(n)=n*f(n-1)是可行的,因为对于大于1的n,n和n-1都是正整数。
一般情况下,对于递归数据结构
RD=(D,Op)
其中,D={di}(1≤i≤n,共n个元素)为构成该数据结构的所有元素的集合,Op是递归运算的集合,Op=(opj}(1≤j≤m,共m个运算),对于d∈D,不妨设opj为一元运算符,则有opj(di)∈D.也就是说,递归运算具有封闭性。
在上述正整数的定义中,D是正整数的集合,Op=(op1,op2)由两个基本递归运算符构成,op1的定义为op1(n)=n-1(n>1);op2的定义为op2(n)=n+1(n≥1)。
对于不带头结点的单链表,其结点类型为LinkNode,每个结点的next域为LinkNode类型的指针,这样的单链表通过首结点指针来标识。采用递归数据结构的定义如下:
SL=(D,Op)
其中,D是由部分或全部结点构成的单链表的集合(含空单链表),Op={op1},op1的定义如下:
op1(L)=L-> next //为含一个或一个以上结点的单链表
显然这个递归运算符是一元运算符,且具有封闭性。也就是说,若L为不带头结点的非空单链表,则L->next也是一个不带头结点的单链表。
实际上,递归算法设计步骤中的第2步是用于确定递归模型中的递归体。在假设原问题f(s)合理的小问题f(s’)时,需要考虑递归数据结构的递归运算。例如,在设计不带头结点的单链表的递归算法时,通常设s为以L为首结点指针的整个单链表,s’为除首结点以外余下结点构成的单链表(由L->next标识,而该运算为递归运算)。
3)基于递归求解方法的递归算法设计
当求解问题的方法是递归(例如Hanoi问题)的或者可以转换成递归方法求解时(例如皇后问题),可以设计成递归算法。