5.7 递归与广义表的递归

递归

  • 一般来说,递归的执行效率很低,实际应用中能不用就不要用。

  • 递归一般是由基本项和归纳项组成。
    基本项:描述了递归过程的一个或几个终结状态(不需要再继续递归而可直接求解的状态)。实际应用中的递归过程必定要经过有限的递归层次到达终结状态。
    归纳项:描述如何从当前状态到终结状态的转化。

  • 递归的实质:一个复杂的问题可以分解为若干子问题来处理;其中的某些子问题与原问题有着相同的特征属性;那么就可以运用与原问题相同的分析处理方法。
    以汉诺塔为例,原问题是将n个圆盘从x塔座移动到z塔座,可以分解成三个子问题:
    1、将编号为1n-1n-1个圆盘从x塔座移动到y塔座
    2、将编号为n的圆盘从x塔座移动到z塔座
    3、将编号为1n-1n-1个圆盘从y塔座移动到z塔座
    子问题13与原问题具有相同属性,由此实现了递归。

  • 代码实例1 :梵塔的递归函数

// 将 n 个盘从 x轴 搬到 z轴,y轴 用作过渡。
void hanoi(int n, char x, char y, char z)
{
	if(n == 1)
	{
		move(x, 1, z);
	}
	else
	{
		hanoi(n-1, x, z, y);
		move(x, n, z);
		hanoi(n-1, y, x, z);
	}
}
  • 代码示例2 :二叉树的先序遍历
void PreOrderTraverse(BiTree T, void (Visit)(BiTree P)) // T总有空的时候
{
	if(T)
	{
		Visit(T->data);
		PreOrderTraverse(T->lchild, Visit);
		PreOrderTraverse(T->rchild, Visit);
	}
}
  • 删除一个单链表中值为 x 的元素
    1、递推法(略)
    2、递归法
void delete(LinkList &L, ElemType x) // 注意第一个引用参数
{
	// L 为无头结点的单链表的指针
	if(L)
	{
		if(L->data == x)
		{
			p = L;
			L = L->next;
			free(p);
			delete(L, x);
		}
		else
		{
			delete(L->next, x);
		}
	}
}

递归函数的几个特性

  • 1、递归函数可读性较好,另外不要强行追求的递归,在利用分割求解设计算法时,子问题和原问题的性质相同,自然导致递归求解。
  • 2、实现递归函数,必须利用栈:一个递归函数必定能改写成为利用栈实现的非递归函数,反之一个用栈实现的非递归函数可以改写为递归函数。递归层次的深度决定所学存储量的大小。
  • 3、分析递归算法的工具是递归树,从递归树上可以得到递归函数的各种信息,例如递归树的深度即为递归函数的递归深度,递归树上的结点数目洽为函数中的主要操作重复进行的次数。若递归树是单支树或者递归树中含有很多相同的结点,则表明该递归函数不适用。
// 将 n 个盘从 x轴 搬到 z轴,y轴 用作过渡。
void hanoi(int n, char x, char y, char z)
{
	if(n == 1)
	{
		move(x, 1, z);
	}
	else
	{
		hanoi(n-1, x, z, y);
		move(x, n, z);
		hanoi(n-1, y, x, z);
	}
}

在这里插入图片描述
如下就是上述代码的递归树,递归执行的过程 就是 中序遍历 的过程,递归树的结点数目是 7,所以移动盘共 7 次,;递归深度为 3。在这里插入图片描述
若递归树蜕化为单支树或者递归树中含有很多相同的结点,则说明递归函数不适用:
如 n! = n * (n-1)!
在这里插入图片描述
单支树的递归深度很深,所需要的存储空间蛮大的。
再如斐波那契数列,递归树如下:
在这里插入图片描述
可以看到图中有很多重复的结点,使用递推更合适。
递归是从上到下,递推是从下到上。

  • 4、递归函数中的尾递归
    传统的递归 中,典型的模型是** 首先执行递归调用,然后获取递归调用的返回值并计算结果。以这种方式,在每次递归调用返回之前,您不会得到计算结果。传统的递归过程 就是 函数调用 ,涉及 返回地址函数参数寄存器值 等压栈(在x86-64上通常用寄存器保存函数参数)
    尾递归:若函数在尾位置调用自身(或是一个尾调用本身的其他函数等等),则称这种情况为 尾递归。当 编译器 检测到 一个函数调用尾递归 的时候,它就 覆盖当前的活动记录 而不是 在栈中去创建一个新的。编译器可以做到这点,因为 递归调用当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过 覆盖当前的栈帧 而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的 运行效率 会变得更高。
    传统模式的编译器 对于 尾调用 的处理方式就像处理 其他普通函数调用 一样,总会在调用时创建一个新的栈帧(stack frame)并将其推入调用栈顶部,用于表示该次函数调用。
    当一个 函数调用 发生时,电脑必须 “记住” 调用函数的位置 —— 返回位置,才可以在调用结束时带着返回值回到该位置,返回位置 一般存在 调用栈 上。在尾调用这种特殊情形中,电脑理论上可以不需要记住尾调用的位置而从 被调用的函数 直接带着返回值返回 调用函数的返回位置(相当于直接连续返回两次)。尾调用消除即是在不改变当前调用栈(也不添加新的返回位置)的情况下跳到新函数的一种优化(完全不改变调用栈是不可能的,还是需要校正调用栈上形式参数与局部变量的信息。)
    由于当前函数帧上包含 局部变量等等大部分的东西 都不需要了,当前的函数帧 经过适当的更动以后可以直接当作 被尾调用的函数的帧 使用,然后程序即可以跳到 被尾调用的函数。产生这种函数帧变动代码与 “jump”(而不是一般常规函数调用的代码)的过程称作尾调用消除 或 尾调用优化尾调用优化位于尾位置的函数调用goto 语句 性能一样高,也因此使得高效的结构编程成为现实。
    对于 C++ 等语言来说,在函数最后 return g(x);
    不一定是尾递归**——在返回之前很可能涉及到 对象的析构函数,使得 g(x) 不是最后执行的那个。这可以通过返回值优化来解决。
    如先序遍历二叉树:
