快捷搜索:

数据结构学习(C++)之二叉树

由于现实天下中存在这“树”这种布局——族谱、等级轨制、目录分类等等,而为了钻研这类问题,必须能够将树储存,而若何储存将取决于所必要的操作。这里有个问题,是否容许存在空树。有些书觉得树都长短空的,由于树表示的是一种现实布局,而0不是自然数;我用过的教科书都是说可以有空树,当然是为了和二叉树统一。这个没有什么原则上的区别,反正便是一种习气。

二叉树

二叉树可以说是人们假想的一个模型,是以,容许有空的二叉树是无争议的。二叉树是有序的,左边有一个孩子和右边有一个的二叉树是不合的两棵树。做这个规定,是由于人们付与了左孩子和右孩子不合的意义,在二叉树的各类利用中,你将会清楚的看到。下面只解说链式布局。看各类讲数据布局的书,你会发明一个有趣的征象:在二叉树这里,基础操作有谋略树高、各类遍历,便是没有插入、删除——那树是怎么建立起来的?着实这很好理解,对付非线性的树布局,插入删除操作不在必然的轨则规定下,是毫无意义的。是以,只有在详细的利用中,才会有插入删除操作。

节点布局

数据域、左指针、右指针肯定是必须的。除非很少用到节点的双亲,或者是资本首要,建议附加一个双亲指针,这将会给很多算法带来方便,尤其是在这个“空间换光阴”的期间。

template

struct BTNode

{

BTNode(T data = T(), BTNode* left = NULL, BTNode* right = NULL, BTNode* parent = NULL)

: data(data), left(left), right(right), parent(parent) {}

BTNode *left, *right, *parent;

T data;

};

基础的二叉树类

template

class BTree

{

public:

BTree(BTNode *root = NULL) : root(root) {}

~BTree() { MakeEmpty(); }

void MakeEmpty() { destroy(root); root = NULL; }

protected:

BTNode *root;

private:

void destroy(BTNode* p)

{

if (p)

{

destroy(p->left);

destroy(p->right);

delete p;

}

}

}

二叉树的遍历

基础上有4种遍历措施,先、中、后根,逐层。当初我对这个很迷惑,搞这么多干什么?到了后面才明白,这是不合的利用必要的。例如,判断两个二叉树是否相等,只要子树根节点不合,那么就不等,显然这时要用先序遍历;而删除二叉树,必须先删除阁下子树,然后才能删除根节点,这时就要用后序遍历。

实际上,搞这么多遍历措施,根滥觞基本因是在内存中储存的树长短线性布局。对付用数组储存的二叉树,这些名目繁多的措施都是没有需要的。使用C++的封装和重载特点,这些遍历措施能很清晰的表达。

1. 前序遍历

public:

void PreOrder(void (*visit)(T &data) = print) { PreOrder(root, visit); }

private:

void PreOrder(BTNode* p, void (*visit)(T &data))

{

if (p){ visit(p->data); PreOrder(p->left, visit); PreOrder(p->right, visit); }

}

2. 中序遍历

public:

void InOrder(void (*visit)(T &data) = print) { InOrder(root, visit); }

private:

void InOrder(BTNode* p, void (*visit)(T &data))

{

if (p){ InOrder(p->left, visit); visit(p->data); InOrder(p->right, visit); }

}

3. 后序遍历

public:

void first() { current = root; while (current->left) current = current->left; }

线索化二叉树

这是数据布局课程里第一个碰着的难点,不知道你是不是这样看,反正我当初是费了不少脑细胞——当然,恼人的矩阵压缩和相关的加法乘法运算不在斟酌之列。我费了不少脑细胞是由于思虑:他们干什么呢?很欣喜的看到在这本黄皮书上,这章被打了*号,虽然我不确定作者是不是跟我一个设法主见——线索化二叉树在现在的PC上是毫无用场的!——不知我做了这个结论是不是会被人骂逝世。

为了证实这个结论,我们来看看线索化二叉树提出的启事:第一,我们想用对照少的光阴,探求二叉树某一个遍历线性序列的先驱或者后继。当然,这样的操作很频繁的时刻,做这方面的改良才是故意义的。第二,二叉树的叶子节点还有两个指针域没有用,可以节省内存。说真的,提出线索化二叉树这样的构思真的很精美,完全做到了“废料使用”——这小我真应该投身环保奇迹。但在谋略机这个古板的器械身上,人们的精美构思每每都是不能实现的——为了速率,谋略机的各个部件都是划一整洁的,而构思的精美每每都是建立在组成的繁杂上的。

我们来看看线索化二叉树究竟能不能达到上面的两个目标。

求遍历后的线性序列的先驱和后继。前序线索化能依次找到后继,然则先驱必要求双亲;中序线索化先驱和后继都不必要求双亲,然则都不很直接;后序线索化能依次找到先驱,然则后继必要求双亲。可以看出,线索化成中序是最佳的选择,基础上算是达到了要求。

