多叉树:判断某个子树是否属于某个父树,如果属于,从父树中删除子树

如果只想直接看算法的,可以跳到第3个标题;其他几个标题的内容作为背景铺垫,介绍为什么要写这个算法,以及可以解决什么实际问题,解决之后的效果如何。因为如果不联系实际,一些人会认为树之类的数据结构没什么用武之地,太抽象。结合其他几个标题的内容可以帮助理解。

1 问题背景分析

最近在公司做一个知识库分享平台的用户权限的后台功能,大概是这样的:
权限分为很多级,如下图所示:

这里写图片描述

当然所有权限不止这些,分类下面还有软件,软件下面还有版本……资源树太大,我就不全画出来了。

举个例子来说明上图。假设一个用户A,他有主题2的所有权限,那么他肯定也拥有主题2下面的分类3和分类4的所有权限,即拥有了一棵子资源树;同时假设A用户还拥有academy这棵子资源树的所有权限。也就是说,一个用户的资源树,很可能有很多棵。对于全局管理员B,他当然只有一棵完整的资源树。

下面说一下赋权列表,一个用户(master)给另一个用户(slave)赋权的时候,首先得到master的所有权限的资源树(可能一棵,也可能很多棵),然后得到slave用户的所有资源树。在master用户的赋权界面选择slave用户的id之后,从用户体验的角度来讲,我们希望只显示master可以赋给slave的权限,即master拥有,而slave没有的资源,master用户通过给这些权限打勾来赋权。也就是要把二者有交集的资源树做减法。

那么在后台需要实现的功能就是:根据用户id得到二者的所有资源树,下一步,得到二者资源树的交集(可能为空),判断master的某个资源树是否是某个交集树的父级,如果是则删除,返回删除后的不完全树;如果不是,则直接返回master的该资源树。

语言描述还是太繁琐,用下面的图进行表示:无论master和slave的资源树有没有交集,三种情况下,最终返回的树都是蓝色部分。

这里写图片描述

2 从数据库得到资源树

Resource.java

public class Resource implements java.io.Serializable{
    private static final long serialVersionUID = -4790317033687903765L;

    private Integer resourceId;
    private Integer parentId;
    private String resourceType;
    private String resourceUrl;
    private String resourceName;
    private String remark;
    private Boolean isDeleted;
    private Date createdTime;
    private Date modifiedTime;
    //省略get和set方法
}

显然这个resource是没法直接拿来生成树的,需要做一个类
ResourceExt.java

/**
 * 简化的资源树
 *
 * @author wujiang
 * @version 1.0.0.
 * @date 2016/9/19
 */
public class ResourceExt implements Serializable{

    private Resource resource;
    //关于该资源名称的描述,我在另一个类中写了生成方法,这里省略
    private String description;
    private List<ResourceExt> childrenList;
    //省略get和set方法

现在需要使用迭代将数据库中查出的Resource组成的List转化为资源树
注意,这里的

    /**
     * 将List<Resource>转换为List<ResourceExt>
     */
    public List<ResourceExt> ResourcetoExt(List<Resource> resourceList){
        List<ResourceExt> resourceExtList = new ArrayList<ResourceExt>();
        List<Resource> childList = new ArrayList<Resource>();

        for(Resource rs:resourceList){
            ResourceExt re = new ResourceExt();
            re.setResource(rs);
            Integer it = rs.getResourceId();
            re.setDescription(descriptionWriter.getDescription(it));
            childList = resourceManager.getSubResource(rs.getResourceUrl());
            if(childList!=null){
                re.setChildrenList(this.ResourcetoExt(childList));
            }else re.setChildrenList(null);
            resourceExtList.add(re);
        }
        return resourceExtList;
    }

注意,这里的List< Resource >并不是某个用户所有权限组成的List,而是他所有资源树的顶点资源组成的List。
取出这个List的方法很简单,就是从数据库中查出他的所有资源,将每个资源的Id放入一个List< Integer >中,遍历,如果哪个Resource的parentid不在这个List中,就把该Resource设置为顶点Resource。

至于如何取到master和slave的公共权限以及master拥有而slave没有的权限呢?将两个用户的资源id得到的List(因为资源id不会重复,所以可以看做是set),求交集即可。代码如下:

    /**
     * 得到master和slave两个用户的公共权限
     */
    public List<Resource> getCheckedList(List<Integer> masterIdList,List<Integer> slaveIdList){
        List<Integer> tempList = new ArrayList<Integer>();
        tempList.addAll(masterIdList);
        tempList.retainAll(slaveIdList);
        return this.IdtoResource(tempList);
    }

    /**
     * 得到master拥有而slave没有的权限,即master可以赋给slave的权限
     */
    public List<Resource> getUncheckedList(List<Integer> masterIdList,List<Integer> slaveIdList){
        List<Integer> tempList1 = new ArrayList<Integer>();
        tempList1.addAll(masterIdList);
        tempList1.removeAll(slaveIdList);
        return this.IdtoResource(tempList1);
    }

