一、概念
1、树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树,是因为它看起来像是一棵倒挂的树,也就是说它的根在上,而叶子在下。
如果一个树的结点n为0,那么这个树叫做空树。反之,如果结点n>0,那么这个树是非空树T。
结点:树中的一个独立单元,如6、1、20、43等都是结点
结点的度:结点拥有的子数的个数或者分支的个数。例如6有3棵子树,那么结点6的度就是3。
树的度:树中各结点的度的最大值
叶子结点:又叫做终端结点。指度为0的结点,例如图中的5,23,29,8,32,10,0都是叶子结点。
非终端结点:又叫做分支结点。指度不为0的结点。
孩子:结点子树的根。如 6结点的孩子为1,20,43。
双亲:与孩子的定义对应。如 1,20,43结点的双亲为6。
兄弟:同一个双亲的孩子之间互为兄弟。如 1,20,43互为兄弟。
祖先:从根到该结点所经分支上的所有结点。
子孙:以某结点为根的子树中的任一结点都称为该结点的子孙。
层次:结点的层次从根开始定义起,根为第一层,根的孩子为第二层。
堂兄弟:双亲在同一层的结点互为堂兄弟。
树的深度:树中结点的最大层次称为树的深度或高度。
有序树和无序树:如果将树中的结点的各子树看成从左到右是有次序的(即不能互换),则称该树为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
森林:是m(m>0)棵互不相交的树的集合。
2、二叉树
二叉树(Binary Tree)是n(n>=0)个节点所构成的集合,每个节点最多有两棵子树,子树又有左右之分,分为左子树和右子树。
二叉树共有5种形态:
- 空二叉树
- 只有一个根结点的二叉树
- 只有左子树的二叉树
- 只有右子树的二叉树
- 既有左子树又有右子树的二叉树
(1)满二叉树
如果一棵二叉树所有的分支节点都存在左子树和右子树,并且所有的叶子节点都在同一层上,这样的二叉树称为满二叉树
满二叉树具有如下特点:
叶子只能出现在最下一层
非叶子节点的度一定是2
同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
(2)完全二叉树
若二叉树的高度为h,除第h层外,其他(1~h-1层)的结点数都达到了最大个数,第h层有叶子节点,并且叶子节点都是从左到右依次排布,这就是完全二叉树。
完全二叉树的特点:
- 叶子节点只能出现在最下两层
- 最下层叶子节点在左侧并且连续
- 同样结点数的二叉树,完全二叉树的深度最小
注意:满二叉树一定是完全二叉树,但是完全二叉树不一定是满二叉树。
二叉树的存储结构有两种方式:顺序存储和链式存储
- 链式存储:链式存储是最常见和直观的二叉树存储方式。每个节点由一个包含数据和指向左右子节点的指针的结构体或类表示。节点之间的指针连接形成了树的结构。链式存储的优点是可以动态地插入、删除节点,并能够灵活地表示任意形状的二叉树。缺点是每个节点都需要额外的指针空间。
- 顺序存储:顺序存储是使用数组来表示二叉树的结构。二叉树的结点按照层序遍历的顺序依次存储在数组中。对于节点的索引i,其左子结点的索引为2i+1,右子节点的索引为2i+2。顺序存储的优点是节省了指针空间,对于完全二叉树来说是非常紧凑的。缺点是插入、删除节点比较麻烦,因为需要移动其他节点的位置。
选择使用哪种存储方式取决于具体的应用场景和需求。链式存储适用于频繁插入、删除节点的场景,而顺序存储适用于空间有限、对查询操作较多的场景。
需要注意的是,二叉树的存储结构并不限于上述两种方式,还可以根据具体需求进行扩展和优化,例如使用线索二叉树、堆等数据结构。
(3)二叉搜索树(二叉排序树)
- 若它的左子树不为空,则左子树上所有结点的值都小于根结点的值
- 若它的右子树不为空,则右子树上所有结点的值都大于根结点的值。
- 它的左右子树也分别是二叉搜索树
注意:二叉搜索树的中序遍历的结果是有序的。
(4)平衡二叉树(又成AVL树)
用于解决二叉排序树高度不确定的情况,如果二叉排序树的高度相差太大,就会让二叉排序树的时间复杂度升级为O(n),为了避免这一情况,就出现了平衡二叉树,使树的高度尽可能的小,其本质还是一棵二叉搜索树。
平衡二叉树的性质:
- 左子树和右子树的高度之差的绝对值小于等于1
- 左子树和右子树也是平衡二叉树
(5)红黑树
红黑树的特性:
- 每个节点要么是红色,要么是黑色
- 根结点为黑色
- 每个叶子节点(NIL)均为黑色【注:这里的叶子节点指的是空(NIL或者NULL)的叶子节点】
- 如果一个节点是红色,那么它的子节点必定是黑色
- 从一个节点到该节点的子孙节点NIL的所有路径上包含相同数据的黑色节点。【注:这里指到叶子节点的路径。】
特别说明:
- 特性第三条指的叶子节点为空(NIL或NULL)的结点
- 特性第五条,确保没有一条路径会比其他路径长出两倍。因为红黑树是相对接近平衡二叉树的。
红黑树的应用:
红黑树可以用来存储有序数据,时间复杂度是O(logn),效率非常高。例如,Java集合中的TreeSet和TreeMap都是通过红黑树来实现的。
红黑树的基本操作——左旋和右旋:
红黑树在删除或者添加节点滞后,很有可能破坏原有的红黑树结构特性,这时候旋转操作就必不可少了,这和AVL树有一点点相像。总而言之,旋转的目的是为了保持红黑树的特性。
左旋
对节点F进行左旋,意味着将节点F变为其右孩子节点R的左孩子结点,并将节点R的左子树变为节点F的右子树。
右旋
对节点F进行右旋,意味着将节点F变为其左孩子节点L的右孩子节点,并将节点L的右子树变为节点F的左子树
二、二叉树的实现及遍历
1、用Java代码实现一个二叉树的创建
(1)创建一个二叉树的结点:
public static class TreeNode {
public int value;
public TreeNode left;
public TreeNode right;
public TreeNode(int value) {
this.value = value;
}
}
(2)创建二叉树:
/**
* 创建二叉树
*
* @return
*/
public TreeNode createBinaryTree(LinkedList<Integer> linkedList) {
TreeNode treeNode = null;
if (linkedList == null || linkedList.size() == 0) return treeNode;
Integer value = linkedList.removeFirst();
if (value != null) {
treeNode = new TreeNode(value);
treeNode.left = createBinaryTree(linkedList);
treeNode.right = createBinaryTree(linkedList);
}
return treeNode;
}
2、二叉树的遍历
(1)先序遍历:
/**
* 二叉树遍历-先序遍历
*
* @param treeNode
*/
public void preOderBinaryTree(TreeNode treeNode) {
if (treeNode == null) return;
System.out.println(treeNode.value);
preOderBinaryTree(treeNode.left);
preOderBinaryTree(treeNode.right);
}
(2)中序遍历:
/**
* 二叉树遍历-中序遍历
*
* @param treeNode
*/
public void midOrderBinaryTree(TreeNode treeNode) {
if (treeNode == null) return;
midOrderBinaryTree(treeNode.left);
System.out.println(treeNode.value);
midOrderBinaryTree(treeNode.right);
}
(3)后序遍历:
/**
* 二叉树遍历-后序遍历
*
* @param treeNode
*/
public void postOrderBinaryTree(TreeNode treeNode) {
if (treeNode == null) return;
postOrderBinaryTree(treeNode.left);
postOrderBinaryTree(treeNode.right);
System.out.println(treeNode.value);
}
(4)层序遍历:
/**
* 二叉树遍历-层序遍历
*
* @param root
*/
public List<List<Integer>> levelOrderBinaryTree(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int queueSize = queue.size();
List<Integer> level = new ArrayList<>();
for (int i = 0; i < queueSize; i++) {
TreeNode treeNode = queue.poll();
if (treeNode != null) level.add(treeNode.value);
if (treeNode != null && treeNode.left != null) {
queue.offer(treeNode.left);
}
if (treeNode != null && treeNode.right != null) {
queue.offer(treeNode.right);
}
}
result.add(level);
}
return result;
}
(5)求二叉树的最大深度:
/**
* 求二叉树的最大深度
*
* @param root
* @return
*/
public int maxDepth(TreeNode root) {
if (root == null) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}