二叉树是一种非线性的数据结构。它是由n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两颗不相交的、被分别称为左子树、右子树的二叉树组成。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也可以称做一个结点。
二叉树是有序的,即若将其左右两个子树颠倒,就成为另一棵不同的二叉树。这也就意味着,即使某棵二叉树的树中结点只有一棵子树,也同样要区分是左子树还是右子树。
二叉树的基本概念
- 结点的度:结点所拥有的子树的个数;
- 叶结点:度为0的结点,或称为终端结点;
- 分枝结点:度不为0的结点,或称为非终端结点;
- 左孩子、右孩子、双亲
- 路径、路径长度
- 结点的层数:规定树的根结点的层数为1,其他结点的层数等于它双亲结点的层数加1;
- 树的深度:树中所有结点的最大层数;
- 树的度:树中各结点度的最大值;
- 满二叉树:一棵二叉树中,如果所有的分枝结点都存在左子树和右子树,且所有的叶子结点都在同一层上;
- 完全二叉树:一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左至右的顺序进行编号,如果编号为i的结点与满二叉树中编号为i的结点在二叉树中的位置相同。
完全二叉树的特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。
二叉树的主要性质
- 一棵非空二叉树的第i层最多有2^(i-1)个结点。
- 一颗深度为k的二叉树中,最多具有2^k-1个结点。
- 对于一棵非空的二叉树,如果叶子结点的数目是n0,度数为2的结点树为n2,则有n0=n2+1。
- 具有n个结点的完全二叉树的深度为[lbn]+1。
- 对于具有n个结点的完全二叉树,如果对树中的结点按从上至下、从左至右的顺序从1开始顺序进行编号,则对于任何的序号为i的结点,有如下的情况:
- 如果i>1,则序号为i的结点的双亲节点的序号为n/2(“/”代表整除);如果i=1,则它是根结点,无双亲节点。
- 如果2i<n,则序号为i的结点的左孩子结点的序号为2i;如果2i>n,则序号为i的结点无左孩子。
- 如果2i+1<n,则序号为i的结点的右孩子结点的序号为2i+1;如果2i+1>n,则序号为i的结点无右孩子。
关于二叉树的第三点性质的证明:
从节点上来讲:n=n0+n1+n2;
从路径上来讲:n-1=n1+2*n2(等式左边是进入分支,等式右边为发出分支);
二叉树的抽象数据类型
- 数据元素:具有相同元素(结点)的数据集合;
- 数据结构:结点之间通过左右引用维护之间的关系;
- 数据操作:对二叉树的基本操作定义在IBiTree中,代码如下:
public interface IBiTree<E> {
void create(E val, Node<E>l, Node<E> r); //以val为根节点元素,l和r为左右子树构造二叉树
void insertL(E val, Node<E> p); //将元素插入p的左子树
void insertR(E val, Node<E> p); //将元素插入p的右子树
Node<E> deleteL(Node<E> p) ; //删除p的左子树
Node<E> deleteR(Node<E> p); //删除p的右子树
Node<E> search(Node<E> root, E value) ; //在root树中查找结点元素为value的结点
void traverse(Node<E> root, int i); //按某种方式i遍历root二权树
}
二叉树的实现
二叉树的顺序存储
二叉树的顺序存储,是用一组连续的存储单元存放二叉树的结点,通常按照二叉树结点从上至下、从左至右的顺序存储。这样结点在存储位置上的前趋后继关系并不一定就是他们在逻辑上的邻接关系。然而只有通过一些方法确定某结点在逻辑上的前趋结点和后继结点,这样的存储方式才有意义。
因此根据二叉树的性质,完全二叉树和满二叉树采用顺序存储的方式比较合适,树中结点的序号可以唯一地反映结点之间的逻辑关系,这样既能最大可能的节省存储空间,又可以利用数组元素的下标值确定结点在二叉树中的位置以及结点之间的关系。
但如果是一般的二叉树,如果还按照结点从上至下、从左至右的顺序存储将结点存储在一个一维数组中,则数组元素下标之间不一定能反映结点之间的逻辑关系,需要添加一些不存在的空节点,使之成为一棵安全二叉树或者满二叉树。这会造成空间的大量浪费,最坏的情况是右单支树。
二叉树的链式存储
二叉树的链式存储结构就是用链表来表示一棵二叉树,即用链表来指示元素之间的逻辑关系。通常有两种存储形式:
- 链表中每个结点由三个域组成,除了数据域之外,还有两个指针域,分别用来给出该结点的左孩子和右孩子所在的存储地址。
- 链表中每个结点由四个域组成,除了数据域之外,还有三个指针域,分别用来给出该结点的左孩子、右孩子和双亲结点所在的存储地址。
两种方式的区别和单链表和双向链表的区别有些类似,下面以第一种方式为例来实现二叉树的链式存储:
public class Node<E>{
private E data; //数据域
private Node<E> lchild; //左孩子
private Node<E> rchild; //右孩子
//构造函数
public Node(E val, Node<E> lp, Node<E> rp){
data = val;
lchild = lp;
rchild = rp;
}
//构造函数
public Node(Node<E> lp, Node<E> rp){
this(null,lp,rp);
}
//构造函数
public Node(E val){
this(val,null,null);
}
//构造函数
public Node() {
this(null);
}
//数据属性
public E getData() {
return data;
}
public void setData(E data) {
this.data = data;
}
//左孩子
public Node<E> getLchild() {
return lchild;
}
public void setLchild(Node<E> lchild) {
this.lchild = lchild;
}
//右孩子
public Node<E> getRchild() {
return rchild;
}
public void setRchild(Node<E> rchild) {
this.rchild = rchild;
}
}
public class LinkBiTree<E> implements IBiTree<E> {
private Node<E> head; // 链表头引用指针
public Node<E> getHead() {
return head;
}
// 构造函数,生成一棵以val为根结点数据域信息,以二叉树lp和rp为左子树和右子树的二叉树。
public LinkBiTree(E val, Node<E> lp, Node<E> rp) {
Node<E> p = new Node<E>(val, lp, rp);
head = p;
}
// 构造函数,生成一棵以val为根结点数据域信息的二叉树
public LinkBiTree(E val) {
this(val, null, null);
}
// 构造函数,生成一棵空的二叉树
public LinkBiTree() {
head = null;
}
// 判断是否是空二叉树
public boolean isEmpty() {
return head == null;
}
// 获取根结点
public Node<E> Root() {
return head;
}
// 获取结点的左孩子结点
public Node<E> getLchild(Node<E> p) {
return p.getLchild();
}
// 获取结点的右孩子结点
public Node<E> getRchild(Node<E> p) {
return p.getRchild();
}
// 创建二叉树
public void create(E val, Node<E> l, Node<E> r) {
Node<E> p = new Node<E>(val, l, r);
head = p;
}
// 将结点p的左子树插入值为val的新结点,
// 原来的左子树成为新结点的左子树
public void insertL(E val, Node<E> p) {
Node<E> tmp = new Node<E>(val);
tmp.setLchild(p.getLchild());
p.setLchild(tmp);
}
// 将结点p的右子树插入值为val的新结点,
// 原来的右子树成为新结点的右子树
public void insertR(E val, Node<E> p) {
Node<E> tmp = new Node<E>(val);
tmp.setRchild(p.getRchild());
p.setRchild(tmp);
}
// 若p非空,删除p的左子树
public Node<E> deleteL(Node<E> p) {
if ((p == null) || (p.getLchild() == null)) {
return null;
}
Node<E> tmp = p.getLchild();
p.setLchild(null);
return tmp;
}
// 若p非空,删除p的右子树
public Node<E> deleteR(Node<E> p) {
if ((p == null) || (p.getRchild() == null)) {
return null;
}
Node<E> tmp = p.getRchild();
p.setRchild(null);
return tmp;
}
// 编写算法,在二叉树中查找值为value的结点
public Node<E> search(Node<E> root, E value) {
Node<E> p = root;
if (p == null) {
return null;
}
if (!p.getData().equals(value)) {
return p;
}
if (p.getLchild() != null) {
return search(p.getLchild(), value);
}
if (p.getRchild() != null) {
return search(p.getRchild(), value);
}
return null;
}
// 判断是否是叶子结点
public boolean isLeaf(Node<E> p) {
return ((p != null) && (p.getLchild() == null) && (p.getRchild() == null));
}
// 中序遍历
public void inorder(Node<E> p) {
}
// 前序遍历
public void preorder(Node<E> p) {
}
// 后序列遍历
public void postorder(Node<E> p) {
}
// 层次遍历
public void levelOrder(Node<E> root) {
}
// 遍历二叉树
public void traverse(Node<E> root, int i) {
}
其中关于链表的几种遍历方式(中序遍历、前序遍历、后序遍历、层次遍历),在下一篇文章中会具体地讲到。