一、树
树是由一个集合以及在该集合上定义的一种关系构成的。
集合中的元素称为树的结点,所定义的关系称为父子关系。
父子关系在树的结点之间建立了一个层次结构。
树的结点包含一个元素及若干指向其子树的若干分支。
在这种层次结构中有一个结点具有特殊的地位,这个结点称为该树的根结点,简称树根。
树是n(n>=0)个结点的有限集
空树(n=0),空树中不包含任何系结点
非空树(n>0),此时有且仅有一个特定的称为根的结点
当n>1时,其余结点可分为m(m>0)个互不相交的有限集,
其中每个本身又是一棵树,并且称为跟的子树
1.结点的度与树的度
- 结点拥有的子树的数目称为结点的度
- 度为0的结点称为叶子或者终端结点
- 度不为0的结点称为非终端结点或者分支结点
- 除根之外的分支结点也称为内部结点
- 树内各结点的度的最大值称为树的度
2.结点的层次和树的深度
- 结点的层次从根开始定义,层次数为1的结点是根结点,其子树的根的层次树为2
- 树中结点的最大层次数为树的深度或高度
有序树:如果将树中各子树看成是从左至右有次序的,则称该树为有序树
无序树:若不考虑子树的顺序则称为无序树
M叉树:树中所有结点的最大度数为M的有序树称为M叉树
森林:是m棵互不相交的树的集合。对于树中每个结点而言,其子树的集合即为森林
二、树的种类
1.二叉树:
每个结点的度均不超过2的有序树,称为二叉树。
二叉树或者是一棵空树,或者是一棵由一个根结点和两棵互不相交的分别称为根的左子树和右子树所组成的非空树
满二叉树:高度为k且有2K+1 个结点的二叉树。在满二叉树中每层结点都达到最大数,即每层结点都是满的,因此称为满二叉树。
完全二叉树:若在一个满二叉树中,在最下层从最右侧起去掉相邻的若干子叶,得到的二叉树即为完全二叉树。
满二叉树必为完全二叉树,而完全二叉树不一定是满二叉树。
二叉树的存储结构
1. 顺序存储结构
对于满二叉树和完全二叉树来说,可以将其数据元素逐层存放到一组连续的存储单元中,用一维数组来实现顺序存储结构,将二叉树中编号为i的结点存放到数组的第i个分量中。如此根据二叉树的性质可以得到结点i的父节点,左右子结点分别存放在i /2,2i,2i+1
2. 链式存储结构
设计不同的结点结构可构成不同的链式存储结构。
在二叉树中每个结点都有两个孩子,则可以设计每个结点至少包括3个域:数据域,左子域,右子域。
数据域存放数据元素,左子域存放指向左孩子的指针,右子域存放指向右孩子的指针,利用此结构得到的二叉树存储结构称为二叉链表。
为了方便找到父节点,可以在上述结点结构中增加一个指针域,指向父结点。采用此结点结构得到的二叉树存储结构称为三叉链表。
二叉树得遍历
Node :
/**
* 二叉链表的结点
* @author Administrator
*
*/
public class Node {
Object value; //结点值
Node leftChild;//左子树的引用
Node rightChild;//右子树的引用
public Node(Object value) {
super();
this.value = value;
}
public Node(Object value, Node leftChild, Node rightChild) {
super();
this.value = value;
this.leftChild = leftChild;
this.rightChild = rightChild;
}
@Override
public String toString() {
return "Node [value=" + value + ", leftChild=" + leftChild
+ ", rightChild=" + rightChild + "]";
}
}
BinaryTree :
/**
* 二叉树接口
* 可以有不同的实现类,每个类可以使用不同的存储结构,比如顺序结构、链式结构
* @author Administrator
*
*/
public interface BinaryTree {
/**
* 是否空树
* @return
*/
public boolean isEmpty();
/**
* 树结点数量
* @return
*/
public int size();
/**
* 获取二叉树的高度
* @return
*/
public int getHeight();
/**
* 查询指定值的结点
* @param value
* @return
*/
public Node findKey(int value); // 查找
/**
* 前序递归遍历
*/
public void preOrderTraverse();
/**
* 中序遍历递归操作
*/
public void inOrderTraverse();
/**
* 后序遍历递归操作
*/
public void postOrderTraverse();
/**
* 后序遍历递归操作
* @param node 树根结点
*/
public void postOrderTraverse(Node node);
/**
* 中序遍历非递归操作
* 1)对于任意节点current,若该节点不为空则将该节点压栈,并将左子树节点置为current,重复此操作,直到current为空。
* 2)若左子树为空,栈顶节点出栈,访问节点后将该节点的右子树置为current
* 3) 重复1、2步操作,直到current为空且栈内节点为空。
*/
public void inOrderByStack();
/**
* 前序遍历非递归操作
* 1)对于任意节点current,若该节点不为空则访问该节点后再将节点压栈,并将左子树节点置为current,重复此操作,直到current为空。
* 2)若左子树为空,栈顶节点出栈,将该节点的右子树置为current
* 3) 重复1、2步操作,直到current为空且栈内节点为空。
*/
public void preOrderByStack();
/**
* 后序遍历非递归操作
* 1)对于任意节点current,若该节点不为空则访问该节点后再将节点压栈,并将左子树节点置为current,重复此操作,直到current为空。
* 2)若左子树为空,取栈顶节点的右子树,如果右子树为空或右子树刚访问过,则访问该节点,并将preNode置为该节点
* 3) 重复1、2步操作,直到current为空且栈内节点为空。
*/
public void postOrderByStack();
/**
* 按照层次遍历二叉树
*/
public void levelOrderByStack();
}
LinkedBinaryTree :
public class LinkedBinaryTree implements BinaryTree{
private Node root;//根结点
//private int size;
public LinkedBinaryTree() {
super();
}
public LinkedBinaryTree(Node root) {
super();
this.root = root;
}
@Override
public boolean isEmpty() {
return root == null;
}
@Override
public int size() {
System.out.println("二叉树结点个数:");
return this.size(root);
}
private int size(Node root) {
if(root == null){
return 0;
}else{
//获取左子树的size
int nl = this.size(root.leftChild);
//获取右子树的size
int nr = this.size(root.rightChild);
//返回左子树、右子树size之和并加1
return nl+nr+1;
}
}
@Override
public int getHeight() {
System.out.println("二叉树的高度是:");
return this.getHeight(root);
}
private int getHeight(Node root){
if(root == null){
return 0;
}else{
//获取左子树的高度
int nl = this.getHeight(root.leftChild);
//获取右子树的高度
int nr = this.getHeight(root.rightChild);
//返回左子树、右子树较大高度并加1
return nl > nr ? nl+1:nr+1;
}
}
@Override
public Node findKey(int value) {
return this.findKey(value, root);
}
public Node findKey(Object value,Node root) {
if(root == null){//递归结束条件1:结点为空,可能是整个树的根节点,也可能是递归调用中叶子节点中左孩子和右孩子
return null;
}else if(root != null && root.value == value){//递归的结束条件2:找到了
return root;
}else {//递归体
Node node1 = this.findKey(value, root.leftChild);
Node node2 = this.findKey(value, root.rightChild);
if(node1 != null && node1.value == value){
return node1;
}else if(node2 != null && node2.value == value){
return node2;
}else{
return null;
}
}
}
@Override
public void preOrderTraverse() {
if(root != null){
//1.输出根结点的值
System.out.print(root.value+" ");
//2.对左子树进行先序遍历
//构建一个二叉树,根是左子树的根
BinaryTree leftTree = new LinkedBinaryTree(root.leftChild);
leftTree.preOrderTraverse();
//对右子树进行先序遍历
//3.构建一个二叉树,根是左子树的根
BinaryTree rightTree = new LinkedBinaryTree(root.rightChild);
rightTree.preOrderTraverse();
}
}
@Override
public void inOrderTraverse() {
System.out.println("中序遍历");
this.inOrderTraverse(root);
System.out.println();
}
private void inOrderTraverse(Node root) {//node7
if(root != null){
//遍历左子树
this.inOrderTraverse(root.leftChild);//null
//输出根的值
System.out.print(root.value+" ");//7
//遍历右子树
this.inOrderTraverse(root.rightChild);//null
}
}
@Override
public void postOrderTraverse() {
System.out.println("后序遍历");
this.postOrderTraverse(root);
System.out.println();
}
@Override
public void postOrderTraverse(Node node) {
if(node != null){
//遍历左子树
this.postOrderTraverse(node.leftChild);
//遍历右子树
this.postOrderTraverse(node.rightChild);
//输出根的值
System.out.print(node.value+" ");
}
}
@Override
public void inOrderByStack() {
System.out.println("中序非递归遍历:");
// 创建栈
Deque<Node> stack = new LinkedList<Node>();
Node current = root;
while (current != null || !stack.isEmpty()) {
while (current != null) {
stack.push(current);
current = current.leftChild;
}
if (!stack.isEmpty()) {
current = stack.pop();
System.out.print(current.value + " ");
current = current.rightChild;
}
}
System.out.println();
}
@Override
public void preOrderByStack() {
System.out.println("先序非递归遍历:");
// 创建栈
Deque<Node> stack = new LinkedList<Node>();
Node current = root;
while (current != null || !stack.isEmpty()) {
while (current != null) {
System.out.print(current.value + " ");
stack.push(current);
current = current.leftChild;
}
if (!stack.isEmpty()) {
current = stack.pop();
current = current.rightChild;
}
}
System.out.println();
}
@Override
public void postOrderByStack() {
System.out.println("后序非递归遍历:");
// 创建栈
Deque<Node> stack = new LinkedList<Node>();
Node current = root;
Node preNode = null;
while (current != null || !stack.isEmpty()) {
while (current != null) {
stack.push(current);
current = current.leftChild;
}
if (!stack.isEmpty()) {
current = stack.pop();
if(current.rightChild == null || current.rightChild == preNode){
System.out.print(current.value + " ");
preNode = current;
current = null;
}else{
stack.push(current);
current = current.rightChild;
stack.push(current);
current = current.leftChild;
}
}
}
System.out.println();
}
@Override
public void levelOrderByStack() {
System.out.println("按照层次遍历二叉树");
if(root == null) return;
Queue<Node> queue = new LinkedList<Node>() ;
queue.add(root);
while(queue.size() != 0)
{
int len = queue.size();
for(int i=0;i <len; i++)
{
Node temp = queue.poll();
System.out.print(temp.value+" ");
if(temp.leftChild != null) queue.add(temp.leftChild);
if(temp.rightChild != null) queue.add(temp.rightChild);
}
}
System.out.println();
}
}
Test :
public class Test {
public static void main(String[] args) {
//创建一个二叉树
Node node5 = new Node(5, null, null);
Node node4 = new Node(4, null, node5);
Node node3 = new Node(3, null, null);
Node node7 = new Node(7, null, null);
Node node6 = new Node(6, null, node7);
Node node2 = new Node(2, node3, node6);
Node node1 = new Node(1,node4,node2);
BinaryTree btree = new LinkedBinaryTree(node1);
//BinaryTree btree = new LinkedBinaryTree();
//判断二叉树是否为空
System.out.println(btree.isEmpty());
//先序遍历递归 1 4 5 2 3 6 7
System.out.println("先序遍历");
btree.preOrderTraverse();
System.out.println();
//中序遍历递归 4 5 1 3 2 6 7
btree.inOrderTraverse();
//后序遍历递归 5 4 3 7 6 2 1
btree.postOrderTraverse();
//中序遍历非递归(借助栈) 4 5 1 3 2 6 7
btree.inOrderByStack();
//按照层次遍历(借助队列) 1 4 2 5 3 6 7
btree.levelOrderByStack();
//在二叉树中查找某个值
System.out.println(btree.findKey(1));
//二叉树的高度
System.out.println(btree.getHeight());
//二叉树的结点数量
System.out.println(btree.size());
}
}
2.二叉查找树
由于最基础的二叉树节点是无序的,想象一下如果在二叉树中查找一个数据,最坏情况可能要要遍历整个二叉树,这样的查找效率是非常低下的。
由于基础二叉树不利于数据的查找和插入,因此我们有必要对二叉树中的数据进行排序,所以就有了「二叉查找树」,可以说这种树是为了查找而生的二叉树,有时也称它为「二叉排序树」,都是同一种结构,只是换了个叫法。
二叉查找树理解了也不难,简单来说就是二叉树上所有节点的,左子树上的节点都小于根节点,右子树上所有节点的值都大于根节点。
这样的结构设计,使得查找目标节点非常方便,可以通过关键字和当前节点的对比,很快就能知道目标节点在该节点的左子树还是右子树上,方便在树中搜索目标节点。
如果对二叉查找树执行中序遍历,因为中序遍历的顺序是:左子树->根节点->右子树,最终可以得到一个节点值的有序列表。
举个栗子:对上图的二叉查找树执行中序遍历,我们可以得到一个有序序列:1 2 3 4 6 7
查询效率
二叉查找树的查询复杂度取决于目标节点的深度,因此当节点的深度比较大时,最坏的查询效率是O(n),其中n是树中的节点个数。
实际应用中有很多改进版的二叉查找树,目的是尽可能使得每个节点的深度不要过深,从而提高查询效率。比如AVL树和红黑树,可以将最坏效率降低至O(log n)。
3.AVL树(平衡二叉查找树)
AVL树是一种平衡二叉查找树,二叉查找树我们已经知道,那平衡是什么意思呢?
我们举个天平的例子,天平两端的重量要差不多才能平衡,否则就会出现向一边倾斜的情况。把这个概念迁移到二叉树中来,根节点看作是天平的中点,左子树的高度放在天平左边,右子树的高度放在天平右边,当左右子树的高度相差「不是特别多」,称为是平衡的二叉树。
AVL树有更严格的定义:在二叉查找树中,任一节点对应的两棵子树的最大高度差为 1,这样的二叉查找树称为平衡二叉树。其中左右子树的高度差也有个专业的叫法:平衡因子。
AVL树的旋转
一旦由于插入或删除导致左右子树的高度差大于1,此时就需要旋转某些节点调整树高度,使其再次达到平衡状态,这个过程称为旋转再平衡。
保持树平衡的目的是可以控制查找、插入和删除在平均和最坏情况下的时间复杂度都是O(log n),相比普通二叉树最坏情况的时间复杂度是 O(n) ,AVL树把最坏情况的时间复杂度控制在可接受范围,非常适合对算法执行时间敏感类的应用。
4.B树
一个 m 阶的B树是一个有以下属性的树:
-
每一个节点最多有 m 个子节点。
-
每一个非叶子节点(除根节点)最少有 ⌈m/2⌉ 个子节点,⌈m/2⌉表示向上取整。
-
如果根节点不是叶子节点,那么它至少有两个子节点。
-
有 k 个子节点的非叶子节点拥有 k − 1 个键。
-
所有的叶子节点都在同一层。
如果之前不了解,相信第一眼看完定义肯定是蒙圈,不过多看几遍好好理解一下就好了,画个图例,对照着看看:
特点
-
B树是所有节点的平衡因子均等于0的多路查找树(AVL树是平衡因子不大于 1 的二路查找树)。
-
B树节点可以保存多个数据,使得B树可以不用像AVL树那样为了保持平衡频繁地旋转节点。
-
B树的多路的特性,降低了树的高度,所以B树相比于平衡二叉树显得矮胖很多。
-
B树非常适合保存在磁盘中的数据读取,因为每次读取都会有一次磁盘IO,高度降低减少了磁盘IO的次数。
B树常用于实现数据库索引,典型的实现,MongoDB索引用B树实现,MySQL的Innodb存储引擎用B+树存放索引。
说到B树不得不提起它的好兄弟B+树,不过这里不展开细说,只需知道,B+树是对B树的改进,数据都放在叶子节点,非叶子节点只存数据索引。
5.红黑树
红黑树中每个结点都被标记了红黑属性,红黑树除了有普通的「二叉查找树」特性之外,还有以下的特征:
-
节点是红色或黑色。
-
根是黑色。
-
所有叶子都是黑色(叶子是NIL节点)。
-
每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
-
从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
这些性质有兴趣可以自行研究,不过,现在你只需要知道,这些约束确保了红黑树的关键特性:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。
红黑树
而节点的路径长度决定着对节点的查询效率,这样我们确保了,最坏情况下的查找、插入、删除操作的时间复杂度不超过O(log n),并且有较高的插入和删除效率。
应用场景
红黑树在实际应用中比较广泛,有很多已经落地的实践,比如学习C++的同学都知道会接触到STL标准库,而STL容器中的map、set、multiset、multimap底层实现都是基于红黑树。
红黑树 VS 平衡二叉树(AVL树)
-
插入和删除操作,一般认为红黑树的删除和插入会比AVL树更快。因为,红黑树不像AVL树那样严格地要求平衡因子小于等于 1,这就减少了为了达到平衡而进行的旋转操作次数,可以说是牺牲严格平衡性来换取更快的插入和删除时间。
-
红黑树不要求有严格的平衡性控制,但是红黑树的特点,使得任何不平衡都会在三次旋转之内解决。而AVL树如果不平衡,并不会控制旋转操作次数,旋转直到平衡为止。
-
查找操作,AVL树的效率更高。因为AVL树设计比红黑树更加平衡,不会出现平衡因子超过 1 的情况,减少了树的平均搜索长度。
6.Trie树(前缀树或字典树)
Trie的核心思想是空间换时间,有3个基本性质:
-
根节点不包含字符,除根节点外每一个节点都只包含一个字符。
-
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
-
每个节点的所有子节点包含的字符都不相同。
比如对单词序列 lru, lua, mem, mcu
建立Trie树如下:
Trie树建立和查询是可以同步进行的,可以在还没建立完成Trie树之前就找到目标数据,而如果用Hash表等结构存储是需要先建立完成表才能开始查询,这也是Trie树查询速度快的原因。
应用
Trie树还用于搜索引擎的关键词提示功能。比如当你在搜索框中输入检索单词的开头几个字,搜索引擎就可以自动联想匹配到可能的词组出来,这正是Trie树的最直接应用。
@TODO-PAN