js遍历树节点下的所有子节点_【数据结构与算法】(3)——树和二叉树

树的基本概念

树是一种非线性的数据结构,样子如图所示:

798a90a96236364c771cae608d1e2780.png

树的主要特点是树中的数据是分层存储的,每个元素称为树的节点,最顶层有且只有一个元素,称为根节点,其余层可以有任意数量的节点。除了根节点,其余每一个节点都与上层的一个节点相连,形状类似一倒过来的树。

树中的几个基本概念

  • 根节点:最顶层的节点,也就是图中的节点0。

  • 父节点:某一个节点的上层节点,比如图中节点4的父节点就是节点1,根节点没有父节点。

  • 孩子节点:某一个节点的下层节点,比如节点2的子节点就是节点6和7。

  • 兄弟节点:有共同父节点的一组节点,比如节点1,2,3的父节点都是节点0,所以节点1,2,3为兄弟节点。

  • 叶子节点:没有孩子节点的节点,图中的叶子节点是4,5,6,7,8。

  • 树的深度:从根节点到最底层叶子节点所经过的节点数。通俗的说就是树有多少层,比如图中树的深度就是3。(有的地方习惯从0开始计算,也就是根节点的深度为0,这里其实就不用太纠结了,重要的是知道深度是用来描述树的层数就足够了)。

  • 树的高度:类似树的深度,也是描述树的层数,只是定义的方式有一点点不同,是从叶子节点到根节点计算的,所以这里也不用太纠结这两个概念的区别了。

  • 节点的度:节点的子节点个数,比如图中节点1的度为2。

二叉树

1. 二叉树的基本概念

二叉树中,每个节点最多有两个子节点,是一种特殊的树,如下图

9ff993717ff93551b7a14a6e22de718b.png

几种特殊的二叉树

  • 满二叉树:满二叉树是指除了叶子节点,其他每个节点都有两个孩子,且所有叶子节点都在同一层,下图就是一个简单的满二叉树

    6ee5a8fa71c183db66b5fb82d870359a.png

  • 完全二叉树:完全二叉树通俗的讲就是同样层数的满二叉树,从右侧开始扣去几个叶子节点,如下图左侧是一棵完全二叉树,右侧就不是

    dfce1b2de4cd2c5df3edd04823c2588c.png

二叉树的几个性质

  1. 二叉树的第k层,最多有2^(k-1)个节点

  2. 在k层的满二叉树中,节点数n与k的关系 n=(2^k)-1,也可以写作

    k=log2(n+1)

  3. 具有n个节点的完全二叉树,高度k=[log2n]+1,[log2n]是指向下取整

这几条性质通过前面几张图就可以简单的验证,还有一些其他的因为用的比较少就没列出来。其中log形式的公式比较重要,在之后计算某些搜索和排序算法的复杂度时会用到。

2、二叉树的存储

基于数组的存储

简单来说,就是按照从上到下,从左到右的顺序为二叉树每个节点编号,编号为对应的数组索引,数组中的值就是每一个节点的值

148bd91ff47caf091d30531641059d08.png

对于节点为空的位置,也要记录该位置的索引,但是数组的值为空

29dc63f03dc86f899317306bb746717e.png

所以可以看到,完全二叉树的数组存储不会有空位置,而非完全二叉树数组中会有空值。

基于链式存储

二叉树每个节点需要记录这么几个信息;节点具体的值,左孩子和右孩子,基于此每个节点的数据结构定义如下

public class TreeNode {    Object val; //具体数据    TreeNode leftChild; //左孩子指针    TreeNode rightChild; //右孩子指针}

简单的示例如下

979fdf25ae686281c822e62557a79c4a.png

3、二叉树的遍历

二叉树的遍历是指根据某个规则,依次访问每个节点,主要有前序遍历,中序遍历,后序遍历和层次遍历4种。遍历结果以下图的树为例

9ff993717ff93551b7a14a6e22de718b.png

所有的遍历都是以根节点为起点,且按照先左后右的方式

前序遍历

前序遍历是指遍历到一个节点时就访问它,然后访问它的左子节点,左侧访问完再访问右子节点

图上的树对应前序遍历为: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树等这几种树多用在搜索算法中,会在以后的文章中再写到。

总的来说二叉树是一种非常重要的数据结构,在很多应用的底层都有用到,也是每次都会被面试官怼的必考点。

未完待续......

下期预告:

【数据结构与算法】-- 哈希表

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值