载一棵小树苗,精心培育,总有一天会长成参天大树
                比如查找二叉、AVL、B + *、红黑……

但是,今天不种树,改成画树……

事情时这样的:在搞懂简单二叉树的过程中,经常需要验证自己的代码有没有问题,我之前的做法是“断点+肉眼观察”大法。随着节点的增多,断点还好,肉眼越来越扛不住,遂决定把树打印出来。把树的各种操作(新增/删除节点)前后进行比对,是非一目了然!

思路

clipboard.png

对于上面的树,我们已经可以从根节点遍历它了。(如果对树的基本操作还不清楚的话,可参看【树结构1】查找二叉树

直接给出遍历方式:

public void treeIterator(TwoForkTree tree){
    if(tree==null){
        return ;
    }

    treeIterator(tree.leftNode);
    System.out.print(tree.getId()+"\t");   //打印节点,这个位置是“中序遍历”
    treeIterator(tree.rightNode);
}

既然我们已经可以遍历它,那有没有方式可以记录下当前节点在第几层呢?也就是,第一层:32;第二层:20、40;第三层:35、41;第四层:38。如果可以做到,我们再按层级,一层一层的输出,不就把树打印出来了嘛!

怎么记录当前层级呢?对遍历方法稍加变动即可

public void record(TwoForkTree tree,int index){
    if(tree==null){
        return ;
    }
    index++;
    record(tree.leftNode,index);
    System.out.println(index+":"+tree.getId()+"\t");
    record(tree.rightNode,index);
}

执行结果:
clipboard.png

代码实现

接下来的事情简单了,我们把上述控制台输出的内容,用Map保存下来,再逐行输出即可。

//按层级存储节点的值
@Getter
Map<Integer,List<Integer>> layerTree = new HashMap<>();

public void record(TwoForkTree tree,int index){
    if(tree==null){
        return ;
    }
    index++;
    record(tree.leftNode,index);

    List<Integer> layerData = layerTree.get(index);
    if(CollectionUtils.isEmpty(layerData)){
        layerData = new LinkedList<>();
        layerTree.put(index,layerData);
    }
    layerData.add(tree.id);

    record(tree.rightNode,index);
}

测试以及逐行输出即可:

@Test
public void testRecord(){
    tree.record(tree,0);
    SimpleNode simpleNode = (SimpleNode) tree;
    Map<Integer,List<Integer>> layerTree = simpleNode.layerTree;

    int layerIndex=0;
    while (layerIndex<layerTree.size()){
        layerIndex++;
        List<Integer> layerData = layerTree.get(layerIndex);
        for (Integer data:layerData){
            System.out.print(data+"\t");
        }
        System.out.println();
    }
}

执行结果:
clipboard.png

改进

网上的资料大部分到这里就结束了,但看看这个产物,虽然是把树按层级打印出来了,但很多部分还需要你脑补才行。留白太大,对艺术作品还好,但学习研究还是尽可能精准的好。我想要的是,带着枝杈的树!

# 目标

   32
  /  \
20    40
     /  \
    35   41
     \
      38

怎么实现呢?遍历节点过程中,像Map中存储节点的时候,我们完全可以知道,它的子节点情况——如果有左子节点,记录一个/;如果有右子节点,记录一个\。由此,我们可以封装一个Bean。

class Printer{
    private Integer id;
    private int index;
    private String leftChildLink;
    private String rightChildLink;
}

对代码进行调整后,效果变成这样:
clipboard.png

虽然还要进行脑补,但似乎容易了些?当然,这不是结束,其实距离目标效果就差最后一步了。我们需要对数值和子节点连接符(“/”、“\”)分别存储,输出时根据上一层的位置做调整!

给出完整实现:

/**
 * 树打印
 */
public void printTree(){
    Map<Integer,List<Printer>> printMap = printTree(this,0);
    int layerIndex = 1;
    StringBuilder idBu = new StringBuilder();
    StringBuilder linkBu = new StringBuilder();
    LinkedList<Integer> nextLineIdPositions = new LinkedList<>();
    while (layerIndex<=layerTreeMap.size()){
        List<Printer> printers = printMap.get(layerIndex);
        int lastIdLen = 0;
        int lastIdPosition = 0;
        for(Printer printer:printers){
            int position;
            if(CollectionUtils.isEmpty(nextLineIdPositions)){
                position = 20;
            }else {
                position = nextLineIdPositions.removeFirst()-idLen(printer.getId())/2;
                if(position<=lastIdPosition+lastIdLen){
                    position+=idLen(printer.getId())/2;
                }
            }
            lastIdPosition = position;
            lastIdLen = idLen(printer.getId());
            appendAt(idBu,position,printer.getId()+"`");

            if(!Strings.isNullOrEmpty(printer.getLeftChildLink())
                    || !Strings.isNullOrEmpty(printer.getRightChildLink())){
                int linkPosition = idBu.length()-idLen(printer.getId());
                if(!Strings.isNullOrEmpty(printer.getLeftChildLink())){
                    appendAt(linkBu,linkPosition-idLen(printer.getId())/2,printer.getLeftChildLink());
                    nextLineIdPositions.add(linkPosition-idLen(printer.getId())/2);
                }

                if(!Strings.isNullOrEmpty(printer.getRightChildLink())){
//                        if(Strings.isNullOrEmpty(printer.getLeftChildLink())){
//                            linkPosition+=2;
//                        }
                    appendAt(linkBu,linkPosition+idLen(printer.getId()),printer.getRightChildLink());
                    nextLineIdPositions.add(linkPosition+idLen(printer.getId())+1);
                }
            }
        }
        System.out.println(idBu.toString());
        System.out.println(linkBu.toString());
        idBu.setLength(0);
        linkBu.setLength(0);
        layerIndex++;
    }

    // 数据还原
    layerTreeMap.clear();
}

private int idLen(Integer id){
    return (id+"").length();
}

private StringBuilder appendAt(StringBuilder bu,int position,String param){
    while (bu.length()<position){
        bu.append(" ");
    }
    return bu.append(param);
}

private String createSpace(int num){
    StringBuilder spaceBu = new StringBuilder();
    for(int k=0;k<num;k++){
        spaceBu.append(" ");
    }
    return spaceBu.toString();
}

//栈,用来记录路径
Map<Integer,List<Printer>> layerTreeMap = new HashMap<>();

private Map<Integer,List<Printer>> printTree(TwoForkTree node,int index){

    if(node==null){
        return null;
    }

    index++;
    List tempList = layerTreeMap.get(index);
    if(CollectionUtils.isEmpty(tempList)){
        tempList = new LinkedList();
        layerTreeMap.put(index,tempList);
    }
    Printer printer = new Printer();
    tempList.add(printer);
    printer.setId(node.getId());
    printer.setIndex(index);
    if(node.leftNode!=null){
        printer.setLeftChildLink("/");
    }
    if(node.rightNode!=null){
        printer.setRightChildLink("\\");
    }

    printTree(node.leftNode,index);


    printTree(node.rightNode,index);

    return layerTreeMap;
}

@Setter
@Getter
public class Printer{
    private Integer id;
    private int index;
    private String leftChildLink;
    private String rightChildLink;
}

最终效果:
clipboard.png

注:之所以加了分割符( ' 32` '后面的符号),是因为打印方法还略有不足——有时两个节点会连在一起,没办法看出具体的节点值。分割符的存在算是投机取巧。


青鱼
268 声望25 粉丝

山就在那里,每走一步就近一些