【数据结构】-树及森林 菜单实现 深度优先 广度优先 递归遍历

2021年11月20日 3点热度 0条评论 来源: 老汉健身

前言:树是一种非常有趣的数据结构,在大学时期有学过,但缺乏实际运用场景,所以学完后来就忘得差不多了...但工作以后才发现,树这种数据结构是那么重要和常见,用得好的话可以让你代码更优雅,性能更佳,为了让树里面的概念更易于理解,关于树的定义这块我尽量通俗,牺牲一些标准性,提高可读性,不至于像读大学课本上对树的定义那样难懂.

1.基本概念

1.1定义

树:是由结点或顶点和边组成的(可能是非线性的)且不存在着任何环的一种数据结构。没有结点的树称为空(null或empty)树。一棵非空的树包括一个根结点,还(很可能)有多个附加结点,所有结点构成一个多级分层结构。

说白话:树是由根节点和树枝节点(暂且这么叫..)和叶子节点共同构成的一种数据结构,树节点之间不存在闭环,每棵树有且仅有一个根节点.直接来张图:

1.2常见术语

根:树的顶点,从它开始可以向下找到任意一个节点,但向上找除了它本身没有任何节点,对应上图中的A节点.

叶子节点:没有孩子的节点就是叶子节点了,对应图中的KLM.

其它非根非叶子节点我一般叫它普通节点,当然不必纠结这些,只要知道根和叶子即可...

森林:多棵树组合在一起就是一个森林,当然如果树的高度>1,把这棵树的根去掉,其它节点所组成的就是一个森林.

森林如下图所示:

高度/深度:树中结点的最大层次

2.树的建立

直接上代码吧,为了代码简洁易懂,暂不考虑NPE:

树节点对象,当然你也可以用Map来封装.

public class Tree {
    /**
     * 当前节点的id
     */
    private Long id;
    /**
     * 当前节点的名字
     */
    private String name;
    /**
     * 当前节点对应父亲节点的id
     */
    private Long parentId;
    /**
     * 当前节点的孩子节点列表
     */
    private List<Tree> children;

