文章目录
前言
本文基于个人学习体会整理的知识内容,主要分享关于树的基本概念、二叉树概念及特性、二叉树的基本操作,内容如有不足,欢迎指正与交流。
1. 树型结构
1.1 基本概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下特点:
- 每个节点都只有有限个子节点或无子节点
- 没有父节点的节点称为根节点
- 每一个非根节点有且只有一个父节点
- 除了根节点外,每个子节点可以分为多个不相交的子树
- 树里面没有环路(cycle),树是递归定义的
注意:在树形结构中,子树之间是没有交集的,否则就不是树形结构
1.2 基本术语
术语 | 定义 |
---|---|
节点的度 | 一个节点含有的子树的个数 |
树的度 | 一棵树中,最大的节点度 |
叶节点/终端节点 | 度为零的节点(无子树) |
非终端节点/分支节点 | 度不为零的节点 |
父节点 | 若一个节点含有子节点,则这个节点称为其子节点的父节点 |
子节点 | 一个节点含有的子树的根节点 |
兄弟节点 | 具有相同父节点的节点互称为兄弟节点 |
节点的层次 | 从根开始定义,根为第1层,根的子节点为第2层,以此类推 |
深度 | 对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0 |
高度 | 对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0 |
堂兄弟节点 | 父节点在同一层的节点互为堂兄弟 |
节点的祖先 | 从根到该节点所经分支上的所有节点 |
子孙 | 以某节点为根的子树中任一节点 |
森林 | 由m(m>=0)棵互不相交的树的集合 |
1.3 树的表现形式
树型结构的表达方式相比线性表更复杂,存储方式也更麻烦。树有多种表现方式,如:双亲表示法、孩子表示法、孩子双亲表示法、孩子兄弟表示法等等。我认为了解其中最常用的孩子兄弟表示法足够初步掌握树的基本知识内容。
class Node {
int value; // 树中存储的数据
Node firstChild; // 第一个孩子引用
Node nextBrother; // 下一个兄弟引用
}
一个指向节点的第一个孩子,另一个指向节点的下一个兄弟节点,来建立节点之间的关系。
1.4 树的应用
此处简单举例
文件系统管理(目录和文件)是树结构的典型应用:
GTA5游戏文件在我电脑里的路径:D:\steam\steamapps\common\Grand Theft Auto V
就是一个树型结构,每个文件对应唯一一个父文件。
2. 二叉树
2.1 什么是二叉树
在计算机科学中,二叉树(Binary tree)是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构。通常分支被称作"左子树"或"右子树"。二叉树的分支具有左右次序,不能随意颠倒。
注意:对于任意的二叉树都是由以下几种情况复合而成的:
2.2 特殊形式的二叉树
满二叉树:一棵二叉树,如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。也就是说,如果一棵二叉树的层数为K,且结点总数是2^k-1,则它就是满二叉树。
完全二叉树:每层(除可能的最深层外)都完全填满节点。最深层的节点尽可能靠左排列。换句话说,从根节点到倒数第二层的节点都是满的,最后一层从左到右依次填充,中间没有空缺。满二叉树是一种特殊的完全二叉树。
2.3 二叉树的性质
-
若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有
2 i − 1 ( i > 0 ) 个结点 2^{i-1} (i>0)个结点 2i−1(i>0)个结点 -
若规定只有根结点的二叉树的深度为1,则深度为K的二叉树的最大结点数是
2 k − 1 ( k > = 0 ) 2^k - 1(k>=0) 2k−1(k>=0) -
对任何一棵二叉树,如果其叶结点个数为 n0,度为2的非叶结点个数为 n2,则有 n0 = n2 + 1 🌟
-
具有n个结点的完全二叉树的深度为
⌈ log 2 ( n + 1 ) ⌉ \lceil\log_2 (n + 1)\rceil ⌈log2(n+1)⌉ -
对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:
- 若i > 0,双亲序号:(i - 1) / 2;i = 0,i为根结点编号,无双亲结点
- 若2i+1 < n,左孩子序号:2i + 1,否则无左孩子
- 若2i+2 < n,右孩子序号:2i + 2否则无右孩子
2.4 二叉树的存储
二叉树可以用数组或链表来存储:
数组存储:若是满二叉树就能紧凑排列而不浪费空间。如果某个节点的索引为i(假设根节点的索引为0),则它左子节点的索引为2i + 1,右子节点为2i + 2。
链式存储:二叉树的链式存储是通过节点间的引用关系构建的,常见的表示方式有:
// 孩子表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用,代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,代表右孩子为根的整棵右子树
}
// 孩子双亲表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用
Node right; // 右孩子的引用
Node parent; // 当前节点的父节点
}
2.5 二叉树的构造
2.5.1 二叉树的遍历
在实现二叉树前,我们需要了解二叉树的遍历方式。遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题(如打印节点内容、节点内容加1等)。遍历是二叉树上最重要的操作之一,是进行其它运算的基础。
遍历二叉树时,L、D、R分别表示遍历左子树、访问根结点和遍历右子树:
遍历方式 | 顺序 | 描述 |
---|---|---|
前序遍历(NLR) | 根→左→右 | 先访问根结点,再遍历左子树,最后遍历右子树 |
中序遍历(LNR) | 左→根→右 | 先遍历左子树,再访问根结点,最后遍历右子树 |
后序遍历(LRN) | 左→右→根 | 先遍历左子树,再遍历右子树,最后访问根结点 |
这些方法的时间复杂度都是O(n),n为结点个数。
1. 前序遍历(Preorder)
前序遍历(Pre-Order Traversal)是依序以根节点、左节点、右节点为顺序遍历的方式:
(前序遍历)F, B, A, D, C, E, G, I, H.
// 前序遍历 根左右
void preOrder(TreeNode root) {
if (root == null) return;
System.out.print(root.val + " "); // 先访问根节点
preOrder(root.left); // 再遍历左子树
preOrder(root.right); // 最后遍历右子树
}
2. 中序遍历(Inorder)
中序遍历(In-Order Traversal)是依序以左节点、根节点、右节点为顺序遍历的方式:
(中序遍历)A, B, C, D, E, F, G, H, I.
// 中序遍历 左根右
void inOrder(TreeNode root) {
if (root == null) return;
inOrder(root.left); // 先遍历左子树
System.out.print(root.val + " "); // 再访问根节点
inOrder(root.right); // 最后遍历右子树
}
3. 后序遍历(Postorder)
后序遍历(Post-Order Traversal)是依序以左节点、右节点、根节点为顺序遍历的方式:
(后序遍历):A, C, E, D, B, H, I, G, F.
// 后序遍历 左右根
void postOrder(TreeNode root) {
if (root == null) return;
postOrder(root.left); // 先遍历左子树
postOrder(root.right); // 再遍历右子树
System.out.print(root.val + " "); // 最后访问根节点
}
4. 层序遍历(广度优先遍历)
层序遍历(广度优先遍历)会先访问离根节点最近的节点,算法借助队列实现:
//层序遍历
public void levelOrder(TreeNode root) {
if (root == null) return;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
TreeNode cur = queue.poll();
System.out.print(cur.val+" ");
if (cur.left != null) queue.offer(cur.left);
if (cur.right != null) queue.offer(cur.right);
}
System.out.println();
}
广度优先遍历 - 层次遍历:F, B, G, A, D, I, C, E, H.
2.5.2 二叉树的基本实现
讲的再多,看的再多不如自己来一遍,可以参考以下实现代码:
/**
* 二叉树的基本实现
*/
public class BinaryTree<E> {
// 节点定义
private static class Node<E> {
E data; // 节点数据
Node<E> left; // 左子节点
Node<E> right; // 右子节点
public Node(E data) {
this.data = data;
this.left = null;
this.right = null;
}
}
private Node<E> root; // 根节点
// 构造空二叉树
public BinaryTree() {
root = null;
}
// 构造只有根节点的二叉树
public BinaryTree(E rootData) {
root = new Node<>(rootData);
}
// 前序遍历
public void preOrderTraversal() {
System.out.println("前序遍历结果:");
preOrderTraversal(root);
System.out.println();
}
private void preOrderTraversal(Node<E> node) {
if (node == null) {
return;
}
System.out.print(node.data + " "); // 访问根节点
preOrderTraversal(node.left); // 遍历左子树
preOrderTraversal(node.right); // 遍历右子树
}
// 中序遍历
public void inOrderTraversal() {
System.out.println("中序遍历结果:");
inOrderTraversal(root);
System.out.println();
}
private void inOrderTraversal(Node<E> node) {
if (node == null) {
return;
}
inOrderTraversal(node.left); // 遍历左子树
System.out.print(node.data + " "); // 访问根节点
inOrderTraversal(node.right); // 遍历右子树
}
// 后序遍历
public void postOrderTraversal() {
System.out.println("后序遍历结果:");
postOrderTraversal(root);
System.out.println();
}
private void postOrderTraversal(Node<E> node) {
if (node == null) {
return;
}
postOrderTraversal(node.left); // 遍历左子树
postOrderTraversal(node.right); // 遍历右子树
System.out.print(node.data + " "); // 访问根节点
}
// 层序遍历(广度优先)
public void levelOrderTraversal() {
System.out.println("层序遍历结果:");
if (root == null) {
return;
}
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
Node<E> current = queue.poll();
System.out.print(current.data + " ");
if (current.left != null) {
queue.offer(current.left);
}
if (current.right != null) {
queue.offer(current.right);
}
}
System.out.println();
}
// 计算树的高度
public int height() {
return calculateHeight(root);
}
private int calculateHeight(Node<E> node) {
if (node == null) {
return 0;
}
int leftHeight = calculateHeight(node.left);
int rightHeight = calculateHeight(node.right);
return Math.max(leftHeight, rightHeight) + 1;
}
// 计算节点个数
public int size() {
return countNodes(root);
}
private int countNodes(Node<E> node) {
if (node == null) {
return 0;
}
return 1 + countNodes(node.left) + countNodes(node.right);
}
// 示例:构建一个简单的二叉树
public static void main(String[] args) {
BinaryTree<Integer> tree = new BinaryTree<>();
tree.root = new BinaryTree.Node<>(1);
tree.root.left = new BinaryTree.Node<>(2);
tree.root.right = new BinaryTree.Node<>(3);
tree.root.left.left = new BinaryTree.Node<>(4);
tree.root.left.right = new BinaryTree.Node<>(5);
/*
构造的二叉树结构:
1
/ \
2 3
/ \
4 5
*/
tree.preOrderTraversal(); // 前序遍历输出:1 2 4 5 3
tree.inOrderTraversal(); // 中序遍历输出:4 2 5 1 3
tree.postOrderTraversal(); // 后序遍历输出:4 5 2 3 1
tree.levelOrderTraversal(); // 层序遍历输出:1 2 3 4 5
System.out.println("树的高度:" + tree.height()); // 输出:3
System.out.println("节点个数:" + tree.size()); // 输出:5
}
}
总结
二叉树的知识点表面上看似简单,但要真正掌握却需要深入理解和大量实践。以下是我的几点体会:
-
概念理解与代码实现相辅相成 - 单纯理解理论概念而不付诸实践,很难真正掌握二叉树的精髓;同样,只会机械地写代码而不理解背后的数据结构原理,也难以写出高效的解决方案。
-
递归思想的重要性 - 二叉树操作中大量使用递归方法,这需要培养"递归思维",学会将复杂问题分解为处理当前节点和递归处理子树的组合。
-
算法设计的启示 - 二叉树的遍历方式(前序、中序、后序、层序)及其应用,让我深刻认识到选择合适的数据结构和遍历策略对算法效率的重要影响。
二叉树的知识点,多吧好像也多,少吧好像也不少🤔但实际上仅仅了解过了一遍知识点是完全不足以掌握的,更多的还是需要结合练习,才能更好的内化掉,这一点适用于数据结构整体的学习。