关于树的几个概念
- 树:非线性结构,数据元素之间的逻辑关系时前驱唯一而后续不唯一,即数据元素之间是一对多关系。
- 二叉树:每个结点最多有两个子树;左右子树是有序的,次序不能颠倒;即使树中只有一个结点,也要区分左右子树。
- 满二叉树:所有分支结点都存在左右子树,并且所有叶子都在同一层上。
- 完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为i的结点与同样深度的满二叉树中编号为i的结点再二叉树中位置完全相同。---叶子结点只能出现在最下两层;最下层的叶子一定集中在左部连续位置;倒数第二层若有叶子,一定在右部连续位置;
二叉树的存储结构
- 顺序存储结构:用一维数组存储二叉树的结点,并且结点的存储位置(数组下标)要能体现结点的逻辑关系,如双亲与孩子关系,左右兄弟等------堆排序
- 二叉链表存储结构
class Node {
private Object data;//数据域
private Node lchild;//左指针
private Node rchild;//右指针
}
遍历二叉树
前序遍历规则:
若二叉树为空,则返回空操作,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树
public void preOrder(Node tree) {
if (tree == null) {
return;
}
System.out.println(tree.data);
preOrder(tree.lchild);//先序遍历左子树
preOrder(tree.rchild);//最后遍历右子树
}
中序遍历规则:
若二叉树为空,则返回空操作,否则先中序遍历左子树,然后根结点,最后中序遍历右子树
public void InOrder(Node tree) {
if (tree == null) {
return;
}
InOrder(tree.lchild);//中序遍历左子树
System.out.println(tree.data);
InOrder(tree.rchild);//最后中序遍历右子树
}
后序遍历规则:
若二叉树为空,则返回空操作,否则先后序遍历左子树,再后序遍历右子树,最后遍历根结点
public void PostOrder(Node tree) {
if (tree == null) {
return;
}
PostOrder(tree.lchild);
preOrder(tree.rchild);
System.out.println(tree.data);
}
已知前序和中序,可以唯一确定一棵二叉树
已知后序和中序,可以唯一确定一棵二叉树
线索二叉树
二叉树的每个结点加上指向前驱和后驱的指针,结点结构如下
class BiThrNode {
private Object data;
//左右指针
private BiThrNode lchild;
private BiThrNode rchilid;
//左右标志
private Tag ltag;
private Tag rtag;
}
enum Tag {Link, Thread}
/**
* Link==0,指向左右孩子指针
* Thread==1指向前驱或后继线索
* */
线索二叉树遍历---双向链表结构,线索化的实质:将二叉链表中的空指针改为指向前驱或后驱的线索
BiThrNode pre;//一直指向刚刚访问过的结点
//中序遍历进行中序线索化
public void InThreading(BiThrNode p) {
if (p != null) {
InThreading(p.lchild);//递归左子树线索化
if (p.lchild == null) {//没有左孩子
p.ltag = Tag.T;//lchild变为前驱或后驱指针
p.lchild = pre;//左孩子指针指向前驱
}
if (pre.rchilid == null)//前驱没有右指针
{
pre.rtag = Tag.T;//pre的rchild变为前驱或后驱指针
pre.rchilid = p;//pre右孩子指针指向后继
}
pre = p;
InThreading(p.rchilid);//递归中序遍历右子树
}
}
在二叉线索链表上添加一个头结点,如图
头结点的lchild指针指向二叉树的根结点,rchild指向中序遍历时最后一个访问的结点,令二叉树中序遍历时第一个访问的lchild指针和最后一个访问的rchlid指针都指向添加的这个头结点。
/**
* T指向头结点,头结点的lchild指向二叉树根结点,头结点的rchild指向中序遍历的最后一个结点
*/
public void InOrder_Thr(BiThrNode T) {
BiThrNode p;
p = T.lchild;//p指向二叉链表树的根结点
while (p != T) {
while (p.ltag == Tag.L)//当ltag==Tag.L时,循环遍历到中序第一个结点
{
p = p.lchild;
}
System.out.println(p.data);//H
while (p.rtag == Tag.T && p.rchilid != T) {//没有指向头节点
p = p.rchilid;
System.out.println(p.data);
}
p = p.rchilid;//p进入右子树
}
}
整个操作相当于遍历了一次链表,时间复杂度O(n)
树的查找方法
二叉排序树(Binary Sort Tree),当其不为空时是具有以下的性质的二叉树:
- 若左子树不空,则左子树上所有结点的值均小于它的根结点的值
- 若右子树不为空,则右子树上所有结点的值均大于它的根结点的值
- 左右子树也为二叉排序树
递归查找二叉排序树T中是否存在key
public boolean searchBST(Node T, int key, Node f, Node p) {
if (T != null) {
p = f;
return false;
} else if (key == T.data) {//找到key
p = T;
return true;
} else if (key < (Integer) T.data) {
return searchBST(T.lchild, key, T, p);//在左子树继续找
} else {
return searchBST(T.rchild, key, T, p);//在右子树继续找
}
}
二叉排序树插入操作
当二叉排序树里面不存在关键字等于key的数据元素时,插入key
首先要先查找key---再插入
public boolean InsertBST(Node T, int key) {
Node p = new Node();
Node s = new Node();
if (!searchBST(T, key, null, p)) {
s.data = key;
s.lchild = s.rchild = null;
if (p != null) {
T = s;//s插入作为根结点
} else if (key < (Integer) p.data) {
p.lchild = s;//插入s 为左孩子
} else {
p.rchild = s;//插入s为右孩子
}
return true;
} else {
return false;
}
}
二叉排序树的删除操作
二叉排序树在删除过后,不能改变二叉排序树的特性。
有三种情况:
- 叶子结点,不影响其他结点的结构
- 仅有左子树或右子树的结点,删除结点后,将其左子树或右子树整个移动到删除结点的位置
- 左右子树都有结点,找到要删除的结点p的直接前驱或直接后继s,用s来替换结点p,然后再删除此结点
删除图中48结点
public boolean DeleteBST(Node T, int key) {
if (T == null) {
return false;
} else {
if (key == (Integer) T.data) {
return Delete(T);
} else if (key < (Integer) T.data) {
return DeleteBST(T.lchild, key);
} else {
return DeleteBST(T.rchild, key);
}
}
}
public boolean Delete(Node p) {
Node q = new Node();
Node s = new Node();
if (p.rchild == null) {
//右子树为空,直接将其左子树接上去
q = p;
p = p.lchild;
} else if (p.lchild == null) {
//左子树为空,直接将其右子树接上去
q = p;
p = p.rchild;
} else {//左右子树都不为空
q = p;
s = p.lchild;
while (s.rchild != null) {
q = s;
s = s.rchild;//以删除的结点为根结点的子树中最大的那个值
}
p.data = s.data;
if (q != p) {
q.rchild = s.lchild;//重新街上q的右子树
} else {
q.lchild = s.lchild;
}
return true;
}
return false;
}
对关键代码的分析
将要删除的p赋给变量q,p的左孩子lchild赋值给s,如下图
q = p;
s = p.lchild;
循环找左子树的右结点,知道右侧尽头,如图s指向的37,37之后没有右子树结点
while (s.rchild != null) {
q = s;
s = s.rchild;
}
将找到的37放到要删除的结点的位置上
p.data = s.data;
复杂度分析
对于平衡的二叉排序树,时间复杂度O(logn),近似于折半查找
最坏情况,时间复杂度为O(n)
平衡二叉树AVL
是一种二叉排序树,其中每个结点的左子树和右子树的高度最多相差1
BF平衡因子:左子树深度-右子树深度,BF={-1,0,1}只有这三个值
最小不平衡子树:距离插入结点最近的,且平衡隐私的绝对值大于1的结点为根的子树(逆向找回去)
如图,从插入结点37倒推回去
平衡二叉树的构建:在二叉排序树构建的过程中,每当插入一个结点时,就先检查是否破坏了平衡性,若是,则找出最小不平衡子树,调整链接关系,使之成为新的平衡子树。
旋转操作:O(1)
左旋(逆时针):p点作为v的左子树根结点
右旋(顺时针):p点作为v的右子树根结点
结点的数据结构
class Node{
private Object data;
private int bf;//平衡因子
private Node lchild;
private Node rchild;
}
右旋操作
/**
* 以p为根结点的二叉排序树右旋
* 旋转之后p指向新的根结点:即旋转前p的左子树的根结点变成了现在p的根结点,p变成了该结点的右子树根结点
*/
public void R_Rotate(Node p) {
Node L;
L = p.lchild;//旋转前L是p的左孩子
p.lchild = L.rchild;
L.rchild = p;
p = L;//p指向新的根结点
}
左旋操作
/**
* 左旋操作
*/
public void L_Rotate(Node p) {
Node R;
R = p.rchild;
p.rchild = R.lchild;
R.lchild = p;
p = R;
}