- 浏览: 201594 次
- 性别:
- 来自: 重庆
最新评论
-
Share_word:
SNMP -
zolo1226:
第一题解答有问题,式子没看出有什么意义
算法导论上几个简单的习题 -
tmj_159:
看这个跟看乱码没有区别,眼睛疼.
国际C语言混乱代码大赛(IOCCC) -
ibio:
呵呵。强悍,顶!~
求解一个简单的逻辑题 -
breakhearts:
你的第一题和最后一题都有问题,第一题random(0,1)不是 ...
算法导论上几个简单的习题
1.先序遍历
从递归说起
- void preOrder(TNode* root)
- {
- if (root != NULL)
- {
- Visit(root);
- preOrder(root->left);
- preOrder(root->right);
- }
- }
递归算法非常的简单。先访问跟节点,然后访问左节点,再访问右节点。如果不用递归,那该怎么做呢?仔细看一下递归程序,就会发现,其实每次都是走树的左分支(left),直到左子树为空,然后开始从递归的最深处返回,然后开始恢复递归现场,访问右子树。
其实过程很简单:一直往左走 root->left->left->left...->null,由于是先序遍历,因此一遇到节点,便需要立即访问;由于一直走到最左边后,需要逐步返回到父节点访问右节点,因此必须有一个措施能够对节点序列回溯。有两个办法:
1.用栈记忆:在访问途中将依次遇到的节点保存下来。由于节点出现次序与恢复次序是反序的,因此是一个先进后出结构,需要用栈。
使用栈记忆的实现有两个版本。第一个版本是模拟递归的实现效果,跟LX讨论的,第二个版本是直接模拟递归。
2.节点增加指向父节点的指针:通过指向父节点的指针来回溯(后来发现还要需要增加一个访问标志,来指示节点是否已经被访问,不知道可不可以不用标志直接实现回溯?想了一下,如果不用这个标志位,回溯的过程会繁琐很多。暂时没有更好的办法。)
(还有其他办法可以回溯么?)
这3个算法伪代码如下,没有测试过。
先序遍历伪代码:非递归版本,用栈实现,版本1
- // 先序遍历伪代码:非递归版本,用栈实现,版本1
- void preOrder1(TNode* root)
- {
- Stack S;
- while ((root != NULL) || !S.empty())
- {
- if (root != NULL)
- {
- Visit(root);
- S.push(root); // 先序就体现在这里了,先访问,再入栈
- root = root->left; // 依次访问左子树
- }
- else
- {
- root = S.pop(); // 回溯至父亲节点
- root = root->right;
- }
- }
- }
preOrder1每次都将遇到的节点压入栈,当左子树遍历完毕后才从栈中弹出最后一个访问的节点,访问其右子树。在同一层中,不可能同时有两个节点压入栈,因此栈的大小空间为O(h),h为二叉树高度。时间方面,每个节点都被压入栈一次,弹出栈一次,访问一次,复杂度为O(n)
先序遍历伪代码:非递归版本,用栈实现,版本2
- // 先序遍历伪代码:非递归版本,用栈实现,版本2
- void preOrder2(TNode* root)
- {
- if ( root != NULL)
- {
- Stack S;
- S.push(root);
- while (!S.empty())
- {
- TNode* node = S.pop();
- Visit(node); // 先访问根节点,然后根节点就无需入栈了
- S.push(node->right); // 先push的是右节点,再是左节点
- S.push(node->left);
- }
- }
- }
preOrder2每次将节点压入栈,然后弹出,压右子树,再压入左子树,在遍历过程中,遍历序列的右节点依次被存入栈,左节点逐次被访问。同一时刻,栈中元素为m-1个右节点和1个最左节点,最高为h。所以空间也为O(h);每个节点同样被压栈一次,弹栈一次,访问一次,时间复杂度O(n)
先序遍历伪代码:非递归版本,不用栈,增加指向父节点的指针
- // 先序遍历伪代码:非递归版本,不用栈,增加指向父节点的指针
- void preOrder3(TNode* root)
- {
- while ( root != NULL ) // 回溯到根节点时为NULL,退出
- {
- if( !root->bVisited )
- { // 判定是否已被访问
- Visit(root);
- root->bVisited = true;
- }
- if ( root->left != NULL && !root->left->bVisited ) // 访问左子树
- {
- root = root->left;
- }
- else if( root->right != NULL && !root->right->bVisited ) // 访问右子树
- {
- root = root->right;
- }
- else // 回溯
- {
- root = root->parent;
- }
- }
- }
preOrder3的关键在于回溯。为了回溯增加指向父亲节点的指针,以及是否已经访问的标志位,对比preOrder1与preOrder2,但增加的空间复杂度为O(n)。时间方面,每个节点被访问一次。但是,当由叶子节点跳到下一个要访问的节点时,需要先回溯至父亲节点,再判断是否存在没有被访问过的右子树,如果没有,则继续回溯,直至找到一颗没有被访问过的右子树,这个过程需要很多的时间。每个叶子节点的回溯需要O(h)时间复杂度,叶子节点最多为(2^(h-1)),因此回溯花费的上限为O(h*(2^(h-1))。这个上限应该可以缩小。preOrder3唯一的好处是不需要额外的数据结构-栈。
2.中序遍历
根据上面的先序遍历,可以类似的构造出中序遍历的三种方式。仔细想一下,只有第一种方法改过来时最方便的。需要的改动仅仅调换一下节点访问的次序,先序是先访问,再入栈;而中序则是先入栈,弹栈后再访问。伪代码如下。时间复杂度与空间复杂度同先序一致。
- // 中序遍历伪代码:非递归版本,用栈实现,版本1
- void InOrder1(TNode* root)
- {
- Stack S;
- while ( root != NULL || !S.empty() )
- {
- while( root != NULL ) // 左子树入栈
- {
- S.push(root);
- root = root->left;
- }
- if ( !S.empty() )
- {
- root = S.pop();
- Visit(root->data); // 访问根结点
- root = root->right; // 通过下一次循环实现右子树遍历
- }
- }
- }
第二个用栈的版本却并不乐观。preOrder2能够很好的执行的原因是,将左右节点压入栈后,根节点就再也用不着了;而中序和后序却不一样,左右节点入栈后,根节点后面还需要访问。因此三个节点都要入栈,而且入栈的先后顺序必须为:右节点,根节点,左节点。但是,当入栈以后,根节点与其左右子树的节点就分不清楚了。因此必须引入一个标志位,表示 是否已经将该节点的左右子树入栈了。每次入栈时,根节点标志位为true,左右子树标志位为false。
伪代码如下:
- // 中序遍历伪代码:非递归版本,用栈实现,版本2
- void InOrder2(TNode* root)
- {
- Stack S;
- if( root != NULL )
- {
- S.push(root);
- }
- while ( !S.empty() )
- {
- TNode* node = S.pop();
- if ( node->bPushed )
- { // 如果标识位为true,则表示其左右子树都已经入栈,那么现在就需要访问该节点了
- Visit(node);
- }
- else
- { // 左右子树尚未入栈,则依次将 右节点,根节点,左节点 入栈
- if ( node->right != NULL )
- {
- node->right->bPushed = false; // 左右子树均设置为false
- S.push(node->right);
- }
- node->bPushed = true; // 根节点标志位为true
- S.push(node);
- if ( node->left != NULL )
- {
- node->left->bPushed = false;
- S.push(node->left);
- }
- }
- }
- }
对比先序遍历,这个算法需要额外的增加O(n)的标志位空间。另外,栈空间也扩大,因为每次压栈的时候都压入根节点与左右节点,因此栈空间为O(n)。时间复杂度方面,每个节点压栈两次,作为子节点压栈一次,作为根节点压栈一次,弹栈也是两次。因此无论从哪个方面讲,这个方法效率都不及InOrder1。
至于不用栈来实现中序遍历。头晕了,暂时不想了。后面再来完善。还有后序遍历,貌似更复杂。对了,还有个层序遍历。再写一篇吧。头都大了。
9.8续
中序遍历的第三个非递归版本:采用指向父节点的指针回溯。这个与先序遍历是非常类似的,不同之处在于,先序遍历只要一遇到节点,那么没有被访问那么立即访问,访问完毕后尝试向左走,如果左孩子补课访问,则尝试右边走,如果左右皆不可访问,则回溯;中序遍历是先尝试向左走,一直到左边不通后访问当前节点,然后尝试向右走,右边不通,则回溯。(这里不通的意思是:节点不为空,且没有被访问过)
- // 中序遍历伪代码:非递归版本,不用栈,增加指向父节点的指针
- void InOrder3(TNode* root)
- {
- while ( root != NULL ) // 回溯到根节点时为NULL,退出
- {
- while ( root->left != NULL && !root->left->bVisited )
- { // 沿左子树向下搜索当前子树尚未访问的最左节点
- root = root->left;
- }
- if ( !root->bVisited )
- { // 访问尚未访问的最左节点
- Visit(root);
- root->bVisited=true;
- }
- if ( root->right != NULL && !root->right->bVisited )
- { // 遍历当前节点的右子树
- root = root->right;
- }
- else
- { // 回溯至父节点
- root = root->parent;
- }
- }
- }
这个算法时间复杂度与空间复杂度与第3个先序遍历的版本是一样的。
3.后序遍历
从直觉上来说,后序遍历对比中序遍历难度要增大很多。因为中序遍历节点序列有一点的连续性,而后续遍历则感觉有一定的跳跃性。先左,再右,最后才中间节点;访问左子树后,需要跳转到右子树,右子树访问完毕了再回溯至根节点并访问之。这种序列的不连续造成实现前面先序与中序类似的第1个与第3个版本比较困难。但是按照第2个思想,直接来模拟递归还是非常容易的。如下:
- // 后序遍历伪代码:非递归版本,用栈实现
- void PostOrder(TNode* root)
- {
- Stack S;
- if( root != NULL )
- {
- S.push(root);
- }
- while ( !S.empty() )
- {
- TNode* node = S.pop();
- if ( node->bPushed )
- { // 如果标识位为true,则表示其左右子树都已经入栈,那么现在就需要访问该节点了
- Visit(node);
- }
- else
- { // 左右子树尚未入栈,则依次将 右节点,左节点,根节点 入栈
- if ( node->right != NULL )
- {
- node->right->bPushed = false; // 左右子树均设置为false
- S.push(node->right);
- }
- if ( node->left != NULL )
- {
- node->left->bPushed = false;
- S.push(node->left);
- }
- node->bPushed = true; // 根节点标志位为true
- S.push(node);
- }
- }
- }
和中序遍历的第2个版本比较,仅仅只是把左孩子入栈和根节点入栈顺序调换一下;这种差别就跟递归版本的中序与后序一样。
4.层序遍历
这个很简单,就不说老。
- // 层序遍历伪代码:非递归版本,用队列完成
- void LevelOrder(TNode *root)
- {
- Queue Q;
- Q.push(root);
- while (!Q.empty())
- {
- node = Q.front(); // 取出队首值并访问
- Visit(node);
- if (NULL != node->left) // 左孩子入队
- {
- Q.push(node->left);
- }
- if (NULL != node->right) // 右孩子入队
- {
- Q.push(node->right);
- }
- }
- }
用栈来实现比增加指向父节点指针回溯更方便;
采用第一个思想,就是跟踪指针移动 用栈保存中间结果的实现方式,先序与中序难度一致,后序很困难。先序与中序只需要修改一下访问的位置即可。
采用第二个思想,直接用栈来模拟递归,先序非常简单;而中序与后序难度一致。先序简单是因为节点可以直接访问,访问完毕后无需记录。而中序与后序时,节点在弹栈后还不能立即访问,还需要等其他节点访问完毕后才能访问,因此节点需要设置标志位来判定,因此需要额外的O(n)空间。
发表评论
-
一个搜索问题的求解
2008-05-06 15:24 854这是一个模拟竞 ... -
撰写仅有一行语句的函数
2008-06-26 10:30 770函数原形已经给出:int p(int i, int N); 功 ... -
链表的几个思考题
2008-08-25 21:54 2133链表若干 1.怎么判断链 ... -
字符串匹配算法
2008-08-28 16:20 5419字符串匹配定义:文本 ... -
蚂蚁爬橡皮筋问题 之二
2008-09-02 09:57 1189昨天想的是蚂蚁爬行的绝对距离,晚上睡觉的时候突然想到,可以用蚂 ... -
[转载]关于快排的优化
2008-09-02 16:25 1296快速排序算法是一种基于分治技术的重要的排序算法,自从它被发明以 ... -
PV操作
2008-09-07 12:58 2530进程间的制约进程1、进程2共享打印机缓冲区(公有资源),显然它 ... -
算法导论上几个简单的习题
2008-09-09 16:11 31265.1-2 用random(0,1)来实现ra ... -
讨论记录之排序
2008-09-22 15:59 1087Participants:CPP,ZY ,LF,HZP Da ... -
生成函数与z变换?会不会有些相似?
2008-09-30 21:08 1246单边z变换定义为 F(z) = f(0)*z^0+f(1)*z ... -
讨论记录之哈希表与二叉树
2008-10-04 15:07 2144Participants: ZY, LF, HZP, CPP ... -
问题记录:可变缓冲区问题
2008-07-10 17:30 896网络传输存在的一个问 ... -
逻辑题:住楼问题
2008-07-08 15:59 931引用Baker, Cooper, Fletcher, Mill ... -
求解一个简单的逻辑题
2008-07-08 15:18 4414引用逻辑题:请编程实现,时间一小时。 某天!一家珠宝公司被盗! ...
相关推荐
非递归遍历二叉树是一种不依赖递归函数来访问树的所有节点的方法,它通常通过栈或队列等数据结构来实现。下面我们将详细探讨非递归遍历二叉树的先序、中序和后序策略。 先序遍历是二叉树遍历的一种方法,其顺序为:...
### 非递归遍历二叉树:C语言实现详解 #### 一、引言 在计算机科学中,二叉树是一种常见的数据结构,它由一个根节点以及最多两个子树构成,这两个子树各自又可以是二叉树。二叉树的遍历是其操作中最基本且频繁的...
二叉树的非递归遍历,使用C++实现二叉树的非递归遍历,对正在学习算法的同学应该挺有帮助的
非递归遍历二叉树是一种不依赖递归函数来访问树中所有节点的方法,这在处理大型或深度较大的树时特别有用,因为它避免了调用栈溢出的风险。 二叉树的主要遍历方法有三种:前序遍历、中序遍历和后序遍历。在递归方式...
先序非递归遍历二叉树: a b c 中序递归遍历二叉树: b a c 中序非递归遍历二叉树: b a c 后序递归遍历二叉树: b c a 后序非递归遍历二叉树: b c a 二叉树的深度是2 二叉树的结点个数是3 Press any key to ...
用先序的方式创建二叉树,“#”代表空,然后自动输出递归与非递归的先中后三种顺序遍历二叉树
递归遍历二叉树、c语言以及数据结构均要用到,我还会上传非递归的··
二叉树的递归遍历、非递归遍历和层次遍历
中根顺序递归建立二叉树,递归及非递归遍历二叉树。C++面向过程实现
java语言实现的二叉树的各种操作(包括递归与非递归遍历二叉树,求二叉树的高度,节点总数,叶子节点等)
更简单的非递归遍历二叉树的方法 思路非常简洁,多看多思考
非递归遍历二叉树的算法可以分为先序遍历和后序遍历两种。 非递归先序遍历算法 非递归先序遍历算法使用栈来存储节点,并使用while循环来遍历二叉树。算法的步骤如下: 1. 初始化栈为空,并将根节点设置为当前节点...
本实验报告将深入探讨二叉树的递归和非递归遍历方法,并附带源代码实现,旨在帮助读者理解这两种遍历策略及其应用场景。 一、二叉树遍历 二叉树遍历主要有三种方法:前序遍历、中序遍历和后序遍历。它们是遍历...
非递归遍历则需要更复杂的逻辑,但可以避免栈溢出问题,并在某些情况下提供更好的性能。 在`Tree.cpp`文件中,可能包含了上述各种遍历方法的C++实现。通过阅读和理解这些代码,你可以学习如何在实际编程中处理完全...
在IT领域,尤其是在数据结构与算法的学习中,中序遍历二叉树的非递归算法是一个关键且实用的知识点。通常,我们首先通过递归来实现二叉树的遍历,但递归方法可能因深度过大导致栈溢出,因此掌握非递归版本的遍历算法...
二种非递归实现 二叉树中序遍历: 递归实现 非递归实现 二叉树后序遍历: 递归实现 非递归实现 二叉树层次遍历 二叉树层次创建,创建方法遵循卡特兰数 http://write.blog.csdn.net/postedit/17380455
除了这三种基本的递归遍历方法外,还有对应的非递归遍历方法。本文将详细介绍这六种遍历二叉树的方法,并给出相应的代码实现。 #### 1. 先序遍历(递归) **定义**: - 访问根节点; - 递归地先序遍历左子树; - ...
标题"关于二叉树前序和后序的非递归遍历算法"指的是不使用递归函数来实现二叉树的前序和后序遍历。递归方法虽然直观,但在处理大型二叉树时可能会导致栈溢出,因此非递归方法是一个更优的选择。 **前序遍历**是...