void PreOrderTraverse(BiTree T)
{
	while(T)
	{
		Visit(T->data);
		PreOrderTraverse(T->lchild);
		PreOrderTraverse(T->rchild);
	}
}

上述代码是个 尾递归,可以改写消除尾递归

void PreOrderTraverse(BiTree T)
{
	while(T)
	{
		Visit(T->data);//访问根结点
		PreOrderTraverse(T->lchild);//访问左子树
		T = T->rchild;
	}
}
  • 5、可以用 递归方程 来表述 递归函数的时间性能。例如:
    例如:假设解 n 个圆盘的梵塔的执行时间为 T(n)
    则递归方程:T(n) = 2*T(n-1) + C
    初始条件为:T(0) = 0
void hanoi(int n, char x, char y, char z)
{
	if(n == 1)
	{
		move(x, 1, z);
	}
	else
	{
		hanoi(n-1, x, z, y);
		move(x, n, z);
		hanoi(n-1, y, x, z);
	}
}
  • 广义表从结构上可以分解成
    1、表头 + 表尾
    2、子表1 + 子表2 + … + 子表n

求广义表的深度

  • 广义表的深度:广义表中括号的重数。
    广义表深度 = Max {子表的深度} + 1
    原子深度是0,空表深度是1
  • 求法:LS = ( a1, a2, … , an )。
    ai 可以是原子,也可以是LS的子表。
    LS深度可以分解为n个子问题,每个子问题求 ai 的深度。若 ai 是原子,则深度为0;若 ai 为子表(广义表),那么就和上述一样处理。
    空表也是广义表,深度为1。
  • 求深度的递归定义
    LS = ( a1, a2, … , an )
    在这里插入图片描述
  • 代码实现
    采用链表法存储结构,如下:
typedef enum
{
	ATOM,	// 0,表示原子
	LIST	// 1,表示列表
} ElemTag;

typedef struct GLNode
{
	ElemType tag;	// 公共部分,用于区分原子结点和表结点
	union
	{
		AtomType atom;		// 原子结点的值域
		struct GLNode *hp;	// 表结点的表头指针
	};
	struct GLNode *tp;		//相当于与线性链表的next,指向下一个结点
} *GList;

int GListDepth(GList L)
{
	//广义表采用的是头尾链表结构
	if(!L)
	{
		// 空表深度为1
		return 1;
	}
	if(L->tag == ATOM)
	{
		// 原子深度为 0
		return 0;
	}
	for(max=0,pp=L; pp; pp=pp->ptr.tp)
	{
		dep = GListDepth(pp->ptr.hp);
		if(dep > max)
		{
			max = dep;
		}
	}
	// 非空表的深度是个元素深度最大值再加上1
	return max+1;
}

在这里插入图片描述

复制广义表

  • 任何一个非空的广义表均可以分解为表头和表尾
    因此广义表的复制可以分解为 表头的复制表尾的复制
    只要建立与原表中每一个结点一一都对应的新结点,即可。

  • 复制广义表的递归定义
    LS 是原表,NEWLS 是复制表。

基本项1建立空表结点
基本项2建立原子结点
归纳项建立表中表结点
  • 代码实现
    采用头尾链表结构
typedef enum 
{
	ATOM,	// 0,表示原子
	LIST	// 1,表示子表
}ElemTag;

typedef struct GLNode
{
	ElemTag tag;	//公共部分,用于区分原子节点和表结点
	union			//根据 tag 二选一
	{				//要么是原子节点的值域,要么是表结点的头尾节点指针域
		AtomType atom;
		struct { struct GLNode *hp, *tp } ptr;
	};
} *GList			//广义表类型
	
Status CopyGList(GList T, GList L)
{
	//采用头尾存储结构,表 = 表头 + 表尾
	// L ---> T
	if(!L)
	{
		// 基本项1:复制空表
		T = NULL;
	}
	else
	{
		// 只要是非空表,必然存在表结点占用空间
		if(!(T = (GList)malloc(sizeof(GLNode))))
		{
			exit(OVERFLOW);
		}
		T->tag = L->tag;
		if(L->tag == ATOM)
		{
			// 基本项2:建立原子结点
			T->atom = L->atom;
		}
		else
		{
			// 递归项
			CopyGlist(L->ptr.hp, T->ptr.hp);
			CopyGlist(L->ptr.tp, T->ptr.tp);
		}
	}
	return OK;
}
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值