数据结构 二叉树
二叉树
二叉树(BinaryTree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树的形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此理解二叉树显得特别重要。二叉树由一个节点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。图中展现了五种不同基本形态的二叉树。
概念
术语
-
结点:表示树中的元素,包括数据项及若干指向其子树的分支。
-
结点的度:结点所拥有的子树的个数称为该结点的度。
-
叶子结点:度为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。
-
树的深度:树中所有结点的最大层数称为树的深度。
-
树的度:树中各结点度的最大值称为该树的度
-
有序树和无序树:如果一棵树中结点的各子树从左到右是有次序的,即若交换了某结点各子树的相对位置,则构成不同的树,称这棵树为有序树;反之,则称为无序树。
-
森林:零棵或有限棵不相交的树的集合称为森林。自然界中树和森林是不同的概念,但在数据结构中,树和森林只有很小的差别。任何一棵树,删去根结点就变成了森林。
满二叉树
如果所有分支结点都存在左子树和右子树,并且所有叶子结点都在同一层上,这样的一棵二叉树称作满二叉树
完全二叉树
完全二叉树是一种叶子结点只能出现在最下层和次下层且最下层的叶子结点集中在树的左边的特殊二叉树
性质
- 一颗非空二叉树的第 i 层最多有 2i-1个节点
- 一棵深度为 k 的二叉树中,最多具有 2k-1 个结点
- 对于一棵非空的二叉树,如果叶子结点数为 n0,度数 1 为 2 的结点数为 n2,则有:n0=n2+1
- 具有 n 个结点的完全二叉树的深度 k 为⌊㏒2n⌋+12
- 对于具有 n 个结点的完全二叉树,如果按照从上至下和从左到右的顺序对二叉树中的所有结点从 0 开始顺序编号,则对于任意的序号为 i 的结点,有:
- 如果 i>1,则序号为 i 的结点的父结点的序号为 ⌊(i-1)/2⌋;如果 i=0,则该结点是根结点,无父结点
- 如果 2i≤n,则序号为 i 的结点的左子结点的序号为 2i+1;如果 2i+1>n,则序号为 i 的结点无左子结点
- 如果 2i+1≤n,则序号为 i 的结点的右子结点的序号为 2i+2;如果 2i+2>n,则序号为 i 的结点无右子结点
存储结构
顺序存储
用一组连续的存储单元存放二叉树中的结点。一般是按照二叉树结点从上至下、从左到右的顺序存储
依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适
对于一般的二叉树,如果仍按从上至下和从左到右的顺序将树中的结点顺序存储在一维数组中,则数组元素下标之间的关系不能够反映二叉树中结点之间的逻辑关系,只有增添一些并不存在的空结点,使之成为一棵完全二叉树的形式,然后再用一维数组顺序存储
一棵深度为k的右单支树,只有k个结点,却需分配2k-1个存储单元
链式存储
链表中每个结点由三个域组成,除了数据域外,还有两个指针域,分别用来给出该结点左孩子和右孩子所在的链结点的存储地址
class Node<T>{
public Node<T> lChild;
private T data;
public Node<T> rChild;
public Node(){
this.data = null;
this.lChild = null;
this.rChild = null;
}
public Node(T x){
this.data = x;
this.lChild = null;
this.rChild = null;
}
}
在Java中描述二叉链表的关键是确定二叉树的根,代码如下
class BinaryTree<T>{
public Node<T> root; //根节点
public BinaryTree(){
this.root = new Node<T>();
}
public BinaryTree(T x){
this.root = new Node<T>(x);
}
}
三叉链表存储
每个结点由四个域组成
这种存储结构既便于查找孩子结点,又便于查找双亲结点,但是,相对于二叉链表存储结构而言,它增加了空间开销
尽管在二叉链表中无法由结点直接找到其双亲,但由于二叉链表结构灵活,操作方便,对于一般情况的二叉树,甚至比顺序存储结构还节省空间。因此,二叉链表是最常用的二叉树存储方式
基本操作
class BinaryTree<T>{
private Node<T> root;
// 创建一颗空二叉树
public BinaryTree(){}
// 创建一颗以数据元素x为根节点的二叉树
public BinaryTree(x){}
// 在当前二叉树的父节点中插入一个新的左子节点,若已存在左子树,则将该左子树变成新左子节点的左子树
public boolean insertLeft(T x, Node<T> parent){}
// 在当前二叉树的父节点中插入一个新的右子节点,若已存在右子树,则将该右子树变成新右子节点的右子树
public boolean insertRight(T x, Node<T> parent){}
// 删除在当前二叉树的父节点中的左子树
public boolean deleteLeft(Node<T> parent){}
// 删除在当前二叉树的父节点中的右子树
pulbic boolean deleteRight(Node<T> parent){}
// 在当前二叉树中查找数据x
public boolean search(T x){}
// 按某种方式遍历当前二叉树的全部节点
public void traversal(int i){}
// 求当前二叉树的高度
public int getHeight(Node<T> parent){}
}
二叉树的遍历
二叉树的遍历是指按照某种顺序访问二叉树中的每个结点,使每个结点被访问一次且仅被访问一次
通过一次完整的遍历,可使二叉树中结点信息由非线性排列变为某种意义上的线性序列。也就是说,遍历操作使非线性结构线性化
前、中、后序遍历
// 二叉树
public class BinaryTree<T> {
private Node<T> root;
public Node<T> getRoot() {
return root;
}
public void setRoot(Node<T> root) {
this.root = root;
}
public void preOrder() {
if (this.root == null) {
System.out.println("二叉树为空");
return;
}
root.preOrder();
}
public void inOrder() {
if (this.root == null) {
System.out.println("二叉树为空");
return;
}
root.inOrder();
}
public void postOrder() {
if (this.root == null) {
System.out.println("二叉树为空");
return;
}
root.postOrder();
}
// 节点
public static class Node<T> {
// 数据
private T data;
// 左子节点
private Node<T> lChild;
// 右子节点
private Node<T> rChild;
public Node(T data) {
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node<T> getLChild() {
return lChild;
}
public void setLChild(Node<T> lChild) {
this.lChild = lChild;
}
public Node<T> getRChild() {
return rChild;
}
public void setRChild(Node<T> rChild) {
this.rChild = rChild;
}
// 先序遍历
public void preOrder() {
System.out.println(this.data);
if (this.lChild != null) {
this.lChild.preOrder();
}
if (this.rChild != null) {
this.rChild.preOrder();
}
}
// 中序遍历
public void inOrder() {
if (this.lChild != null) {
this.lChild.inOrder();
}
System.out.println(this.data);
if (this.rChild != null) {
this.rChild.inOrder();
}
}
// 后续遍历
public void postOrder() {
if (this.lChild != null) {
this.lChild.postOrder();
}
if (this.rChild != null) {
this.rChild.postOrder();
}
System.out.println(this.data);
}
}
}
构建二叉树并分别使用三种遍历
BinaryTree<Integer> binaryTree = new BinaryTree<>();
BinaryTree.Node<Integer> a = new BinaryTree.Node<>(1);
BinaryTree.Node<Integer> b = new BinaryTree.Node<>(2);
BinaryTree.Node<Integer> c = new BinaryTree.Node<>(3);
BinaryTree.Node<Integer> d = new BinaryTree.Node<>(4);
BinaryTree.Node<Integer> e = new BinaryTree.Node<>(5);
BinaryTree.Node<Integer> f = new BinaryTree.Node<>(6);
BinaryTree.Node<Integer> h = new BinaryTree.Node<>(7);
binaryTree.setRoot(a);
a.setLChild(b);
a.setRChild(c);
b.setLChild(d);
b.setRChild(e);
c.setLChild(f);
c.setRChild(h);
System.out.println("前序遍历");
binaryTree.preOrder();
System.out.println("中序遍历");
binaryTree.inOrder();
System.out.println("后序遍历");
binaryTree.postOrder();
前序遍历
1 245 367
中序遍历
425 1 637
后序遍历
452 673 1
层次遍历
public void levelOrder() {
// 声明一个Node数组
Node<?>[] queue = new Node<?>[this.maxNodes];
int front = -1, rear = 0;
if (this.root == null) {
System.out.println("二叉树为空");
return;
}
// 当数组为空时,把root放入
queue[rear] = this.root;
while (front != rear) {
front++; // 用来取下一个元素
System.out.println(queue[front].data);
// 如果有左子树,放入数组
if (queue[front].lChild != null) {
rear++; // 用来取下一个子树
queue[rear]=queue[front].lChild;
}
// 如果有右子树,放入数组
if (queue[front].rChild != null) {
rear++;
queue[rear]=queue[front].rChild;
}
}
}
查找
public Node<T> search(T x) {
Node<T> node = null;
if (this.data == x) {
return this;
}
if (this.lChild != null) {
node = this.lChild.search(x);
if (node != null) {
return node;
}
}
if (this.rChild != null) {
node = this.rChild.search(x);
}
return node;
}
平衡二叉树
线索二叉树
为了保留结点在某种遍历序列中直接前驱和直接后继的位置信息,可以利用二叉树的二叉链表存储结构中的那些空指针域来指示。这些指向直接前驱结点和指向直接后继结点的指针被称为线索(thread),加了线索的二叉树称为线索二叉树
在2n个指针域中只有n-1个指针域用来存储结点孩子的引用,而另外n+1个指针域存放的都是null。因此,可以利用某结点空的左指针域(lchild)指出该结点在某种遍历序列中的直接前驱结点的存储地址,利用结点空的右指针域(rchild)指出该结点在某种遍历序列中的直接后继结点的存储地址
如何区别某结点的指针域内存放的是指针,还是线索?一般通过为每个结点增设两个标志位域ltag和rtag来实现
构建线索二叉树
public class ClueTree<T> {
private ClueNode<T> root;
ClueNode<T> pre = null;
public void midCluingTree() {
midCluingTree(root);
}
/**
* 方法一
* @param node
*/
public void midCluingTree(@NotNull ClueNode<T> node) {
ClueNode<T> left = node.getLeft();
ClueNode<T> right = node.getRight();
while (left != null) {
midCluingTree(left);
break;
}
if (left == null) {
left = pre;
node.setLeftFlag((byte) 1);
}
if (pre != null && pre.getRight()==null) {
pre.setRight(node);
pre.setLeftFlag((byte) 1);
}
pre = node;
while (right != null) {
midCluingTree(right);
break;
}
}
/**
* 方法二
* @param node
*/
public void midCluingTree(ClueNode<T> node) {
if (node == null) {
return;
}
// 处理左子树
midCluingTree(node.getLeft());
if (node.getLeft() == null) {
node.setLeft(pre);
node.setLeftFlag((byte) 1);
}
if (pre != null && pre.getRight() == null) {
pre.setRight(node);
pre.setRightFlag((byte) 1);
}
pre = node;
// 处理右子树
midCluingTree(node.getRight());
}
}
public class ClueNode<T> {
private T data;
private byte leftFlag;
private byte rightFlag;
private ClueNode<T> left;
private ClueNode<T> right;
public ClueNode(T data) {
this.data = data;
}
@Override
public String toString() {
return "ClueNode{" +
"data=" + data +
", leftFlag=" + leftFlag +
", rightFlag=" + rightFlag +
'}';
}
}
遍历线索二叉树
public void traverse() {
traverse(this.root);
}
public void traverse(ClueNode<T> node) {
while (node != null) {
while (node.getLeftFlag() == 0) {
node = node.getLeft();
}
System.out.println(node.getData());
while (node.getRightFlag() == 1) {
System.out.println(node.getRight().getData());
node = node.getRight();
}
node = node.getRight();
}
}
红黑树
## 红黑树
[^1]: 结点所拥有的子树的个数称为该结点的度
[^2]: ⌊㏒~2~n⌋ 表示 ㏒~2~n 的对数取整数部分,还有一种表示方法⌈㏒~2~n⌉,表示向上取整
结点所拥有的子树的个数称为该结点的度 ↩︎
⌊㏒2n⌋ 表示 ㏒2n 的对数取整数部分,还有一种表示方法⌈㏒2n⌉,表示向上取整
verse(ClueNode node) {
while (node != null) {
while (node.getLeftFlag() == 0) {
node = node.getLeft();
}
System.out.println(node.getData());
while (node.getRightFlag() == 1) {
System.out.println(node.getRight().getData());
node = node.getRight();
}
node = node.getRight();
}
} ↩︎