文章目录
一、树的存储结构
1.1 树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
- 根节点没有前驱结点
- 除根结点外,其余的结点被分为M(M>0)个互不相交的集合T1,T2,…Tm;每一个集合又是一棵与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
- 树是递归定义的。
注意:在树形结构中,子树之间不能有交集。
1.2 树的结点
结点: 使用树结构存储的每一个数据元素都被称为“结点”;例如上图中的A,B,C…都是一个结点
叶子结点或终端结点: 如果结点没有任何子结点,那么此结点称为叶子结点(叶结点,终端结点);例如上图中的K,L,F…都是这棵树的叶子结点。
双亲结点或父结点: 若一个结点含有子结点,则这个结点称为其子结点的父结点;上图中的A是B,C,D的父结点。
孩子结点或子结点: 一个结点含有的子树的根结点称为该结点的子结点;上图中的K为E的子结点。
根结点: 一棵树中,没有双亲结点的结点;只有A结点
非终端结点或分支结点: 度不为0的结点;例如A,B,C,D,E,H
兄弟结点: 具有相同父结点的结点互称为兄弟结点;E和F的父节点都是B,所以它们为兄弟结点。
堂兄弟结点: 双亲在同一层的结点互为堂兄弟;B,C,D或者K,L,M为堂兄弟结点
结点的祖先: 从根到该结点所经分支上的所有结点;A是祖先
1.3 子树和空树
子树: 在上图中,整棵树的根结点为结点 A,而如果单看结点 B、E、F、K、L 组成的部分来说,也是棵树,而且节点 B 为这棵树的根结点。所以称 B、E、F、K、L 这几个结点组成的树为整棵树的子树;同样,结点 E、K、L 构成的也是一棵子树,根结点为 E。
注意:单个结点也是一棵树,只不过根结点就是它本身。上图中,结点 K、L、F 等都是树,且都是整棵树的子树。
知道了子树的概念后,树可以这样定义:树是由根结点和若干棵子树构成的。
空树: 如果集合本身为空,那么构成的树就被称为空树。空树中没有结点。
1.4 结点的度和层次
结点的度: 一个结点含有子树的个数称为该结点的度;例如A结点,在其下分为3颗子树,所以该结点的度为3.
树的度: 一棵树中,所有结点度的最大值称为树的度;上图中各个结点中的最大值为3,所以树的度为3.
结点的层次: 从根开始定义起,根为第1层,根的子结点为第2层;上图中,A 结点在第一层,B、C、D 为第二层,E、F、G、H、I、J 在第三层,K、L、M 在第四层。
树的高度或深度: 树中结点的最大层次;上图中树的深度为 4。
1.5 森林
子孙: 以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙
森林: 由m(m>=0)棵互不相交的树组成的集合称为森林,分别以 B、C、D 为根结点的三棵子树就可以称为森林。
1.6 树的表示形式
树不同于线性结构,它是非线性的,其存储的是具有“一对多”关系的数据元素的集合;其表示方法有双亲表示法,孩子表示法、孩子双亲表示法、孩子兄弟表示法等等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
class Node {
int value; // 树中存储的数据
Node firstChild; // 第一个孩子引用
Node nextBrother; // 下一个兄弟引用
}
1.7 树的应用
- 文件系统管理(目录和文件)
二、 二叉树
2.1 概念
一棵二叉树是结点的一个有限集合,该集合:
- 或者为空
- 或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
从上图中可以得出二叉树的条件:
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
- 树中包含的各个节点的度不能超过 2,即只能是 0、1 或者 2;
所以 1a) 就是一棵二叉树,而 b) 则不是。
注:对于任意的二叉树都是由以下几种情况复合而成的
2.2 二叉树的性质
- 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有 2 ^( i -1)(i>0)个结点
- 若规定只有根结点的二叉树的深度为1,则深度为K的二叉树的最大结点数是2^(k - 1) (k>=0)
- 对任何一棵二叉树, 如果其叶结点个数为 n0, 度为2的非叶结点个数为 n2,则有n0=n2+1
性质 3 的计算方法为:对于一个二叉树来说,除了度为 0 的叶子结点和度为 2 的结点,剩下的就是度为 1 的结点(设为 n1),那么总结点 n=n0+n1+n2。
同时,对于每一个结点来说都是由其父结点分支表示的,假设树中分枝数为 B,那么总结点数 n=B+1。而分枝数是可以通过 n1 和 n2 表示的,即 B=n1+2* n2。所以,n 用另外一种方式表示为 n=n1+2*n2+1。
两种方式得到的 n 值组成一个方程组,就可以得出 n0=n2+1。
2.3 满二叉树
如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。
上图就是满二叉树。
满二叉树除了满足普通二叉树的性质,还具有以下性质:
- 满二叉树中第 i 层的节点数为 2^(n-1) 个。
- 深度为 k 的满二叉树必有 2^k-1 个节点 ,叶子数为 2 ^(k-1)。
- 满二叉树中不存在度为 1 的节点,每一个分支点中都两棵深度相同的子树,且叶子节点都在最底层。
- 具有 n 个节点的满二叉树的深度为 log2(n+1)。
2.4 完全二叉树
如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。
如图 a) 所示是一棵完全二叉树,图 b) 由于最后一层的节点没有按照从左向右分布,因此只能算作是普通的二叉树。
完全二叉树除了具有普通二叉树的性质,它自身也具有一些独特的性质:
- 具有n个结点的完全二叉树的深度k为 ⌊log2n⌋+1。上取整
⌊log2n⌋ 表示取小于 log2n 的最大整数。例如,⌊log2 4⌋ = 2,而 ⌊log2 5⌋ 结果也是 2。
- . 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:
若i>0,双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点
若2i+1<n,左孩子序号:2i+1,否则无左孩子
若2i+2<n,右孩子序号:2i+2,否则无右孩子
2.5 二叉树的存储
二叉树的存储结构有两种,分别为顺序存储和链式存储
顺序存储
二叉树的顺序存储,指的是使用顺序表(数组)存储二叉树。需要注意的是,顺序存储只适用于完全二叉树。换句话说,只有完全二叉树才可以使用顺序表存储。因此,如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树。
完全二叉树的顺序存储,仅需从根节点开始,按照层次依次将树中节点存储到数组即可。
例如,存储图 2 所示的完全二叉树,其存储状态如图 3 所示:
我们在讲优先级队列时仔细讲解
链式存储
其实二叉树并不适合用数组存储,因为并不是每个二叉树都是完全二叉树,普通二叉树使用顺序表存储或多或少会存在空间浪费的现象。
常见的链式存储方式:
// 孩子表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
}
// 孩子双亲表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
Node parent; // 当前节点的根节点
}
我们主要用孩子表示法来说明。
2.6 二叉树的遍历
遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题(比如:打印节点内容)
我们用这颗树来组织;
public class BinaryTree {
static class TreeNode {
private char val; // 节点值
private TreeNode left; // 左结点的引用
private TreeNode right; // 右结点的引用
public TreeNode(char val) {
this.val = val;
}
}
public TreeNode root;
//简单的构建二叉树
public void createBinaryTree(){
TreeNode A = new TreeNode('A');
TreeNode B = new TreeNode('B');
TreeNode C = new TreeNode('C');
TreeNode D = new TreeNode('D');
TreeNode E = new TreeNode('E');
TreeNode F = new TreeNode('F');
TreeNode G = new TreeNode('G');
TreeNode H = new TreeNode('H');
A.left = B;
A.right = C;
B.left = D;
B.right = E;
C.left = F;
C.right = G;
E.right = H;
root = A;
}
}
前序遍历(先序遍历)
实现思想:访问根结点—>根的左子树—>根的右子树
动态图解:先序遍历就是小人从根结点开始,依次遍历得到结点值,最后回到原点
void preOrder(TreeNode root){
//递归结束条件:结点为空
if (root == null) {
return;
}
System.out.print(root.val + " ");
preOrder(root.left);
preOrder(root.right);
}
中序遍历
实现思想:根的左子树—>根节点—>根的右子树
动态图解:中序遍历的结果就是把结点依次投影。
void inOrder(TreeNode root){
if (root == null) {
return;
}
preOrder(root.left);
System.out.print(root.val + " ");
preOrder(root.right);
}
后序遍历
实现思想:根的左子树—>根节点—>根的右子树
动态图解:后序遍历就像剪葡萄,只能一个个剪,按照掉落顺序即为后序遍历结果。
void postOrder(TreeNode root){
if (root == null) {
return;
}
preOrder(root.left);
preOrder(root.right);
System.out.print(root.val + " ");
}
层序遍历
实现思想:从根结点开始,从左到右,至上而下的访问每一个结点。
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);
}
}
}
2.7二叉树的基本操作
获取树中节点的个数
int nodeCount = 0;
// 获取树中节点的个数
int size1(TreeNode root) {
if (root == null) {
return 0;
}
nodeCount++;
size1(root.left);
size1(root.right);
return nodeCount;
}
//子问题求解
int size(TreeNode root){
return root == null ? 0 : size(root.left) + size(root.right) + 1;
}
获取叶子节点的个数
// 遍历 = 求叶子结点个数
static int leafCount = 0;
void getLeafNodeCount(TreeNode root) {
if(root == null) {
return;
}
if(root.left == null && root.right == null) {
leafCount ++;
}
getLeafNodeCount(root.left);
getLeafNodeCount(root.right);
}
// 子问题思路-求叶子结点个数
int getLeafNodeCount2(TreeNode root) {
if(root == null) {
return 0;
}
if(root.left == null && root.right == null) {
return 1;
}
return getLeafNodeCount2(root.left) + getLeafNodeCount2(root.right);
}
获取第K层节点的个数
// 获取第K层节点的个数
int getKLevelNodeCount(TreeNode root, int k){
if (root == null || k <= 0) {
return 0;
}
return k == 1 ? 1 : getKLevelNodeCount(root.left, k - 1) + getKLevelNodeCount(root.right, k - 1);
}
获取二叉树的高度
// 获取二叉树的高度
int getHeight(TreeNode root){
if (root == null) {
return 0;
}
int leftMax = getHeight(root.left);
int rightMax = getHeight(root.right);
return Math.max(leftMax, rightMax) + 1;
}
检测值为value的元素是否存在
// 检测值为value的元素是否存在
TreeNode find(TreeNode root, char val){
if (root == null) return null;
if (root.val == val) {
return root;
}
TreeNode ret = find(root.left, val);
if (ret != null) {
return ret;
}
ret = find(root.right, val);
if (ret != null) {
return ret;
}
return null;
}
判断一棵树是不是完全二叉树
boolean isCompleteTree(TreeNode root){
if (root == null) {
return true;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (node != null) {
queue.offer(node.left);
queue.offer(node.right);
} else {
break;
}
}
while (!queue.isEmpty()) {
TreeNode tmp = queue.peek();
if (tmp != null) {
return false;
}
queue.poll();
}
return true;
}