上次我们一起看了稀疏矩阵的压缩存储以及基于三元组表存储结构的矩阵的一些基本操作的实现。这次我们一起来看看树这一章节最重要的数据结构——二叉树的二叉链表存储结构的实现以及基于二叉链表的二叉树的四种遍历操作的实现。
还是老规矩:
程序在码云上可以下载。
地址:https://git.oschina.net/601345138/DataStructureCLanguage.git
一起来看看二叉树的ADT定义,了解一下二叉树的基本操作有哪些:
ADT BinaryTree{
数据对象D:D是具有相同特性的数据元素的集合。
数据关系R:
若D=Φ,则R=Φ,称BinaryTree为空二叉树;
若D≠Φ,则R={H},H是如下二元关系;
(1)在D中存在惟一的称为根的数据元素root,它在关系H下无前驱;
(2)若D-{root}≠Φ,则存在D-{root}={D1,Dr},且D1∩Dr =Φ;
(3)若D1≠Φ,则D1中存在惟一的元素x1,<root,x1>∈H,且存在D1上的关系H1含于H;
若Dr≠Φ,则Dr中存在惟一的元素xr,<root,xr>∈H,且存在Dr上的关系Hr含于H;
H={<root,x1>,<root,xr>,H1,Hr};
(4)(D1,{H1})是一棵符合本定义的二叉树,称为根的左子树;
(Dr,{Hr})是一棵符合本定义的二叉树,称为根的右子树。
基本操作:
InitBiTree( &T )
操作结果:构造空二叉树T。
DestroyBiTree( &T )
初始条件:二叉树T已存在。
操作结果:销毁二叉树T。
CreateBiTree( &T, definition )
初始条件:definition给出二叉树T的定义。
操作结果:按definiton构造二叉树T。
ClearBiTree( &T )
初始条件:二叉树T存在。
操作结果:将二叉树T清为空树。
BiTreeEmpty( T )
初始条件:二叉树T存在。
操作结果:若T为空二叉树,则返回TRUE,否则返回FALSE。
BiTreeDepth( T )
初始条件:二叉树T存在。
操作结果:返回T的深度。
Root( T )
初始条件:二叉树T存在。
操作结果:返回T的根。
Value( T, e )
初始条件:二叉树T存在,e是T中某个结点。
操作结果:返回e的值。
Assign( T, &e, value )
初始条件:二叉树T存在,e是T中某个结点。
操作结果:结点e赋值为value。
Parent( T, e )
初始条件:二叉树T存在,e是T中某个结点。
操作结果:若e是T的非根结点,则返回它的双亲,否则返回“空”。
LeftChild( T, e )
初始条件:二叉树T存在,e是T中某个结点。
操作结果:返回e的左孩子。若e无左孩子,则返回“空”。
RightChild( T, e )
初始条件:二叉树T存在,e是T中某个结点。
操作结果:返回e的右孩子。若e无右孩子,则返回“空”。
LeftSibling( T, e )
初始条件:二叉树T存在,e是T中某个结点。
操作结果:返回e的左兄弟。若e是T的左孩子或无左兄弟,则返回“空”。
RightSibling( T, e )
初始条件:二叉树T存在,e是T中某个结点。
操作结果:返回e的右兄弟。若e是T的右孩子或无右兄弟,则返回“空”。
InsertChild( T, p, LR, c )
初始条件:二叉树T存在,p指向T中某个结点,LR为0或1,非空二叉树c与T不相交且右子树为空。
操作结果:根据LR为0或1,插入c为T中p所指结点的左或右子树。
p所指结点的原有左或右子树则成为c的右子树。
DeleteChild( T, p, LR )
初始条件:二叉树T存在,p指向T中某个结点,LR为0或1。
操作结果:根据LR为0或1,删除T中p所指结点的左或右子树。
PreOrderTraverse( T, visit() )
初始条件:二叉树T存在,Visit是对结点操作的应用函数。
操作结果:先序遍历T,对每个结点调用函数Visit一次且仅一次。
一旦visit()失败,则操作失败。
InOrderTraverse( T, visit() )
初始条件:二叉树T存在,Visit是对结点操作的应用函数。
操作结果:中序遍历T,对每个结点调用函数Visit一次且仅一次。
一旦visit()失败,则操作失败。
PostOrderTraverse( T, visit() )
初始条件:二叉树T存在,Visit是对结点操作的应用函数。
操作结果:后序遍历T,对每个结点调用函数Visit一次且仅一次。
一旦visit()失败,则操作失败。
LevelOrderTraverse( T, visit() )
初始条件:二叉树T存在,Visit是对结点操作的应用函数。
操作结果:层次遍历T,对每个结点调用函数Visit一次且仅一次。
一旦visit()失败,则操作失败。
}ADT BinaryTree
在了解了二叉树可以进行的基本操作后,我们需要再复习一下递归的概念(不需要的话自行跳过本部分,直接看代码)。严蔚敏的书在P56讲到了关于递归和栈的关系。但是书中没有提到递归的基本概念以及递归算法和非递归算法的转换方法。
通俗的说递归就是函数自己调用自己。递归的典型例子就是汉诺塔问题和斐波那契数列。各种算法书都有介绍,严蔚敏书上也讲到了汉诺塔问题,在书上P57页。
递归算法程序的优点:结构清晰、可读性强、便于理解、容易证明等。
缺点:函数的调用需要消耗时间和空间,如果调用次数多,这种成本会很高,所以递归算法的执行效率差。
设计递归算法的思路:
①将复杂问题分割成若干个解法相似的子问题;
②子问题的解组合后可以得出原问题的解。
③递归程序必须要有个终止条件,要不然程序执行没完没了。
严蔚敏书上P56页描述了递归调用期间系统的栈究竟发生了什么变化。
为什么需要把递归算法转换成非递归算法:
(1)递归算法执行效率差,时间和空间成本高;
(2)某些程序设计语言不支持递归。
算法书中经常提到分治法,它的思想是将问题划分为若干子问题,分别求解子问题,然后对子问题的解组合得出原问题的解。
递归问题求解:(自顶向下求解)。把待求解问题分划成若干子问题,问题的求解从原问题开始。程序执行过程中存在许多重复的问题求解过程。
递推问题求解:(自底向上求解)。同样采用划分的策略,但是先求规模最小的子问题的解,然后规模由小到大,直到最后产生原问题的解。
递推相对于递归的优势在于它不仅能够减少函数调用的开销,还能减少重复问题的求解。
递归算法如何转换成非递归算法:
有的递归问题是可以直接求解,不需要回溯的,在转换为非递归算法时可以采用递推方法直接求解,关键是要找到递推公式,也就是找出规律。比如斐波那契数列问题就属于此类问题,因为它有明显的递推公式。
但是有的递归问题是不能直接求解的,因为求解规程中包含了试探和回溯的过程,无法直接根据子问题的解通过递推公式得到原问题的解。此类问题在转换为非递归算法时必须使用栈来记忆回溯点。我们将要看的二叉树先序、中序、后序的非递归算法都包含了回溯的过程,需要借助栈来记忆回溯点。
在复习完递归和非递归算法的相关知识后,如果你对栈和队列的操作不熟悉,请先看看栈和队列的基本操作及其实现,之前的文章我们也写过栈和队列的实现。我们将会使用自己写的顺序栈和链队列作为栈和队列的实现辅助完成二叉树的遍历算法。
数据结构编程笔记八:第三章 栈和队列 顺序栈和进位制程序的实现
http://blog.csdn.net/u014576141/article/details/77368176
数据结构编程笔记九:第三章 栈和队列 链队列的实现
http://blog.csdn.net/u014576141/article/details/77418397
在叙述完这些内容之后,就可以开始看代码了。
此次代码由三个源文件构成:
- Stack.cpp 顺序栈的数据结构定义及其实现
- Queue.cpp 链队列的数据结构定义及其实现
- 二叉树引用版.cpp 二叉树的基本操作以及遍历算法的实现,引入了栈和队列两个源文件作为栈和队列的实现。
这三个源文件必须放在同一目录中编译。
这篇文章的重点是二叉树,不是栈和队列的实现。如果你已经看过第八和第九篇笔记或者已经了解顺序栈和链队列的实现方式,就不用看栈和队列的源文件了,我就是用的这两篇文章的源码直接复制粘贴的,代码一行没改,只删除了部分用不到的函数和注释。
为了源代码的完整和清楚,我把栈和队列的源码附在文章后面,对栈和队列实现有疑问的童鞋就去文章的总结部分后面看看。
好了,在这些影响代码实现的问题交代清楚之后,接下来该看看二叉树的相关操作以及遍历算法的实现了。
//>>>>>>>>>>>>>>>>>>>>>>>>>>>引入头文件<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
#include <stdio.h> //使用了标准库函数
#include <stdlib.h> //使用了动态内存分配函数
//>>>>>>>>>>>>>>>>>>>>>>>>>自定义符号常量<<<<<<<<<<<<<<<<<<<<<<<<<<<<
#define STACK_INIT_SIZE 50 //顺序栈存储空间初始分配量
#define STACKINCREMENT 10 //顺序栈存储空间分配增量
#define OVERFLOW -2 //内存溢出错误常量
#define OK 1 //表示操作正确的常量
#define ERROR 0 //表示操作错误的常量
#define TRUE 1 //表示逻辑真的常量
#define FALSE 0 //表示逻辑假的常量
//>>>>>>>>>>>>>>>>>>>>>>>>>自定义数据类型<<<<<<<<<<<<<<<<<<<<<<<<<<<<
typedef int Status; //状态码为int类型,用于保存操作结果(1成功0失败)
typedef char TElemType; //二叉树节点数据域的元素类型
//----------------二叉树的二叉链表存储表示--------------------
typedef struct BiNode{
TElemType data;
struct BiNode *lchild,*rchild; //孩子结点指针
}BiNode,*BiTree;
//--------引入栈和队列的实现(实际上应该放在头部,由于编译原因,只好这样了)----------------
#include "Queue.cpp" //引入队列的实现
#include "Stack.cpp" //引入栈的实现
//---------------------二叉树的主要操作--------------------------
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>1.构造二叉树<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
/*
函数:CreateBiTree
参数:BiTree &T 二叉树引用
返回值:状态码,操作成功返回OK,否则返回ERROR
作用:按先序次序输入二叉树中结点的值(一个字符),空格字符表示空树,
递归的构造二叉链表表示二叉树T
*/
Status CreateBiTree(BiTree &T){ //按先序序列构造二叉树
//ch存储从键盘接收的字符
char ch;