    /**
     * 根据idList得到List<Resource>
     */
    public List<Resource> IdtoResource(List<Integer> list){

        List<Resource> resourceList = new ArrayList<Resource>();
        for(Integer i:list){
            resourceList.add(resourceManager.selectByPrimaryKey(i));
        }
        return resourceList;
    }

将这两个得到的List取出头结点,然后按照ResourceToExt方法就可以得到需要的若干棵父类资源树List< ResourceExt > masterExtHeads, 和子类资源树List< ResourceExt > slaveExtHeads。 这两个资源树集合是我们在第三部分中要进行处理资源树的集合。

3 判断某个子树是否属于某个父树,如果属于,从父树中删除子树

这里的大概思路是:想要从父资源树中删除子资源树,首先判断子资源树的顶点资源是否是父资源树顶点资源的后代,用一个IteratorHelper的类来记录,如果是,则返回true,并且将该节点的父类资源记录下来,然后进行删除操作。否则,返回false和null。

/**
 * @author wujiang
 * @version 1.0.0.
 * @date 2016/9/27
 */
public class IteratorHelper {
    private boolean flag;
    private ResourceExt parentResourceExt;

    public IteratorHelper(boolean flag, ResourceExt parentResourceExt) {
        this.flag = flag;
        this.parentResourceExt = parentResourceExt;
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public ResourceExt getParentResourceExt() {
        return parentResourceExt;
    }

    public void setParentResourceExt(ResourceExt parentResourceExt) {
        this.parentResourceExt = parentResourceExt;
    }
}

删除的方法是,首先找到想要删除的资源的父节点,遍历其所有子资源节点,如果id与想要删除的相同,就删除。为什么想要删除子树,删除其头结点就可以?因为在JVM中,一旦该子树的头结点被删除,之后的所有后代都会因为没有被引用而被当做垃圾回收掉。所以不需要再迭代删除子树中的每一个子节点。

/**
 * ListProcessor处理器
 *
 * @author wujiang
 * @version 1.0.0.
 * @date 2016/9/19
 */
@Service
public class ListProcessor {

    /**
     * 通过传入的两个list得到AuthorizeMap
     *
     * @param masterList
     * @param slaveList
     * @return AuthorizeMap
     */
     //传入的参数就是第二部分中得到的若干父资源树和子资源树的集合
    public List<ResourceExt> getAuthorizeMap(List<ResourceExt> masterExtHeads, List<ResourceExt> slaveExtHeads) {

        for (ResourceExt masterExt : masterExtHeads) {
            for (ResourceExt slaveExt : slaveExtHeads) {
                if (this.traverseTree(masterExt, slaveExt.getResource().getParentId()).isFlag()) {
                    this.deleteNode(masterExt, slaveExt);
                }
            }
        }

        return masterExtHeads;
    }

    /**
     * 从父资源树的顶点中序遍历一棵树
     * 如果找到某个想要删除的资源的父资源,返回true并得到该父类资源;否则返回false,父类资源为空
     * 目的是根据parentid找到对应的ResourceExt
     */
    public IteratorHelper traverseTree(ResourceExt headResource, Integer parentId) {
        IteratorHelper iteratorHelper = new IteratorHelper(false, null);
        if (headResource.getResource().getResourceId().equals(parentId)) {
            iteratorHelper.setFlag(true);
            iteratorHelper.setParentResourceExt(headResource);
        } else {
            List<ResourceExt> childrenList = headResource.getChildrenList();
            if (childrenList != null && childrenList.size() > 0) {
                for (ResourceExt rs : childrenList) {
                    if (iteratorHelper.isFlag()) {
                        return iteratorHelper;
                    }
                    iteratorHelper = this.traverseTree(rs, parentId);
                }
            }
        }
        return iteratorHelper;
    }

    /**
     * 删除节点及其下的所有子节点
     *
     * @param
     * @return
     */
    public void deleteNode(ResourceExt headExt, ResourceExt slaveResource) {
        //得到slaveResource的父ResourceExt
        ResourceExt parentExt = this.traverseTree(headExt, slaveResource.getResource().getParentId()).getParentResourceExt();

        Integer toDeleteId = slaveResource.getResource().getResourceId();

        if (parentExt != null) {
            this.deleteChildNode(parentExt, toDeleteId);
        }
    }

    /**
     * 相当于通过该资源的上一级父类资源删除其本身
     *
     * @param
     * @return
     */
    public void deleteChildNode(ResourceExt parentExt, Integer toDeleteId) {
        List<ResourceExt> childList = parentExt.getChildrenList();
        for (ResourceExt re : childList) {
            if (re.getResource().getResourceId().equals(toDeleteId)) {
                childList.remove(re);
                break;
            } else
                continue;
        }
    }
}

4 前端效果展示

其实前面我省略了很多步骤,只是把最关键的几步放上来了,诸如数据库因为高度不冗余,无法直接查出某个用户全部资源之类的坑我都遇到了。。。。总之就是遇到问题解决问题吧。直接按照上面的代码跑不一定能跑通,因为我删除了一些代码只留下最关键的,给需要的人提供一些解决问题的思路。代码稍作修改应该就可以使用。

我把最终生成的树返回给前端,在页面上就会有如下的显示效果:
当一个用户输入另一个用户的userId想要给他赋权时,前端就会这样显示:

这里写图片描述

页面中,你并不知道被赋权的人有哪些权限,你只知道你可以赋给他哪些权限,并且是按照权限的层级树状展开的

说明
本人水平有限,不当之处希望各位高手指正。另外,文中的插图都是我自己在word的smart art中绘制的,看起来不精致请见谅。
如有转载务必注明出处
http://blog.csdn.net/antony9118/article/details/52704457
谢谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值