节省内存。添加了两个标志位,问题是这两个位怎么储存?纵然是在支持位存储的CPU上,也是不能拿位存储器来存的,第一是由于布局体成员的地址是在一路的,第二是位存储器的数目是有限的。是以,起码必要1个字节来储存这两个标志位。而为了速率和移植,一样平常来说,内存是要对齐的,实际上根本就没节省内存!然而,当这个空间用来储存双亲指针时,带来的方便绝对不是线索化所能相比的,前面已经给出了无栈的非递归遍历。并且,在线索化二叉树上插入删除操作附加的价值太大年夜。

对付输出款式,留意的是到了第1、2、4、8号节点要换行,并且在同一行中,第一个节点的域宽是后序节点的一半。上面的函数在树的层次少于即是5(height#include "BaseTree.h"

template

class BSTree : public BTree

{

public:

BTNode* &find(const T &data)

{

BTNode** p = &root; current = NULL;

while(*p)

{

if ((*p)->data == data) break;

if ((*p)->data right); }

else { current = *p; p = &((*p)->left); }

}

return *p;

}

bool insert(const T &data)

{

BTNode* &p = find(data); if (p) return false;

p = new BTNode(data, NULL, NULL, current); return true;

}

bool remove(const T &data)

{

return remove(find(data));

}

private:

bool remove(BTNode* &p)

{

if (!p) return false; BTNode* t = p;

if (!p->left || !p->right)

{

if (!p->left) p = p->right; else p = p->left;

if (p) p->parent = current;

delete t; return true;

}

t=p->right;while(t->left) t=t->left;p->data=t->data;current=t->parent;

return remove(current->left==t?current->left:current->right);

}

};

以上代码有点费解,有需要阐明一下——非线性链式布局操作的实现都是很让人操心。insert和remove都因此find为根基的,是以必须让find能最大年夜限度的被这两个操作使用。

【注1】这是由于,假如3既有左子树又有右子树,那么2必然在3的左子树上,4必然在3的右子树上;假如2有右子树,那么在2和3之间还应该有一个节点;假如4有左子树,那么3和4之间也应该还有一个节点。

【闲话】上面关于remove操作p->left和p->right都不为空的处置惩罚措施的解说,源于严蔚敏师长教师的课件,看完后我豁然豁达,真不知道为什么她自己那本《数据布局(C说话版)》这里写的那么难解,我是生逝世没看明白。

递归遍历与非递归遍历

在没有树的慨念,对递归的理解老是很艰苦,由于不能提出有说服力的例子,只是阐述了“递归是一种思惟”。但只要能能让你建立“递归是一种思惟”这个不雅念,我的努力就没有白搭。现在,讲完了二叉搜索树,终于有了能阐明问题的例子了。按照前面供给的代码,应该能调试经由过程下面的法度榜样。

#include

using namespace std;

#include

#include

#include "BSTree.h"

#include "Timer.h"

#define random(num) (rand() % (num))

#define randomize() srand((unsigned)time(NULL))

#define NODENUM 200000//node number

int data[NODENUM];

void zero(int &t) { t = 0; }

int main()

{

BSTree a; Timer t; randomize(); int i;

for (i = 0; i data = 0;

cout

以下是timer.h的内容

我们来看看为什么。递归的实现是将参数压栈,然后call自身,着末按层返回,一系列的动作是在客栈上操作的,用的是push、pop、call、ret之类的指令。而用ADT栈来模拟递归调用,实现的照样上述指令的功能,不合的是那些指令对比的ADT实现可就不光是一条指令了。谁都明白模拟的履行效率肯定比真实的差,怎么会在这个问题上就犯糊涂了呢?

当然,你非要在visit函数中加入类似这样的istream file1(“input.txt”),然后在用栈模拟的把这个放在轮回的外貌,着末你说,栈模拟的比递归的快,我也无话可说——曾经就见过一小我,http://www.csdn.net/Develop/Read_Article.asp?Id=18342将数据库连接放在visit函数里面,然后说递归的速率慢。

假如一个递归历程用非递归的措施实现后,速率前进了,那只是由于递归做了一些无用功。比如用轮回消解的尾递归,是多了无用的压栈和出栈才使速率受损的;斐波那契数列谋略的递归改轮回迭代所带来的速率大年夜幅提升,是由于改掉落了重复谋略的搭档。借使一个递归历程必须要用栈才能消解,那么,完全模拟后的结果根本就不会对速率有任何提升,只会减慢;假如你改完后速率提升了,那只证实你的递归函数写的有问题,例如多了许多重复操作——打开关闭文件、连接断开数据库,而这些完全可以放到递归外貌。递归措施本身是简洁高效的,只是应用的人不留意应用措施。

这么看来,钻研递归的栈消解似乎是无用的,着实不然,用栈模拟递了债是有点意义的,只是并不大年夜,下面将给出示例来阐明。

栈模拟递归的好处是节省了客栈

将上面的法度榜样//node number那行的数值改为15000——不改没反映了别找我,将//random swap那行注释掉落,运行debug版,耐心的等30秒,就会抛非常了,着末的输出结果是这样的:

您可能还会对下面的文章感兴趣: