【樹結構2】樹打印

載一棵小樹苗,精心培育,總有一天會長成參天大樹
                比如查找二叉、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` '後面的符號),是因爲打印方法還略有不足——有時兩個節點會連在一起,沒辦法看出具體的節點值。分割符的存在算是投機取巧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章