二叉树的每个结点至多只有二棵子树(不存在度大于2的结点),二叉树的子树有左右之分,次序不能颠倒。二叉树的第i层至多有2^{i-1}个结点;深度为k的二叉树至多有2^k-1个结点;对任何一棵二叉树T,如果其终端结点数为n_0,度为2的结点数为n_2,则n_0=n_2+1。
二叉树主要有以下几类:
(1) 完全二叉树——若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。
(2) 满二叉树——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
(3) 平衡二叉树——平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
二叉树的性质:(1) 在非空二叉树中,第i层的结点总数不超过 , i>=1;
(2) 深度为h的二叉树最多有个结点(h>=1),最少有h个结点;
(3) 对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1;
(4) 具有n个结点的完全二叉树的深度为
(5)有N个结点的完全二叉树各结点如果用顺序方式存储,则结点之间有如下关系:
若I为结点编号则 如果I>1,则其父结点的编号为I/2;
如果2*I<=N,则其左儿子(即左子树的根结点)的编号为2*I;若2*I>N,则无左儿子;
如果2*I+1<=N,则其右儿子的结点编号为2*I+1;若2*I+1>N,则无右儿子。
(6)给定N个节点,能构成h(N)种不同的二叉树。h(N)为卡特兰数</a>的第N项。h(n)=C(2*n,n)/(n+1)。
二叉树有两种存储方式:1)顺序存储;2)链表存储。
树和二叉树的区别:
1. 树中结点的最大度数没有限制,而二叉树结点的最大度数为2;
2. 树的结点无左、右之分,而二叉树的结点有左、右之分。
线索二叉树
在结点结构中增加两个标志域LTag和RTag。n个结点的二叉链表中含有n+1(2n-(n-1)=n+1)个空指针域。利用二叉链表中的空指针域,存放指向结点在某种遍历次序下的前趋和后继结点的指针(这种附加的指针称为"线索")。
优点:这种二叉树解决了无法直接找到该节点在某种遍历序列中的前驱和后继节点的问题。
这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
二叉树的遍历本质上是将一个复杂的非线性结构转换为线性结构,使每个结点都有了唯一前驱和后继(第一个结点无前驱,最后一个结点无后继)。对于二叉树的一个结点,查找其左右子女是方便的,其前驱后继只有在遍历中得到。为了容易找到前驱和后继,有两种方法。一是在结点结构中增加向前和向后的指针fwd和bkd,这种方法增加了存储开销,不可取;二是利用二叉树的空链指针。现将二叉树的结点结构重新定义如下:其中:ltag=0 时lchild指向左子女;ltag=1 时lchild指向前驱;rtag=0 时rchild指向右子女;rtag=1 时rchild指向后继;
建立线索二叉树,或者说对二叉树线索化,实质上就是遍历一棵二叉树。在遍历过程中,访问结点的操作是检查当前的左,右指针域是否为空,将它们改为指向前驱结点或后续结点的线索。为实现这一过程,设指针pre始终指向刚刚访问的结点,即若指针p指向当前结点,则pre指向它的前驱,以便设线索。
另外,在对一颗二叉树加线索时,必须首先申请一个头结点,建立头结点与二叉树的根结点的指向关系,对二叉树线索化后,还需建立最后一个结点与头结点之间的线索。
平衡二叉树
平衡二叉树(Balanced Binary Tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
最小二叉平衡树的节点的公式如下 F(n)=F(n-1)+F(n-2)+1 这个类似于一个递归的数列,可以参考Fibonacci数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量。
平衡二叉树的作用与优点:
我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度(O(log2n))同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度。
平衡二叉树高度一般都良好地维持在O(log2n),大大降低了操作的时间复杂度。
下面是二叉树基本上经常用的所有操作。
<span style="font-size:14px;">/*
*使用二叉链表实现二叉树
*
*/
public class BinaryTree <T>
{
private Node root;//根节点
/**
*节点类
*@author Bao Xukai
*/
class Node
{
int value;
Node leftChild;
Node rightChild;
Node(int value){
this.value = value;
leftChild = null;
rightChild = null;
}
}
//默认构造器
public BinaryTree(){
root = null;
}
/**
*根据给定数组构建二叉树
*@param arr 用来构造二叉树的数组
*/
public BinaryTree(int[] arr){
buildBinTree(arr);
}
//根据数组建立二叉树
private void buildBinTree(int[] arr){//可以看出,由数组建立了二叉搜索树
for(int i : arr){
insert(i);
}
}
private void insert(int value){
root = insert(root,value);
}
/**
*将给定数值插入到二叉树中,比当前节点小的插入到当前节点的左侧,比当前节点大的插入到当前节点的右侧。
*@param nood 当前的节点,就是根节点
*@param value 要插入的值
*@return 插入值结束后的根节点
*/
private Node insert(Node node, int value){
if(node == null){
node = new Node(value);
}else{
if(value <= node.value){
node.leftChild = insert(node.leftChild,value);//递归调用
}else{
node.rightChild = insert(node.rightChild,value);
}
}
return node;
}
private void visit(Node node){
if(node == null){//节点为空返回
return;
}
System.out.print(node.value + " ");
}
//先序遍历二叉树
/*
*从根节点开始递归对树进行先序遍历
*/
public void preTraverse(){
System.out.println("先序遍历:");
preTraverse(root);
}
//中序遍历二叉树
public void inTraverse(){
System.out.println("中序遍历:");
inTraverse(root);
}
//后序遍历二叉树
public void postTraverse(){
System.out.println("后序遍历:");
postTraverse(root);
}
/*
*先序遍历
*从指定的节点作为根节点开始递归对树进行先序遍历
*/
private void preTraverse(Node node){
if(node == null)
return;
visit(node);
preTraverse(node.leftChild);
preTraverse(node.rightChild);
}
/*
*中序遍历
*/
private void inTraverse(Node node){
if(node == null)
return;
preTraverse(node.leftChild);
visit(node);
preTraverse(node.rightChild);
}
/*
*后序遍历
*/
private void postTraverse(Node node){
if(node == null)
return;
preTraverse(node.leftChild);
preTraverse(node.rightChild);
visit(node);
}
//建立大顶堆,进行堆排序
//堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,时间复杂度O(N*logN),空间复杂度O(1)
//堆排序的过程共需建堆和交换元素n-1次
public void buildMaxHeap(int[] a, int lastIndex){
for(int i=(lastIndex-1)/2; i>=0; i--){//i=(lastIndex-1)/2即从最后一个非叶子节点开始不断向前调整堆。
}
}
//查找某个节点
public Node find(int data){
Node current = root;
while (current.value != data && current != null)
{
if (data <= current.value)
{
current = current.leftChild;
}else{
current = current.rightChild;
}
/* if (current == null)
{
return NULL;
}
*/ //必须在while循环中判断current != NULL,如果在这个地方判断,容易导致当root==NULL时,在while循环中发生空指针异常
}
return current;
}
//查找树中关键字最小(或最大)的节点
public Node findMinNode(){//查找二叉树中的最小值,只需不断地在左子树中查找。因为在建树的时候保证了树中任意一个节点的右子树
//中的所有节点永远比其左子树的节点大
Node current, last;
last = null;
current = root;
if (current == null || current.leftChild == null)
{
return current;
}else{
while (current != null)
{
last = current;
current = current.leftChild;//查找最大节点current = current.leftChild;
}
return last;
}
}
//求树的深度
/**
*思路:
* 递归。
* 若为空,则其深度为0,否则,其深度等于左子树和右子树的深度的最大值加1。
*/
public void depthOfTree(){
int depth = depthOfTree(root);
System.out.println("当前树的深度是:" + depth);
}
private int depthOfTree(Node root){
int depthLeft, depthRight;
if (root == null)
{
return 0;
}
//左子树的深度
depthLeft = depthOfTree(root.leftChild);
//右子树的深度
depthRight = depthOfTree(root.rightChild);
if (depthLeft > depthRight){
return depthLeft + 1;
}else{
return depthRight + 1;
}
}
//插入节点
public void insertNode(int data){
Node newNode = new Node(data);//构造要插入的节点
if (root == null)//寻找插入的位置并插入节点
{
root = newNode;
}else{
Node current,parent;
current =root;
while (true)
{
parent = current;//每次循环首先记录下当前节点
if (data <= current.value){
current = current.leftChild;
if (current == null)
{
current = newNode;//插入
return;
}
}else{
current = current.rightChild;
if (current == null)
{
current = newNode;
return;
}
}
}
}
}
//删除节点
/**
*思路:
* 1、找到所要删除的节点
* 2、再考虑要删除的节点是怎样的节点,经分析,有三种情况:叶节点、有一个节点的节点、有两个节点的节点
* 2.1、如果删除的是一个叶子节点,直接删除即可
* 2.2、如果删除的节点有一个节点时:分两种情况,删除的节点只有一个左子节点,或者只有一个右子节点
* 2.3、如果删除的节点有两个节点时,这种情况就比较复杂,需要去寻找一个节点去替代要删除的节点。这个节点应该是什么节点呢?
* 最合适的节点是后继节点,即比要删除的节点的关键值次高的节点是它的后继节点。
* 说得简单一些,后继节点就是比要删除的节点的关键值要大的节点集合中的最小值。
*
*
*/
public boolean deleteNode(int data){
//1、找到要删除的节点
Node current = root;
Node parent = current;
boolean isLeftChild = false;//查找的过程中必须标明当前找到的节点是其父节点的左孩子还是有孩子,以便后面的删除操作
if (current == null)
{
return false;
}
while (current.value != data /*&& current != null*/ )
{
parent = current;
if (data < current.value){
isLeftChild = true;
current = current.leftChild;
}else{
isLeftChild = false;
current.rightChild = current;
}
if (current == null)
{
return false;
}
}
//currrent节点即为当前找到的节点
//程序执行到这说明已经找到了要删除的节点
//对要删除的节点进行3种情况分类讨论
//1、要删除的节点是叶子节点,直接删除即可
if (current.leftChild == null && current.rightChild == null)//删除的是叶子节点
{
if (current == root)//必须做这一步判断,否则current没有父亲parent,会导致下面的if判断出现空指针异常。
{
root == null;
}
if (isLeftChild)//可以在上面搞个boolean标记 isLeftChild
{
parent.leftChild == null;
}else{
parent.rightChild == null;
}
}else if (current.rightChild == null)//删除的节点只有一个左子节点
{
if (current == root)//要删除的节点是root根节点,那么在下面调用parent.leftChild时将发生空指针异常。
{
root = current.leftChild;
}
if (isLeftChild)
{
parent.leftChild = current.leftChild;
}else{
parent.rightChild = current.leftChild;
}
}else if (current.leftChild == null)//删除的节点只有一个右子节点
{
if (current == root)
{
root = current.rightChild;
}
if (isLeftChild)
{
parent.leftChild = current.rightChild;
}else{
parent.rightChild = current.rightChild;
}
}
else{//删除的节点有两个子节点
//先找到要删除节点的后继节点
Node successor = getSuccessor(current);
if (current == root)
{
root = successor;
}
if (isLeftChild)
{
parent.leftChild = successor;
}else{
parent.rightChild = successor;
}
//把要删除节点(current)的左子树整体移上来
successor.left = current.right;
}
return true;
}
/*
a) 如果后继节点是刚好是要删除节点的右子节点(此时可以知道,这个右子节点没有左子点,如果有,就不该这个右子节点为后继节点)
//要删除的节点为左子节点时
parent.left = successor ;
successor.left = current.left ;
b) 节点如果后继为要删除节点的右子节点的左后代
//假如要删除的节点为右子节点
successorParent.left = successor.right ;//第一步
successor.right = current.right ;//第二步
parent.right = successor ;
successor.left = current.left ;
注意:第一步和第二步在getSuccessor()方法的最后的if语句中完成;最后两步在deleteNode中完成。
*/
//寻找后继节点
//后继节点就是比要删除的节点的关键值要大的节点集合中的最小值。
private Node getSuccessor(Node delNode)//在要删除的节点delNode的右子树中寻找最小的节点(即为要删除节点的后继节点)
{
Node current = delNode.rightChild;
Node successorParent = delNode;
Node successor = delNode;
while (current != null)
{
parent = current;
current = current.leftChild;
}
//successor节点就是找到的后继节点
if (successor != delNode.rightNode)//节点如果后继为要删除节点的右子节点的左后代
{
successorParent.leftChild = successor.right;
successor.right = successorParent.leftChild;
}//后继节点是刚好是要删除节点的右子节点,此时没必要做上述两部,直接在deleteNode()方法中进行操作就行啦。
return successor;
}
//二叉树的层次遍历
//即按照层次访问,通常用队列来做。访问根,访问子女,再访问子女的子女(越往后的层次越低)(两个子女的级别相同)
public void layoutTraverse()
{
}
//返回二叉树中的叶子节点的个数
public int leaves()
{
}
//将树中的每个节点的左右孩子节点交换位置
public void reflect()
{
}
public static void main(String[] args)
{
int[] a = {5,12,-3,0,45,26,-49,8,16,89,-23};
BinaryTree binTree = new BinaryTree(a);
binTree.preTraverse();
System.out.println();
binTree.inTraverse();
System.out.println();
binTree.postTraverse();
System.out.println();
/* //二叉堆排序
for(int i=0; i<a.length-1; i++){
binTree.buildMaxHeap(a, a.length-1-i);
int temp = a[0];
a[0] = a[a.length-1-i];
a[a.length-1-i] = temp;
}
*/
// Node minNode = binTree.findMinNode();
System.out.println("当前二叉树中的最小值是:" + binTree.findMinNode().value);
//树的深度
binTree.depthOfTree();
binTree.deleteNode(16);
binTree.depthOfTree();
}
}
/*
程序输出:
先序遍历:
5 -3 -49 -23 0 12 8 45 26 16 89
中序遍历:
-3 -49 -23 0 5 12 8 45 26 16 89
后序遍历:
-3 -49 -23 0 12 8 45 26 16 89 5
当前二叉树中的最小值是:-49
当前树的深度是:5
*/</span>