数据结构-二叉树(1)


前言

  这篇文章是我学习二叉树的总结。希望本文章能给那么带来一些帮助。有错的地方也请指出,我会更正的。.

二叉树简介

百度百科

我理解的概念

  所谓的二叉树也就是树结构的一种,只是二叉树中的任意一个点最多只能指向两个点,并且这个指向是有方向性的也就是我们所说的“左孩子”和“右孩子”。这个方向性是很重要的,因为你在之后使用二叉树需要区别数的时候就要用到。比如堆排序和编码等。如果你不明白什么是堆排序或者编码也没关系。你只要知道二叉树每个点的“孩子”最多两个且是它是有方向性的。

树的相关术语

树的节点(node):包含一个数据元素及若干指向子树的分支;

孩子节点(child node):节点的子树的根称为该节点的孩子;

双亲节点:B 节点是A 节点的孩子,则A节点是B 节点的双亲;

兄弟节点:同一双亲的孩子节点; 堂兄节点:也就是同一层上的节点但是双亲不同;

祖先节点: 从根到该节点的所经分支上的所有节点

子孙节点:以某节点为根的子树中任一节点都称为该节点的子孙

节点层:根节点的层定义为1;根的孩子为第二层节点,依此类推;

树的深度:树中最大的节点层

节点的度:节点子树的个数

树的度: 树中最大的节点度。

根节点:没有双亲的点;

叶子节点:也叫终端节点,是度为 0 的节点;

分枝节点:度不为0的节点;

层序遍历:一层一层的节点访问树。

二叉树的术语

左子树:一个节点左指针指向节点所构成的树结构;

右子树:一个结右指针指向节点所构成的树结构;

右斜树:一个树上任意的节点只有;

前序遍历:访问二叉树的节点的时候先访问根节点的数据,然后访问左子树最后访问右子树;

中序遍历:访问二叉树的节点的时候先访问左子树,然后访问根节点的数据最后访问右子树;

后序遍历:访问二叉树的节点的时候先访问左子树,然后访问右子树最后访问根节点的数据;

二叉树示例图:

  一开始我们在学习二叉树的时候,会将节点的空节点补齐。如图一的B节点,它没有右孩子但是我们要把他的右孩子补齐填上#。所以#也就表示一个空的节点。

图一:

在这里插入图片描述

图二:

在这里插入图片描述

之后的前序遍历等都以上面的两个图为例子。

二叉树节点的实现

结构体实现

struct Node
{
    int data;//存某个数据当然不一定是int型,也可以不只一个。
    Node* lchild,* rchild;
    //左右孩子,有时候也会有Node *parent用来表示指向这个节点的节点。也就是这个节点的父节点。
};

类实现

class Node
{
private:
    int data;
    Node* lchild,* rchild;
};

此时我们已经有了各个节点的样子,接下来便是创建二叉树。

创建与遍历二叉树

  前面我们已经将二叉树某一个节点的结构实现了。那如何让那些节点连起来并形成一颗树就是我们现在面临的问题了。

  古人云:“无规矩不成方圆。” 当然“无规矩也不成树”。二叉树的建立也依照某种规则来进行。首先我们要设置一个根节点(下面以root表示)。那么以后我们遍历和访问二叉树的数据都由这个root作为媒介进行遍历和访问二叉树数据。如果你先学过链表那root的作用就和链表中的head效果一致。没学过话我这边简单说明一下:root的作用就是唯一标识一个树相对于一个人的身份证。之后我们对树进行操作的时候就以root为标志进行操作。归根结底root依然是一个树的节点。

  当选择好那个节点作为root后, 面对其他的节点我们依然按照某种规则来进行二叉树的创建。我刚刚接触二叉树的时候老师是给定一个二叉树,然后让我用前序遍历的方式把二叉树的数据以字符串的样子表示出来后再进行创建一颗树。