    public Tree(Long id, String name, Long parentId) {
        this.id = id;
        this.name = name;
        this.parentId = parentId;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getParentId() {
        return parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    public List<Tree> getChildren() {
        return children;
    }

    public void setChildren(List<Tree> children) {
        this.children = children;
    }

    @Override
    public String toString() {
        return "Tree{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", parentId=" + parentId +
            '}';
    }
}

 建树工具类:

public class TreeBuilder {
    /**
     * 根节点列表
     */
    private List<Tree> rootList;
    /**
     * 非根节点列表 当然也可以包含根 不影响
     */
    private List<Tree> bodyList;

    public TreeBuilder(List<Tree> rootList, List<Tree> bodyList) {
        this.rootList = rootList;
        this.bodyList = bodyList;
    }

    public List<Tree> build() {
        Map<Long, Long> filterOperated = new HashMap<>(rootList.size() + bodyList.size());
        //对每个根节点都封装它的孩子节点
        rootList.forEach(root -> setChildren(root, filterOperated));
        return rootList;
    }

    private void setChildren(Tree root, Map<Long, Long> filterOperated) {
        List<Tree> children = new ArrayList<>();
        bodyList.stream()
            //过滤出未操作过的节点
            .filter(body -> !filterOperated.containsKey(body.getId()))
            //过滤出孩子节点
            .filter(body -> Objects.equals(root.getId(), body.getParentId()))
            .forEach(body -> {
                filterOperated.put(body.getId(), root.getId());
                children.add(body);
                //递归 对每个孩子节点执行同样操作
                setChildren(body, filterOperated);
            });
        root.setChildren(children);
    }
}

测试类: 

public class Client {
    public static void main(String[] args) {
        Tree tree1 = new Tree(1L,"A",0L);
        Tree tree2 = new Tree(2L,"B",1L);
        Tree tree3 = new Tree(3L,"C",1L);
        Tree tree4 = new Tree(4L,"D",0L);
        Tree tree5 = new Tree(5L,"E",4L);
        Tree tree6 = new Tree(6L,"F",4L);
        Tree tree7 = new Tree(7L,"G",4L);
        Tree tree8 = new Tree(8L,"H",5L);
        Tree tree9 = new Tree(9L,"I",5L);
        Tree tree10 = new Tree(10L,"J",9L);
        List<Tree> rootList = new ArrayList<>();
        List<Tree> bodyList = new ArrayList<>();
        rootList.add(tree1);
        rootList.add(tree4);
        bodyList.add(tree2);
        bodyList.add(tree3);
        bodyList.add(tree5);
        bodyList.add(tree6);
        bodyList.add(tree7);
        bodyList.add(tree8);
        bodyList.add(tree9);
        bodyList.add(tree10);
        TreeBuilder treeBuilder = new TreeBuilder(rootList,bodyList);
        List<Tree> treeList = treeBuilder.build();
        System.out.println(treeList);
    }
}

运行结果:

 我一共建立了两棵树,如图:

手绘的,有点丑,将就着看吧...

3.树的几种遍历方法

树建好了,那么如何访问树中每一个节点? 方法有很多种,最常见,听的最多的就是深度优先和广度优先遍历,两种遍历方式各有各的应用场景,我在实际开发中最近正好有碰到,所以再拿出来分享一下.

3.1广度优先遍历

广度优先也就是树的遍历是横向的,逐层遍历,先把高度为1的节点全部访问完,再访问高度为2的节点,以此类推...

广度优先的遍历需要利用队列先进先出的特性,以我建立的树为例,访问顺序是:A->B->C D->E->F->G->H->I->J

下面用代码来演示:

public class TreeIterator {
    /**
     * 广度优先遍历森林
     *
     * @param treeList
     */
    public void broadFirst(List<Tree> treeList) {
        treeList.forEach(tree -> broadFist(tree));
    }

    /**
     * 广度优先遍历树
     *
     * @param tree
     */
    private void broadFist(Tree tree) {
        Queue<Tree> queue = new LinkedBlockingQueue<>();
        queue.add(tree);
        while (!queue.isEmpty()) {
            Tree firstNode = queue.poll();
            System.out.print(firstNode.getName() + "->");
            if (firstNode.getChildren() != null && firstNode.getChildren().size() > 0) {
                queue.addAll(firstNode.getChildren());
            }
        }
    }
}

 Client类中其它代码保持不变,新增下面两行:

        TreeIterator iterator = new TreeIterator();
        iterator.broadFirst(treeList);

运行结果:

 

3.2深度优先遍历

深度优先对树的遍历是纵向的,是一条线走到底,从根节点开始一直找到叶子节点,然后再换另一条线,直到所有孩子都被访问过为止.

深度优先的遍历需要利用栈的先进后出特性来实现,以我在上面建立的树为例,访问顺序为:A->C->B D->G->F->E->I->J->H

代码:

在TreeIterator类中新增以下两个方法:

    /**
     * 深度优先遍历森林
     *
     * @param treeList
     */
    public void deepFirst(List<Tree> treeList) {
        treeList.forEach(tree -> deepFirst(tree));
    }

    /**
     * 深度优先遍历树
     *
     * @param tree
     */
    private void deepFirst(Tree tree) {
        Stack<Tree> stack = new Stack<>();
        stack.add(tree);
        while (!stack.isEmpty()) {
            Tree firstNode = stack.pop();
            System.out.print(firstNode.getName() + "->");
            if (!firstNode.getChildren().isEmpty()) {
                for (Tree child : firstNode.getChildren()) {
                    stack.push(child);
                }
            }
        }
    }

然后在测试类Client中新增:

        TreeIterator iterator = new TreeIterator();
        //iterator.broadFirst(treeList);
        iterator.deepFirst(treeList);

 运行结果如图:

当然对树的遍历除了我上面这种方式,你还可以采用递归,不过如果树的高度比较高,数据量比较大的话,建议不要递归,容易StackOverFlow.

4.业务场景

前面提到的所有概念,建树方法,以及遍历方法最终都是为了具体的业务场景,在日常开发中,树可以做很多事情,特别是在查找方面,树在查询的时间复杂度上具有一定优势(不是所有树都有),所以在Mysql中就使用BTree作为查询索引,再比如Jdk1.8中Doug lea在HashMap中引入红黑树作为在Hash碰撞后单个Hash桶中元素超过8个时的数据存放结构,使查询复杂度从O(n)降至Olog(n). 所以当在业务场景中遇到需要自己维护查询,并要求较高的查询性能时,可以优先考虑使用树作为数据存储的结构,虽然现实中这种场景并不多,除非是大厂的中间件,小公司一般用用别人写好的开源框架或中间件就够了.

更常见的场景是,类似于菜单这种具有展开(父子)层级结构的场景,甚至是地址:中国->浙江->杭州->余杭区->xxx路  都可以用到树这种数据结构,可以让你代码可读性更高,减少更多代码开发量和重复.

下面以一个实际的例子来分别演示下使用树和不使用树在开发上会有什么区别:

就以地址为例,如果不使用树这种数据结构,在建数据库表时就需要对国籍,省,市,区,路分别建表,一共需要建立五张表(当然你也可以只建1张表,但查询起来不友好),然后在封装VO时,你的数据结构是这样的:

国籍:

public class CountryVO {
    //省略get set
    private Long id;
    private String countryName;
    private List<ProvinceVO> provinceVOList;
}

省:

public class ProvinceVO {
    private Long id;
    private String provinceName;
    private List<CityVO> cityVOList;
}

区和路雷同...我就不写下去了,这样的结构首先在查询和封装时就非常吃力,其次你需要把内容几乎相似的VO重复定义多次,对前端而言也是同样的工作量,太不友好了...

那如果是用树这种结构来做的话,建表你只需要建立一张统一的表,通过parentId把具有父子关系的省市区路相互关联起来.

然后把树建好之后,就可以直接把树丢给前端,问题就解决了!

VO就拿我在上面演示时候建的Tree就可以满足了:

        //实际开发中这些数据来源于SQL
        Tree tree1 = new Tree(1L,"中国",0L);
        Tree tree2 = new Tree(2L,"浙江省",1L);
        Tree tree3 = new Tree(3L,"江苏省",1L);
        Tree tree4 = new Tree(4L,"杭州市",2L);
        Tree tree5 = new Tree(5L,"宁波市",2L);
        Tree tree6 = new Tree(6L,"苏州市",3L);
        Tree tree7 = new Tree(7L,"常州市",3L);
        Tree tree8 = new Tree(8L,"余杭区",4L);
        Tree tree9 = new Tree(9L,"聚橙路",8L);
        Tree tree10 = new Tree(10L,"姑苏区",6L);

 直接从数据库里查出来,然后建树,返回,几行代码搞定,前端拿到这些数据也可以通过递归简洁优雅的渲染到页面,测试结果如图:

当然在实际开发中可能还有一些场景必须得通过树这种数据结构来解决,比如我近期开发的一个项目,简单描述下:

除了要建立树的层级结构以外,还需要对树的每个节点按顺序编号,在导出时按照顺序打印路径,以便在Excel这种平面展示里也能知道树的层级结构:

比如我的第一级的第一项是开发工程,第二级的第一项是主体建安工程,第五级是精装修工程,那么1->1->5 的含义就是

开发工程->主体建安工程->精装修工程,如果没有这串数字,谁搞的清楚精装修工程到底是哪个明细下的?

除此之外,树中每个父亲节点的金额只能通过叶子节点的加总算出,如果当前节点已经是叶子节点,那无需计算,可以从库里直接查出.

以上所有这些内容和操作,必须对掌握树这种数据结构,否则寸步难行,而且树里面的大部分操作都是要递归或者使用队列/栈的,不了解树和递归,很容易被绕进去,而且算法需要考虑性能,之所以开此篇除了分享也是为了加固自己对树这种数据结构的掌握程度,以便在以后遇到类似树的场景能够快速上手以及写出最佳性能的代码.

    原文作者:老汉健身
    原文地址: https://blog.csdn.net/lovexiaotaozi/article/details/102718254
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。