您的位置:首页 > 理论基础 > 数据结构算法

数据结构之二叉树的非递归实现及“狡猾”的指针

2012-06-30 17:28 288 查看
先说说我实现二叉树的方法,才能开始讲述我被指针搞得晕头晕脑的痛苦经历。

/****************二叉树非递归的总体分析*****************/

(1)建立二叉树

栈的应用:用空格键表示空指针,我们利用栈来保存左右孩子指针未都被赋值的节点,空指针不允许入栈

开始建立:以循环遍历用户输入的字符串来建立二叉树,如:"AB#D##CE###"(这里先用#代替空格,方便查看)

我们以先序建立二叉树,按上面例子中的字符串建立的二叉树如下:



(visio不怎么会用,这图看着。。。忍耐一下)

从图中可以清晰的发现一个规律

字符串中,任意一个字符的前一个(即i-1)字符若不为空格的话,这个字符一定是前一个非空格字符的左子树

在由先序这个条件还可以发现,我们始终是从左向右建立子树的,所以,如果第i-1个字符为空格,第i个字符就

一定是前一个刚建立的子树的右子树

(2)遍历二叉树

栈的应用:我们从二叉树顶端开始走的时候,将未遍历的节点的数据入栈,遍历一个节点从栈中弹一个出来

中序遍历:我们采用中序遍历二叉树,在非递归算法中,根据中序的定义,我们也需要让第一个取得的头节点A

从右开始向下走,一直往右边走(途中遇到的节点依次入栈),直到走到第一个左子树为空的节点,这里显然是B,

然后访问B,然后B退栈,若B右指针非空,则遍历往右走一步(为空的时候下面讨论),然后B的右子树入栈,

继续循环以B的右子树为头节点走到最左端,直到把节点都走完,这里走到D 的时候就已经结束了,直接访问D,

D退栈,根据栈顶元素A,访问A,A退栈,这是往右走,C入栈,走到最左端E,E入栈,访问E,E退栈,这时和D

的情况一样了,已经是一个叶子节点了,就根据栈顶C,访问C,C退栈,栈空,表明遍历结束

特殊情况:当我们遍历完最左端的节点之后,会向右走一步,如果此节点的右子树本为空,就不能往右走了,依据

中序的定义,如果已经访问到了右子树并且为空,说明这个双亲节点已经被访问过了,那么我们就要去访问栈顶数据,

弹出栈顶元素,如果新的栈顶元素的右子树依旧为空并且此时栈还非空,就继续这种循环,直到找到一个右子树为被

访问过的栈顶元素,或者栈以为空(这种情况表明遍历结束)

(3)销毁二叉树

栈的应用:从头节点依次入栈,从栈顶开始销毁节点

后序销毁:需要从叶子节点开始销毁,并且要修改其双亲节点的指针值。首先,依旧要走到最左端B,依次入栈A、B,

发现B右子树非空,往右走一步,D入栈,这个时候,以D为头节点走到D的最左端行不通了,因为D的左子树为空,就看

能不能往D的右边走一步,发现D的右子树也为空,就说明D为叶子节点,弹出它,并将新的栈顶元素的右指针设为空,

freeD,(这里有个疑问:怎么确定当前的节点是双亲节点的左孩子还是右孩子呢?根据后序的定义,如果一个节点的右

指针为空的话,就说明这个节点为叶子节点,如果它的双亲节点左孩子还不为空,就说明这个节点对应的是左指针,如果

双亲节点的左孩子已经为空,说明它本身就是空或者已经被释放了,则这个节点就一定是右指针。)释放了D之后,根据

栈顶指示,销毁B,再跳到C,跳到E,释放E、C、A,最后栈空,表明销毁结束。

/****************指针的误用*****************/

从上面的设计过程来看,建立和遍历中对栈的使用是大不同的。

遍历二叉树的时候,我们只需要访问节点的数据即可,但在建立二叉树的时候,我们需要节点的准确地址来连接这个树。

一开始我并没有注意到这个巨大的区别,依然用遍历时采用的栈的使用来建立二叉树,结果可想而知,“灵活”的指针肯定

已经失控了。

栈的指针误区:

栈里面是申请的一块连续的动态内存空间,每一次压入或弹出的元素其实都不是你真正想用的数据,因为数据的位置变了。

栈中存放的每一个数据都是放到了你申请的这块空间上,但你实际的数据是放到另一块内存空间上的。

一开始我就直接从栈中弹出节点,将新建的节点连到里面,额。。。情况具体是这样的(就以前两个节点为例吧):

实际的内存地址

栈中的内存地址

A

0x004758h6

0x00853542

B(A的左子树)

0x004758f0

0x0085354c

很显然,B实际应该连到A的左子树上,但是,如果先把A入栈,根据栈顶指示发现应该将B连到栈顶元素(A)的左子树上,

于是便直接获取栈顶地址0x00853542,将0x004758f0处的内容连上,然后B再入栈,遇到B的左子树应该为空的时候,获取

栈顶元素(B)的地址0x0085354c,将0x00000000给连上,没有调试程序的话,是很难发现这里的所谓“连上”其实根本就是

断的。因为我们用的是正确的子树地址,但是却把它连到了栈里面去了,相当于从栈那棵“笔直的大树”里长了许多叶子出来。。。

而原本的“根”却只是孤零零的一片小叶子而已。

以前总听牛人说指针经常是C的双刃剑,如果没有足够的功力是无法驾驭它的,现在也是深有体会了,因为这个错误我调试

了不下十次才发现。

指针误区的解决:

为了解决指针指向错误的问题,我想了很多方法,甚至还想将建立二叉树的算法来个大转变,我试过这种方法:

将入栈、出栈的语句改为:

elem = --StackPtr->TopPtr;
这是出栈语句,两边都是指针,入栈的时候也采用地址赋值,本以为这样就可以轻易的改变栈中元素的指向的,整个树就可以

接到一块儿了,结果,随之而来又是另一个指针使用的误区:

地址赋值会将指针的地址修改,也就是将连续的栈空间的每一个元素的地址改为了树节点的地址了,这样栈的结构又被破坏了。。。

如果是*elem = *--StackPtr->TopPtr;就只是改变栈中元素的指向内容而已,也就是最开始存放节点的数据内容的说法

痛苦了好久。。。。。不过还好找到了一个解决办法:

就是将自身节点的实际内存地址保存起来,保存在哪里可以自己定,因为只要能在连接子树的时候连到正确的地址就可以解决

问题,我是把它保存到自身结构体里面,如下:

typedef struct tree
{
ELEMTYPETREE data;
struct tree *This;
struct tree *RightChildPtr;
struct tree *LeftChildPtr;
}ThreadBinaryTree;


This指针就指向其自身,由于它是放到结构体里面的,在入栈、出栈的时候是不会被改变的,这样我们就可以使用类似这样

的方法来连接节点了:

(TopPtr->This)->LeftChildPtr = LeavePtr;
同时我们也不必关心TopPtr所指向的的数据内容了,只管对TopPtr的元素this进行修改,因为那才是节点的真身。

(这个方法不知道会不会引起节点数据过大的后果,请大牛们指出此方法是否恰当)

/****************内存管理*****************/

接下来就是C中的内存管理了,我们需要把在各个函数中申请的动态内存释放了,不然会造成严重的内存泄漏。

(1)对栈的释放:

栈的释放比较简单,用栈顶指针从上到小依次指向要被释放的空间即可

(2)二叉树的内存申请与释放:

建立二叉树的函数最终原型:(传入的Root为RootPtr,RootPtr类型为ThreadBinaryTree *)

int DIY_ThreadBinaryTree(ThreadBinaryTree **Root,char *str)


最初的RootPtr使用的类型是ThreadBinaryTree *。

我们的先序建立二叉树中动态申请了每个节点所需的内存,并且已经按要求连接到了一块儿,现在他们是一个整体,领头的

就是头节点A,我们需要将A节点的地址传出去,大家都知道子函数里面定义的内容是会随着子函数的结束而释放的,如果我们

只是简单的想把RootPtr这个指针传入二叉树建立的函数的话,当函数结束时,我们真正的头节点确实没有被释放,因为它需要

我们认为释放,但是保存它地址的RootPtr是不会正确保存它的地址的,因为函数的参数传递终究是相当于赋值的,也就是函数

的参数Root和我们传入函数的RootPtr不是同一个东西,Root中有我们需要的地址,但是它会随着函数的结束而被销毁,也就是

不会把正确的地址传回给RootPtr。

我们要怎么样才能将地址正确返回?答案就是我们要将参数设为指向指针的指针的,这样,当我们传入一个*RootPtr的时候,我

们就需要用&RootPtr传入,传给函数的参数**Root,解释出来就是**(&RootPtr),这样就相当于*RootPtr,实现了对Root地址的

间接传递,保护了其实际内容。

释放的方法在最开始的算法分析中已经解释过了。

/****************编程总结******************/

在学习C的时候,一定要深入底层,从内存地址着手,不然真的会被指针给耍得团团转。

(二叉树的建立、遍历、销毁的实现代码放代码分享里了)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