树
树的基本概念
树是一种非线性的数据结构,样子如图所示:
树的主要特点是树中的数据是分层存储的,每个元素称为树的节点,最顶层有且只有一个元素,称为根节点,其余层可以有任意数量的节点。除了根节点,其余每一个节点都与上层的一个节点相连,形状类似一倒过来的树。
树中的几个基本概念
根节点:最顶层的节点,也就是图中的节点0。
父节点:某一个节点的上层节点,比如图中节点4的父节点就是节点1,根节点没有父节点。
孩子节点:某一个节点的下层节点,比如节点2的子节点就是节点6和7。
兄弟节点:有共同父节点的一组节点,比如节点1,2,3的父节点都是节点0,所以节点1,2,3为兄弟节点。
叶子节点:没有孩子节点的节点,图中的叶子节点是4,5,6,7,8。
树的深度:从根节点到最底层叶子节点所经过的节点数。通俗的说就是树有多少层,比如图中树的深度就是3。(有的地方习惯从0开始计算,也就是根节点的深度为0,这里其实就不用太纠结了,重要的是知道深度是用来描述树的层数就足够了)。
树的高度:类似树的深度,也是描述树的层数,只是定义的方式有一点点不同,是从叶子节点到根节点计算的,所以这里也不用太纠结这两个概念的区别了。
节点的度:节点的子节点个数,比如图中节点1的度为2。
二叉树
1. 二叉树的基本概念
二叉树中,每个节点最多有两个子节点,是一种特殊的树,如下图
几种特殊的二叉树
满二叉树:满二叉树是指除了叶子节点,其他每个节点都有两个孩子,且所有叶子节点都在同一层,下图就是一个简单的满二叉树
完全二叉树:完全二叉树通俗的讲就是同样层数的满二叉树,从右侧开始扣去几个叶子节点,如下图左侧是一棵完全二叉树,右侧就不是
二叉树的几个性质
二叉树的第k层,最多有2^(k-1)个节点
在k层的满二叉树中,节点数n与k的关系 n=(2^k)-1,也可以写作
k=log2(n+1)
具有n个节点的完全二叉树,高度k=[log2n]+1,[log2n]是指向下取整
这几条性质通过前面几张图就可以简单的验证,还有一些其他的因为用的比较少就没列出来。其中log形式的公式比较重要,在之后计算某些搜索和排序算法的复杂度时会用到。
2、二叉树的存储
基于数组的存储
简单来说,就是按照从上到下,从左到右的顺序为二叉树每个节点编号,编号为对应的数组索引,数组中的值就是每一个节点的值
对于节点为空的位置,也要记录该位置的索引,但是数组的值为空
所以可以看到,完全二叉树的数组存储不会有空位置,而非完全二叉树数组中会有空值。
基于链式存储
二叉树每个节点需要记录这么几个信息;节点具体的值,左孩子和右孩子,基于此每个节点的数据结构定义如下
public class TreeNode { Object val; //具体数据 TreeNode leftChild; //左孩子指针 TreeNode rightChild; //右孩子指针}
简单的示例如下
3、二叉树的遍历
二叉树的遍历是指根据某个规则,依次访问每个节点,主要有前序遍历,中序遍历,后序遍历和层次遍历4种。遍历结果以下图的树为例
所有的遍历都是以根节点为起点,且按照先左后右的方式
前序遍历
前序遍历是指遍历到一个节点时就访问它,然后访问它的左子节点,左侧访问完再访问右子节点
图上的树对应前序遍历为:013425
具体过程如下:
从根节点0开始,首先访问0节点
向左到达节点1并输出
节点1向左到节点3并输出
节点3没有子节点,回退到节点1
节点1向右到节点4并访问
节点4回退到节点1,此时节点1的所有子节点已被访问完,回退到节点0
节点0向右到节点2,根据之前的步骤直到访问完所有节点
前序遍历多用递归实现,如下所示
/** * @Description 前序遍历 * @Param [root -> 当前节点] **/public void preorderSearch(TreeNode root){ //判断当前节点是否为空 if(root == null){ return; } System.out.println(root.val); //访问该节点 //如果存在左子节点,向左子节点递归 if(root.left != null){ preorderSearch(root.left); } //最后遍历右子节点 if(root.right != null){ preorderSearch(root.right); }}
前序遍历也可以通过非递归的形式进行,简单来说,因为遍历顺序是先左后右,所以依次把右节点和左节点压入栈中,利用栈控制遍历的顺序
/***
* @Description 非递归前序遍历
* @Param [root -> 根节点]
**/
public void preorderSearchBystack(TreeNode root) {
Stack treeNodeStack = new Stack<>(); //记录站,为空代表遍历完毕
treeNodeStack.push(root); //首先将根节点压入栈
while (!treeNodeStack.isEmpty()) {
TreeNode treeNode = treeNodeStack.pop(); //记录出栈节点
System.out.println(treeNode.val);
//先压入右子节点,再压入左子节点,保证左节点先于右节点出栈
if (treeNode.right != null) {
treeNodeStack.push(treeNode.right);
}
if (treeNode.left != null) {
treeNodeStack.push(treeNode.left);
}
}
}
中序遍历
中序遍历是指先访问完该节点的所有左侧节点,再访问该节点,最后访问右侧节点;或者说根据访问路径,当第二次到达该节点时再访问
图上的树对应中序遍历为:314052
具体过程如下:
从根节点0开始,首先向左到节点1
节点1向左到节点3
节点3没有子节点,访问节点3
节点3回退到节点1
此时节点1的左侧已经访问完,或者第二次到达了节点1,输出节点1
节点1向右到节点4并输出
节点4回退到节点1,此时节点1的所有子节点已被访问完,回退到节点0
节点0的左侧已经访问完,此时输出节点0
节点0向右到节点2,根据之前的步骤直到访问完所有节点
采用递归方式实现中序遍历
/** * @Description 中序遍历 * @Param [root -> 当前节点] **/public void inorderSearch(TreeNode root) { //判断当前节点是否为空 if (root == null) { return; } //如果存在左子节点,向左子节点递归 if (root.left != null) { midorderSearch(root.left); } System.out.println(root.val); //访问该节点 //最后遍历右子节点 if (root.right != null) { midorderSearch(root.right); }}
中序的非递归的方式同样使用栈控制节点的访问顺序,先将父节点和所有左侧
节点压入栈中,父节点出栈后再压入右子节点。因为非递归的方式相对复杂些
具体的过程可以参考leetcode94题:
/***
* @Description 非递归中序遍历
* @Param [root -> 根节点]
**/public List inorderSearchByStack(TreeNode root) {
Stack treeNodeStack = new Stack<>();
TreeNode pointer = root;
while (! treeNodeStack.isEmpty() || pointer != null){
//不断向左侧遍历,直到当前节点最左的叶子节点
while (pointer != null){
treeNodeStack.push(pointer);
pointer = pointer.left;
}
pointer = treeNodeStack.pop();//弹出栈顶节点
System.out.println(pointer.val);
pointer = pointer.right; //指向右子节点
}
return res;
}
后序遍历
后序遍历是当该节点的所有左右侧节点都访问完,最后再访问该节点。
图上的树对应中序遍历为:341520
具体过程如下:
从根节点0开始,首先向左到节点1
节点1向左到节点3
节点3没有子节点,访问节点3
节点3回退到节点1
节点1向右到节点4并输出
此时节点1的两侧都已访问完,或者第三次到达了节点1,输出节点1
节点1回退到节点0
节点0的向右到节点2,重复上述步骤,依次输出节点5,节点2,最后节点0
后序遍历的递归方式实现
/** * @Description 后序遍历 * @Param [root -> 当前节点] **/public void postorderSearch(TreeNode root) { //判断当前节点是否为空 if (root == null) { return; } //如果存在左子节点,向左子节点递归 if (root.left != null) { midorderSearch(root.left); } //之后遍历右子节点 if (root.right != null) { midorderSearch(root.right); } System.out.println(root.val); //最后访问该节点}
利用栈实现后序遍历的方式要复杂些,因为在根节点出栈时,需要判断根节点
的左右子节点是否已经访问到,所以无法只通过一个栈实现,要么为节点设置
标志位,要么利用两个栈。
这里给出一个我的拙劣实现,利用额外的空间记录访问过的节点,其他的可以
具体参考leetcode145题
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public ListpostorderTraversal(TreeNode root) {
List res = new ArrayList<>();
if (root == null) {
return res;
}
//遍历指针
TreeNode pNode = null;
//记录栈
LinkedList nodeList = new LinkedList<>();
//记录已访问过的节点,从而将父节点出栈
Set searchNode = new HashSet<>();
//添加null节点,方便判断
searchNode.add(null);
//首先添加根节点
nodeList.add(root);
while (nodeList.size() > 0) {
//取栈顶节点,该节点可能会遍历输出,也可能需要压入自己的子节点
pNode = nodeList.getLast();
//遍历顺序为左右中,所以先右节点入栈,再左节点入栈
//判断不光需要存在子节点,还需要子节点没被遍历过
if (pNode.right != null && !searchNode.contains(pNode.right)) {
nodeList.add(pNode.right);
}
if (pNode.left != null && !searchNode.contains(pNode.left)) {
nodeList.add(pNode.left);
}
//如果该节点左右都为空或者左右都被遍历过,该节点出栈同时添加到已访问的列表
if (searchNode.contains(pNode.left) && searchNode.contains(pNode.right)) {
searchNode.add(nodeList.removeLast());
res.add(pNode.val);
}
}
return res;
}
}
层次遍历
层次遍历是指从上到下,一层一层遍历,每一层按照从左到右的顺序
图上的树对应层次遍历为012345
层次遍历主要通过队列实现,访问完父节点后,将左右子节点分别添加到队列中,在下一次迭代时出队:
/** * @Description:层次遍历 * @Param [root -> 根节点] **/public void layerSearch(TreeNode root) { Queue treeNodeQueue = new LinkedList<>(); treeNodeQueue.offer(root); //首先添加根节点 while (!treeNodeQueue.isEmpty()) { //记录出队节点并访问 TreeNode treeNode = treeNodeQueue.poll(); System.out.println(treeNode.val); //分别将当前节点的左右子节点加入队列中,注意添加的顺序 if (treeNode.left != null) { treeNodeQueue.offer(treeNode.left); } if (treeNode.right != null) { treeNodeQueue.offer(treeNode.right); } }}
4、结束语
这里主要介绍了二叉树最基础的内容,二叉树有非常多的类型,包括堆,二叉查找树,平衡二叉树,红黑树,B树等,这几种树多用在搜索算法中,会在以后的文章中再写到。
总的来说二叉树是一种非常重要的数据结构,在很多应用的底层都有用到,也是每次都会被面试官怼的必考点。
未完待续......
下期预告:
【数据结构与算法】-- 哈希表