前序遍历/创建二叉树

  我们先按照前序遍历的规则来遍历图一。首先从root进入也就是图一中的A进入然后输出A的值。接下来判断A的左子树发现A有左孩子B那么我们就进入B判断一下B是否有左孩子。我们可以看到D是B的左孩子所以输出并进入D。那么D是没有左孩子的,这时候我们再判断D是否有右孩子发现也没有。则我们返回到D的父节点B去判断B是否有右孩子,图一中B是没有右孩子的。所以我们再次返回到B的父节点A上判断A的右孩子是否存在。存在就进入并且输出值然后像之前一样先左后右。最终我们得到的结果是(#表示补全的空节点)ABD###CEH###F##的字符串。遍历图二得ABD###C#F##的字符串。

  得到这些字符串后我们就可以按照前序遍历的规则来把这些字符串变成树结构。以下是前序遍历的代码和按照前序遍历规则创建树的代码:

int pos;//因为要计算s所处于的位置,我为了方便设计成了全局变量。如果你会引用的话也可以用引用。
Node* preCreate(string s)//按照前序遍历规则创建树
{
	Node* r = new Node();//初始化
	r->lchild = NULL;
	r->rchild = NULL;
	if (s[pos] == '#')
	{
		pos = pos + 1;
		return NULL;//如果碰到#表示这个是非空节点的子空节点,那么就要返回NULL
	}
	r->data = s[pos];
	pos = pos + 1;
	r->lchild = preCreate(s);//这里我用递归进行创建
	r->rchild = preCreate(s);
	return r;//最后要返回一个值让所有我们创建的指针都不是野指针
}
void preOrder(Node* r)//前序遍历
{
	if (r)//判断是否为空
	{
		cout << r->data;//先访问根节点
		preOrder(r->lchild);//再访问左
		preOrder(r->rchild);//访问右
	}
	else
	{
		cout << "#";
	}
}

中序遍历

  还是以图一为例子。我们依然先进入A(也就是root)但是与前序遍历不一样的是我们不先输出A而是先去看看它的左孩子是否存在然后进入。那现在我们就应该进入B,再看B的左孩子是否存在发现是有的那就进入B的左孩子D。然后判断D的左孩子,但是D的左孩子不存在这时候我们才开始输出。此时我们要先输出D,接下来是判断D的右孩子存在吗?D没有右孩子所以返回到D的父节点B,这时候我们已经将B的左子树全部判断完了,所以我们这时候就应该输出B。B不存在右孩子所以返回并输出A。接下来也是一样的。总之中序遍历与前序遍历不一样的就是中序先访问左孩子然后输出数据。前序是线输出数据再访问左孩子。共同点是它们都最后访问右孩子。其实上面的说法并不是很正确但是你可以这么理解。正确的说法(以中序为例):先访问左子树再访问根节点最后访问右子树(上面所谓的根节点是变化的,你每进入一个子树那根节点就值这子树最顶层的节点)。按照中序遍历所得到的图一结果为:#D#B#A#H#E#C#F#。图二为:#D#B#A#C#F#。其实现代码如下:

void midOrder(Node* r)
{
	if (r)
	{
		midOrder(r->lchild);
		cout << r->data;
		midOrder(r->rchild);
	}
	else
		cout << "#" ;
}

后序遍历

  前序是根节点输出在前面,中序是根节点输出在中间,那后序其实也就是根节点输出在后面。那这个前后是以先左后右的访问子树为参照。后序遍历你们可以按照我先前的说法推一边。(不会的话,那我只能说对不起了。我写的文章太垃圾了)图一结果:##D#B##H#E##FCA。图二结果是:##D#B###FCA。代码如下:

void aftOrder(Node* r)
{
	if (r)
	{
		aftOrder(r->lchild);
		aftOrder(r->rchild);
		cout << r->data;
	}
	else
		cout << "#";
}

删除二叉树

  创建的时候我们利用了前序遍历的想法来创建二叉树。然而删除二叉树的时候我们也可以用一种遍历的方式来删除二叉树——后序遍历。所以删除二叉树的代码是:

void deleteT(Node* r)
{
	if (r)//保证不为空,否则delete的时候会报错
	{
		deleteT(r->lchild);
		deleteT(r->rchild);
		delete r;
	}
}

(如果你发现文章哪里写错的话的欢迎指出。对于二叉树点称节点和结点这叫法问题,其实我觉得不用太在意啦。最后你懂是什么意思就好了)

测试代码:

#include<iostream>
#include<string>
using namespace std;
struct Node
{
	char data;
	Node* lchild, * rchild;
};
int pos;
Node* preCreate(string s)//创建树
{
	Node* r = new Node();//初始化
	r->lchild = NULL;
	r->rchild = NULL;
	if (s[pos] == '#')
	{
		pos = pos + 1;
		return NULL;//如果碰到#表示这个是非空节点的子空节点,那么就要返回NULL
	}
	r->data = s[pos];
	pos = pos + 1;
	r->lchild = preCreate(s);//这里我用递归进行创建
	r->rchild = preCreate(s);
	return r;//最后要返回一个值让所有我们创建的指针都不是野指针
}
void preOrder(Node* r)//前序遍历
{
	if (r)//判断是否为空
	{
		cout << r->data;//先访问根节点
		preOrder(r->lchild);//再访问左
		preOrder(r->rchild);//访问右
	}
	else
	{
		cout << "#";
	}
}
void midOrder(Node* r)
{
	if (r)
	{
		midOrder(r->lchild);
		cout << r->data;
		midOrder(r->rchild);
	}
	else
		cout << "#" ;
}
void aftOrder(Node* r)
{
	if (r)
	{
		aftOrder(r->lchild);
		aftOrder(r->rchild);
		cout << r->data;
	}
	else
		cout << "#";
}
void deleteT(Node* r)//保证不为空,否则delete的时候会报错
{
	if (r)
	{
		deleteT(r->lchild);
		deleteT(r->rchild);
		delete r;
	}
}
int main()
{
	string s;//用来输入字符串
	while (cin>>s)
	{
		pos = 0;
		Node* root = preCreate(s);
		//preOrder(root);//前序
		//midOrder(root);//中序
		aftOrder(root);//后序
		deleteT(root);//删除
		cout << endl;
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值