1、树定义和基本术语
节点
package com.young.tree;
/**
* <p>
* Title:树节点:二叉链表结构
* </p>
*
* @Author: yangyongbing
* @Date: 2023-04-18 13:25
* @version: v1.0
*/
public class Node<T> {
public Node<T> lChild;
private T data;
public Node<T> rChild;
public Node() {
lChild=null;
data=null;
rChild=null;
}
public Node(T data) {
this.data = data;
this.lChild=null;
this.rChild=null;
}
public Node<T> getlChild() {
return lChild;
}
public void setlChild(Node<T> lChild) {
this.lChild = lChild;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node<T> getrChild() {
return rChild;
}
public void setrChild(Node<T> rChild) {
this.rChild = rChild;
}
}
二叉树
package com.young.tree;
/**
* <p>
* Title: 二叉树
* </p>
*
* @Author: yangyongbing
* @Date: 2023-04-18
* @version: v1.0
*/
public class BinaryTree<T> {
private final int maxNodes = 100;
// 根节点
public Node<T> root;
// 创建一棵空二叉树
public BinaryTree() {
this.root = new Node<>();
}
// 创建一棵以数据元素x为根节点的二叉树
public BinaryTree(T x) {
this.root = new Node<>(x);
}
/*在当前二叉树的parent结点中插入一个新的左子结点,
若已存在左子树,则将该左子树变成新左子结点的左孩子树*/
public boolean addLeft(T x, Node<T> parent) {
if (parent == null) {
return false;
}
// 创建一个空节点
Node<T> p = new Node<>(x);
// 如果父节点的左子树为空,则直接将数据素x赋给父节点的左孩子节点
if (parent.lChild != null) {
// 将父节点的左子树赋给这个新节点左子节点
p.lChild = parent.lChild;
}
// 将新节点赋给父节点的左孩子节点
parent.lChild = p;
return true;
}
/*在当前二叉树的parent结点中插入一个新的右子节点,
若已存在右子树,则将该右子树变成新右子节点的右孩子树*/
public boolean addRight(T x, Node<T> parent) {
if (parent == null) {
return false;
}
// 创建一个空节点
Node<T> p = new Node<>(x);
if (parent.rChild != null) {
p.rChild = parent.rChild;
}
parent.rChild = p;
return true;
}
// 删除当前二叉树的parent节点中的左子树
public boolean deleteLeft(Node<T> parent) {
if (parent == null) {
return false;
} else {
parent.lChild = null;
return true;
}
}
// 删除当前二叉树的parent节点中的右子树
public boolean deleteRight(Node<T> parent) {
if (parent == null) {
return false;
} else {
parent.rChild = null;
return true;
}
}
// 先序遍历
public void preorder(Node<T> node) {
if (node != null) {
// 访问根节点
visit(node.getData());
// 先序遍历左子树
preorder(node.getlChild());
// 先序遍历右子树
preorder(node.getrChild());
}
}
// 中序遍历
public void inorder(Node<T> node) {
if (node != null) {
// 中序遍历左子树
inorder(node.lChild);
// 访问根节点
visit(node.getData());
// 中序遍历右子树
inorder(node.rChild);
}
}
// 后序遍历
public void postorder(Node<T> node) {
if (node != null) {
// 后续遍历左子树
postorder(node.lChild);
// 后续遍历右子树
postorder(node.rChild);
// 访问根节点
visit(node.getData());
}
}
// 按层次遍历
public void levelOrder() {
// 节点数组,用于存放节点
Node<T>[] queue = new Node[this.maxNodes];
if (this.root == null) {
return;
}
// 定义队首和队尾指针
int front, rear;
// 队列为空,对首指针不指向任何一个数组元素
front = -1;
// 队列为空,对尾指针指向数组第一个位置
rear = 0;
// 根节点入队
queue[rear] = this.root;
while (front != rear) {
// 访问对首节点的数据域
front++;
visit(queue[front].getData());
// 左节点入队
if (queue[front].lChild != null) {
rear++;
queue[rear] = queue[front].lChild;
}
// 右节点入队
if (queue[front].rChild != null) {
rear++;
queue[rear] = queue[front].rChild;
}
}
}
private void visit(T x) {
System.out.println(x);
}
// 在当前二叉树中查找数据x
public boolean search(Node<T> node,T x) {
if (node != null) {
// 访问根节点
T t = node.getData();
if(x==t){
return true;
}
// 先序遍历左子树
search(node.getlChild(),x);
// 先序遍历右子树
search(node.getrChild(),x);
}
return false;
}
// 按某种方式遍历二叉树中的所有节点
//按指定方式遍历二叉树
//i=0表示先序遍历,=1表示中序遍历,=2表示后序遍历,=3表示层次遍历
public void traversal(int i)
{
switch(i)
{
case 0: preorder(this.root);break;
case 1: inorder(this.root);break;
case 2: postorder(this.root);break;
default: levelOrder();
}
}
// 求当前二叉树的高度
public int getHeight(Node<T> parent) {
int lh,rh,max;
if(parent!=null){
lh=getHeight(parent.lChild);
rh=getHeight(parent.rChild);
max= Math.max(lh, rh);
return max+1;
}
return 0;
}
}
1.1、树定义
树(Tree)是若干个结点组成的有限集合,其中必须有一个结点是根结点,其余结点划分为若干个互不相交的集合,每一个集合还是一棵树,但被称为根的子树。注意,当树的结点个数为0时,我们称这棵树为空树,记为Φ。
特点:
- 树的根结点没有前驱结点,除根结点之外的所有结点有且只有一个前驱结点。
- 树中所有结点可以有零个或多个后继结点。
1.2、术语
- 结点:表示树中的元素,包括数据项及若干指向其子树的分支。
- 结点的度:结点所拥有的子树的个数称为该结点的度。
- 叶子结点:度为0的结点称为叶子结点,或者称为终端结点。
- 分支结点:度不为0的结点称为分支结点,或者称为非终端结点。一棵树的结点除叶子结点外,其余的都是分支结点。
- 孩子、双亲、兄弟:若在树中一个结点A的子树的根结点是B,则称B为A的孩子(也称子结点),称A为B的双亲(也称父节点)。具有同一个双亲的子结点互称为兄弟。
- 路径、路径长度:如果一棵树的一串结点n1,n2,…,nk有如下关系,即结点ni是ni+1的父结点(1≤i<k),就把n1,n2,…,nk称为一条由n1至nk的路径。这条路径的长度是k-1。
- 祖先、子孙:在树中,如果有一条路径从结点M到结点N,那么M就称为N的祖先,而N称为M的子孙。
- 结点的层数:规定树的根结点的层数为1,其余结点的层数等于它的双亲结点的层数加1。
- 树的深度:树中所有结点的最大层数称为树的深度.
- 树的度:树中各结点度的最大值称为该树的度。
- 有序树和无序树:如果一棵树中结点的各子树从左到右是有次序的,即若交换了某结点各子树的相对位置,则构成不同的树,称这棵树为有序树;反之,则称为无序树。
- 森林:零棵或有限棵不相交的树的集合称为森林。自然界中树和森林是不同的概念,但在数据结构中,树和森林只有很小的差别。任何一棵树,删去根结点就变成了森林。
2、二叉树
2.1、二叉树基本概念
-
二叉树
二叉树(Binary Tree)是一种每结点最多拥有2个子树的树结构,其中第1个子树被称为左子树,第2个子树被称为右子树。注意,当二叉树的结点个数为0时,我们称这个二叉树为空二叉树,记为Φ。二叉树是有序的,即若将其左、右子树颠倒,就成为另一棵不同的二叉树。即使树中结点只有一棵子树,也要区分它是左子树,还是右子树。因此二叉树具有五种基本形态。
-
满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子结点都在同一层上,这样的一棵二叉树称作满二叉树。(a)图就是一棵满二叉树,(b)图则不是满二叉树,因为该二叉树的D,F,G,H,I叶子结点未在同一层上。
-
完全二叉树
完全二叉树是一种叶子结点只能出现在最下层和次下层且最下层的叶子结点集中在树的左边的特殊二叉树。图5.5(a)所示为一棵完全二叉树,图5.4(b)和图5.5(b)都不是完全二叉树。对比图5.4(a)和图5.5(a)可以发现,满二叉树与完全二叉树存在如下关系:当树的深度相同时,若对树的结点按从上至下、从左到右的顺序进行编号,则在两种树上同一个位置上的结点的编号相同。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。
-
二叉树性质
性质1 一棵非空二叉树的第i层上最多有2i-1个结点(i≥1)。
性质2 一棵深度为k的二叉树中,最多具有2k-1个结点。
性质3 对于一棵非空的二叉树,如果叶子结点数为n0,度数为2的结点数为n2,则有:n0=n2+1。
性质4 具有n个结点的完全二叉树的深度k为[Log2n]
性质5 对于具有n个结点的完全二叉树,如果按照从上至下和从左到右的顺序对二叉树中的所有结点从1开始顺序编号,则对于任意的序号为i的结点,有:
(1)如果i>1,则序号为i的结点的父结点的序号为[插图];如果i=1,则该结点是根结点,无父结点。
(2)如果2i≤n,则序号为i的结点的左子结点的序号为2i;如果2i>n,则序号为i的结点无左子结点。
(3)如果2i+1≤n,则序号为i的结点的右子结点的序号为2i+1;如果2i+1>n,则序号为i的结点无右子结点。+1。
2.2、二叉树的存储结构
- 顺序存储结构
所谓二叉树的顺序存储,就是用一组连续的存储单元存放二叉树中的结点。一般是按照二叉树结点从上至下、从左到右的顺序存储。这样结点在存储位置上的前驱、后继关系并不一定就是它们在逻辑上的邻接关系,然而只有通过一些方法确定某结点在逻辑上的前驱结点和后继结点,这种存储才有意义。因此,依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一地反映出结点之间的逻辑关系,这样既能够最大可能地节省存储空间,又可以利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。
- 链式存储结构
所谓二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常有下面两种形式。
(1) 二叉链表存储
链表中每个结点由三个域组成,除了数据域外,还有两个指针域,分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。结点的存储结构为:
其中,data域存放某结点的数据信息;lchild与rchild分别存放指向左孩子和右孩子的指针,当左孩子或右孩子不存在时,相应指针域值为空(用符号∧或null表示)。
二叉树链式存储的每个结点可描述为:
class Node<T>
{
public Node<T> lChild; //左孩子
private T data; //数据域
public Node<T> rChild; //右孩子
public Node() //构造函数,创建一个空节点
{
data = null;
lChild = null;
rChild = null;
}
public Node(T x) //重载构造函数,创建一个数据值为x的节点
{
data = x;
lChild = null;
rChild = null;
}
}
二叉链表也可以带头结点的方式存放:
在Java中描述二叉链表的关键是确定二叉树的根,代码如下。
class BinaryTree<T>
{
public Node<T> root; //根结点
public BinaryTree() //创建一棵空二叉树
{
this.root = new Node<T>();
}
public BinaryTree(T x) //创建一棵以数据元素x为根结点的二叉树
{
this.root = new Node<T>(x);
}
//……
}
(2)三叉链表存储:
每个结点由四个域组成,具体结构为:
其中,data、lchild以及rchild三个域的意义同二叉链表结构,parent域为指向该结点双亲结点的指针。这种存储结构既便于查找孩子结点,又便于查找双亲结点,但是,相对于二叉链表存储结构而言,它增加了空间开销。
一棵二叉树的三叉链表表示:
尽管在二叉链表中无法由结点直接找到其双亲,但由于二叉链表结构灵活,操作方便,对于一般情况的二叉树,甚至比顺序存储结构还节省空间。因此,二叉链表是最常用的二叉树存储方式。
2.3、二叉树的基本操作及实现
- 二叉树的基本操作:
class BinaryTree<T>
{
private Node<T> root;
public BinaryTree(){} //创建一棵空二叉树
public BinaryTree(T x){} //创建一棵以数据元素x为根结点的二叉树
/*在当前二叉树的parent结点中插入一个新的左子结点,
若已存在左子树,则将该左子树变成新左子结点的左孩子树*/
public boolean insertLeft(T x, Node<T> parent){ }
/*在当前二叉树的parent结点中插入一个新的右孩子结点,
若已存在右子树,则将该右子树变成新右孩子结点的左子树*/
public boolean insertRight(Node<T> parent){ }
//删除在当前二叉树的parent结点中的左子树
public boolean deleteLeft(Node<T> parent){ }
//删除在当前二叉树的parent结点中的右子树
public boolean deleteRight(Node<T> parent){ }
public boolean search(T x){ } //在当前二叉树中查找数据x
public void traversal(int i){ } //按某种方式遍历当前二叉树的全部结点
public int getHeight(Node<T> parent){ } //求当前二叉树的高度
}
- 算法的实现:
算法的实现依赖于具体的存储结构,当二叉树采用不同的存储结构时,上述各种操作的实现算法是不同的。下面讨论基于二叉链表存储结构的上述操作的实现算法。
建立一棵空二叉树:
public BinaryTree()
{
this.root = new Node<T>(); //创建根结点,该结点的数据域为空
}
生成一棵二叉树:
public BinaryTree(T x) //创建一棵以数据元素x为根结点的二叉树
{
this.root = new Node<T>(x);
}
向二叉树中插入一个左孩子结点:
//在当前二叉树的parent节点中插入一个新的左孩子结点,若已存在左子树,则将该左子树变成新左孩子结点的左子树
public boolean insertLeft(T x, Node<T> parent)
{
if(parent==null) return false;
Node<T> p= new Node<T>(x); //创建一个新结点
if(parent.lChild==null)
parent.lChild = p; //将新结点直接设置到父结点的左孩子结点
else
{
//先将父结点原来的左子树设置为新结点的左子树
p.lChild = parent.lChild;
//再将新结点设置到父结点的左孩子结点
parent.lChild = p;
}
return true;
}
//注意,若要执行本操作,则必须先确定插入位置,即parent节点
删除二叉树的左子树:
//删除当前二叉树的parent结点中的左子树
public boolean deleteLeft(Node<T> parent)
{
if(parent==null) return false;
else
{
parent.lChild=null;
return true;
}
}
2.4、二叉树的遍历
二叉树的遍历是指按照某种顺序访问二叉树中的每个结点,使每个结点被访问一次且仅被访问一次。
遍历是二叉树中经常要用到的一种操作。因为在实际应用问题中,常常需要按一定顺序对二叉树中的每个结点逐个进行访问,查找具有某一特点的结点,然后对这些满足条件的结点进行处理。
通过一次完整的遍历,可使二叉树中结点信息由非线性排列变为某种意义上的线性序列。也就是说,遍历操作使非线性结构线性化。
由二叉树的定义可知,一棵二叉树由根结点、根结点的左子树和根结点的右子树三部分组成。因此,只要依次遍历这三部分,就可以遍历整个二叉树。若以D、L、R分别表示访问根结点、遍历根结点的左子树、遍历根结点的右子树,则二叉树的遍历方式有六种:DLR、LDR、LRD、DRL、RDL和RLD。如果限定先左后右,则只有前三种方式,即DLR(称为先序遍历)、LDR(称为中序遍历)和LRD(称为后序遍历)。
- 先序遍历(DLR)
先序遍历的递归过程为:
若二叉树为空,遍历结束,否则,
(1)访问根结点;
(2)先序遍历根结点的左子树;
(3)先序遍历根结点的右子树。
public void preorder(Node<T> node)
{
if(node==null) return;
else
{
visit(node.getData()); //访问根结点
preOrder(node.lChild); //先序遍历左子树
preOrder(node.rChild); //先序遍历右子树
}
}
- 中序遍历(LDR)
中序遍历的递归过程为:
若二叉树为空,遍历结束,否则,
(1)中序遍历根结点的左子树;
(2)访问根结点;
(3)中序遍历根结点的右子树。
public void inorder(Node<T> node)
{
if(node==null) return;
else
{
inorder(node.lChild); //中序遍历左子树
visit(node.getData()); //访问根结点
inorder(node.rChild); //中序遍历右子树
}
}
- 后序遍历(LRD)
后序遍历的递归过程为:
若二叉树为空,遍历结束,否则,
(1)后序遍历根结点的左子树;
(2)后序遍历根结点的右子树;
(3)访问根结点。
public void postorder(Node<T> node)
{
if(node==null) return;
else
{
postorder(node.lChild); //后序遍历左子树
postorder(node.rChild); //后序遍历右子树
visit(node.getData()); //访问根结点
}
}
- 层次遍历
二叉树的层次遍历,是指从二叉树的第一层(根结点)开始,从上至下逐层遍历,在同一层中,则按从左到右的顺序对结点逐个访问。
由层次遍历的定义可以推知,在进行层次遍历时,对一层结点访问完后,再按照它们的访问次序对各个结点的左孩子和右孩子顺序访问,这样一层一层进行,先遇到的结点先访问,这与队列的操作原则比较吻合。因此,在进行层次遍历时,可设置一个队列结构,遍历从二叉树的根结点开始,首先将根结点进队列,然后从队头取出一个元素,每取一个元素,执行下面两个操作。
(1)访问该元素所指结点。
(2)若该元素所指结点的左、右孩子结点非空,则将该元素所指结点的左孩子结点和右孩子结点顺序进队。此过程不断进行,当队列为空时,二叉树的层次遍历结束。
public void levelOrder()
{
Node<T>[] queue= new Node[this.maxNodes];//构造一个队列
int front,rear; //队首指针、队尾指针
if (this.root==null) return;
front=-1; //队列暂时为空,队首指针不指向任何一个数组元素
rear=0; //队列暂时为空,队尾指针指向第一个数组元素
queue[rear]=this.root; //二叉树的根结点进队列
while(front!=rear)
{
front++;
visit(queue[front].getData()); /*访问队首结点的数据域*/
/*将队首结点的左孩子结点进队列*/
if (queue[front].lChild!=null)
{
rear++;
queue[rear]=queue[front].lChild;
}
/*将队首结点的右孩子结点进队列*/
if (queue[front].rChild!=null)
{
rear++;
queue[rear]=queue[front].rChild;
}
}
}
2.5、完整代码
package com.young.tree;
/**
* <p>
* Title:树节点:二叉链表结构
* </p>
*
* @Author: yangyongbing
* @Date: 2023-04-18 13:25
* @version: v1.0
*/
public class Node<T> {
public Node<T> lChild;
private T data;
public Node<T> rChild;
public Node() {
lChild=null;
data=null;
rChild=null;
}
public Node(T data) {
this.data = data;
this.lChild=null;
this.rChild=null;
}
public Node<T> getlChild() {
return lChild;
}
public void setlChild(Node<T> lChild) {
this.lChild = lChild;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node<T> getrChild() {
return rChild;
}
public void setrChild(Node<T> rChild) {
this.rChild = rChild;
}
}
package com.young.tree;
/**
* <p>
* Title: 二叉树
* </p>
*
* @Author: yangyongbing
* @Date: 2023-04-18
* @version: v1.0
*/
public class BinaryTree<T> {
private final int maxNodes = 100;
// 根节点
public Node<T> root;
// 创建一棵空二叉树
public BinaryTree() {
this.root = new Node<>();
}
// 创建一棵以数据元素x为根节点的二叉树
public BinaryTree(T x) {
this.root = new Node<>(x);
}
/*在当前二叉树的parent结点中插入一个新的左子结点,
若已存在左子树,则将该左子树变成新左子结点的左孩子树*/
public boolean addLeft(T x, Node<T> parent) {
if (parent == null) {
return false;
}
// 创建一个空节点
Node<T> p = new Node<>(x);
// 如果父节点的左子树为空,则直接将数据素x赋给父节点的左孩子节点
if (parent.lChild != null) {
// 将父节点的左子树赋给这个新节点左子节点
p.lChild = parent.lChild;
}
// 将新节点赋给父节点的左孩子节点
parent.lChild = p;
return true;
}
/*在当前二叉树的parent结点中插入一个新的右子节点,
若已存在右子树,则将该右子树变成新右子节点的右孩子树*/
public boolean addRight(T x, Node<T> parent) {
if (parent == null) {
return false;
}
// 创建一个空节点
Node<T> p = new Node<>(x);
if (parent.rChild != null) {
p.rChild = parent.rChild;
}
parent.rChild = p;
return true;
}
// 删除当前二叉树的parent节点中的左子树
public boolean deleteLeft(Node<T> parent) {
if (parent == null) {
return false;
} else {
parent.lChild = null;
return true;
}
}
// 删除当前二叉树的parent节点中的右子树
public boolean deleteRight(Node<T> parent) {
if (parent == null) {
return false;
} else {
parent.rChild = null;
return true;
}
}
// 先序遍历
public void preorder(Node<T> node) {
if (node != null) {
// 访问根节点
visit(node.getData());
// 先序遍历左子树
preorder(node.getlChild());
// 先序遍历右子树
preorder(node.getrChild());
}
}
// 中序遍历
public void inorder(Node<T> node) {
if (node != null) {
// 中序遍历左子树
inorder(node.lChild);
// 访问根节点
visit(node.getData());
// 中序遍历右子树
inorder(node.rChild);
}
}
// 后序遍历
public void postorder(Node<T> node) {
if (node != null) {
// 后续遍历左子树
postorder(node.lChild);
// 后续遍历右子树
postorder(node.rChild);
// 访问根节点
visit(node.getData());
}
}
// 按层次遍历
public void levelOrder() {
// 节点数组,用于存放节点
Node<T>[] queue = new Node[this.maxNodes];
if (this.root == null) {
return;
}
// 定义队首和队尾指针
int front, rear;
// 队列为空,对首指针不指向任何一个数组元素
front = -1;
// 队列为空,对尾指针指向数组第一个位置
rear = 0;
// 根节点入队
queue[rear] = this.root;
while (front != rear) {
// 访问对首节点的数据域
front++;
visit(queue[front].getData());
// 左节点入队
if (queue[front].lChild != null) {
rear++;
queue[rear] = queue[front].lChild;
}
// 右节点入队
if (queue[front].rChild != null) {
rear++;
queue[rear] = queue[front].rChild;
}
}
}
private void visit(T x) {
System.out.println(x);
}
// 在当前二叉树中查找数据x
public boolean search(Node<T> node,T x) {
if (node != null) {
// 访问根节点
T t = node.getData();
if(x==t){
return true;
}
// 先序遍历左子树
search(node.getlChild(),x);
// 先序遍历右子树
search(node.getrChild(),x);
}
return false;
}
// 按某种方式遍历二叉树中的所有节点
//按指定方式遍历二叉树
//i=0表示先序遍历,=1表示中序遍历,=2表示后序遍历,=3表示层次遍历
public void traversal(int i)
{
switch(i)
{
case 0: preorder(this.root);break;
case 1: inorder(this.root);break;
case 2: postorder(this.root);break;
default: levelOrder();
}
}
// 求当前二叉树的高度
public int getHeight(Node<T> parent) {
int lh,rh,max;
if(parent!=null){
lh=getHeight(parent.lChild);
rh=getHeight(parent.rChild);
max= Math.max(lh, rh);
return max+1;
}
return 0;
}
}
3、线索二叉树
3.1、定义
按照某种遍历方式对二叉树进行遍历,可以把二叉树中所有结点排列为一个线性序列。在该序列中,除第一个结点外,每个结点有且仅有一个直接前驱结点,除最后一个结点外,每个结点有且仅有一个直接后继结点。但是,二叉树中每个结点在这个序列中的直接前驱结点和直接后继结点是什么,二叉树的存储结构中并没有反映出来,只能在对二叉树遍历的动态过程中得到这些信息。为了保留结点在某种遍历序列中直接前驱和直接后继的位置信息,可以利用二叉树的二叉链表存储结构中的那些空指针域来指示。这些指向直接前驱结点和指向直接后继结点的指针被称为线索(thread),加了线索的二叉树称为线索二叉树。线索二叉树将为二叉树的遍历提供许多方便。