相信大家对于树都不会太陌生,今天就来简单聊聊关于数据结构中的树的那些东西吧~
一、树的简单介绍
1.1 什么是树?
先来看看百度百科上对树的解释
树状图是一种数据结构,它是由n(n >= 1) 个有限结点组成一个具有层次关系的集合
把它叫做 “树” 是因为它看起来像一颗倒挂的树,也就是说它是根朝上,而叶朝下
它大多长这样:
再来看看一些结点的称呼或是概念性的东西
- A 结点是 B 和 C 结点的上级,也就是说 A 结点是 B 和 C 结点的
父节点,B 和 C 结点是 A 结点的子结点 - B 和 C 结点同时是 A 结点的子结点,称为
兄弟结点 - A 结点没有父节点,称为
根节点 - D 、E 和 F 结点没有子结点,称为
叶子节点 树深即为树中结点的最大层次数(也称高度,深度),如图深度即为 3
可以看出,树由根结点和若干颗子树构成,换句话说,剔除一颗树的根节点后,它的子结构也满足树的特性:
- 每个结点有零个或多个结点
- 每个非根结点有且仅有一个父节点
- 树里面没有环路
提到树就不得不说起二叉树了
1.2 什么是二叉树
二叉树:每个结点最多含有两个结点的树,两个结点分别为左子结点和右子结点
来看看一个二叉树的简单实现
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
二叉树当中也有几种特殊的类型:
- 满二叉树:所有结点都有两个子结点(当然除开最后一层的叶子结点)
- 完全二叉树:除最后一层结点个数都达到最大,并且最后一层的叶子结点都往左排列
(从定义可以发现,一个满二叉树一定是完全二叉树)
之所以会称为完全二叉树,是从它存储空间利用率来看的
比如上面的完全二叉树:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| A | B | C | D | E | F | G | H |
如果这里是一颗非完全二叉树,则会浪费比较多的存储空间:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| A | B | C | D | E | F | ||
| 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| G | H | I |
-
二叉搜索树:满足下列约束条件:
- 若左子树不为空,则左子树上所有结点的值均小于它的根节点的值
- 若右子树不为空,则右子树上所有结点的值均大于它的根节点的值
- 左、右子树也必须是二叉搜索树
二叉查找树中,会尽可能避免两个结点数值相等的情况
二叉查找树也有几种优化,例如 AVL 树、红黑树、哈夫曼树 ... 之后再做介绍
二、二叉树的遍历方式
二叉树的遍历是指从根结点触发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问且仅被访问一次
二叉树的遍历方式大体上有四种经典方式:前序遍历、中序遍历、后序遍历、层序遍历
其中前序、中序和后序遍历本质上可以认为是深度优先遍历(DFS),而二叉树的层序遍历本质上可以认为是广度优先遍历(BFS),这里不作详细介绍,想了解的朋友可以期待下一篇文章哦~
DFS 简单来说就是对每一个可能的分支路径深入到最底的遍历方式;而 BFS 简单来说就是一层一层由内而外的遍历方式
-
前序遍历:对于树中的任意结点,先打印这个结点,然后前序遍历它的左子树,最后前序遍历它的右子树
-
中序遍历:对于树中的任意结点,先中序遍历它的左子树,然后打印这个结点,最后中序遍历它的右子树
-
后序遍历:对于树中的任意结点,先后序遍历它的左子树,然后 后序遍历它的右子树,最后打印这个结点
(也就是说,这里的序指的是父节点的遍历顺序)
实现前序、中序和后序遍历普遍用的还是递归,个人觉得递归相比非递归的方法更妙一些~,下面贴出代码片段方便大家理解
其实这三种遍历方式有很大的相同之处,正如它们的定义一般
2.1 前序遍历
public static void preOrderTraversal(TreeNode head) {
if (head == null) {
return;
}
System.out.print(head.val + " ");
preOrderTraversal(head.left);
preOrderTraversal(head.right);
}
2.2 中序遍历
public static void inOrderTraversal(TreeNode head) {
if (head == null) {
return;
}
inOrderTraversal(head.left);
System.out.print(head.val + " ");
inOrderTraversal(head.right);
}
这里额外提一下二叉查找树的中序遍历:
对二叉查找树进行中序遍历,就可以输出一个按数值从小到大的有序数据队列(这里就直接拿上面的图吧,偷懒~)
比如这里就会打印出:9、13、15、16、18、21、23、25
2.3 后序遍历
public static void postOrderTraversal(TreeNode head) {
if (head == null) {
return;
}
postOrderTraversal(head.left);
postOrderTraversal(head.right);
System.out.print(head.val + " ");
}
2.4 层序遍历
层序遍历就像上面所提到的那样,即逐层的、从左到右访问所有结点
层序遍历的实现可以使用队列的特性,把每个还没有访问到的结点依次放入队列,然后再弹出队列的头部元素当作是当前的结点:
public static void levelOrder(TreeNode head){
Queue<TreeNode> queue = new LinkedList<>();
queue.add(node);
while (!queue.isEmpty()){
TreeNode cur = queue.poll();
if(cur.left != null) {
queue.add(cur.left);
}
if(cur.right != null) {
queue.add(cur.right);
}
}
}
使用队列保存每层的所有结点,每次把队列里原先的所有结点进行出队列操作,再把每个元素的非空左右子节点入队,即可得到每层的遍历
时隔五个月多自己又有了写博客的想法,希望自己能一直坚持下去,另外,朋友们的点赞和关注是对我最大的支持 :D