如何在控制台中“绘制”二叉树?

时间:2017-05-10 16:48:12

标签: swift algorithm binary-tree

如何在Swift中打印二叉树,以便输入79561打印输出如下:

    7
   / \
  5   9
 / \
1   6

我尝试使用For LoopsIf Statements使用一些代码来安排此操作,但它没有奏效。 我的代码是:

import UIKit

//Variable "node" used only to arrange it in output.
var node = "0"
var space = " "
var linkLeft = "/"
var linkRight = "\\"
var str = "Hello, playground"

var height = 6
var width = height * 2 + 1

print()

//Height
for h in 1...height {
    //Width
    for w in 1...width {
        switch h {
        case 1:
            if(w == width/2 + h) {
                print(node, terminator: "")
            } else {
                print(space, terminator: "")
            }

            if (w == width) {
                print()
            }
        case 2:
            //print(linkLeft, terminator: "")
            if(w == width/3 + h) {
                print(linkLeft, terminator: "")
            } else if(w == width/3 + h + 4) {
                print(linkRight, terminator: "")
            } else {
                print(space, terminator: "")
            }

            if (w == width) {
                print()
            }
        case 3:
            if(w == width/5 + h) {
                print(node, terminator: "")
            } else if(w == width/h + h){
                print(node, terminator: "")
            } else {
                print(space, terminator: "")
            }

            if (w == width) {
                print()
            }
            break
        default:
            break
        }
    }
}

我尝试使用两个For Loops一个用于高度,另一个用于宽度。但是如果节点数量发生变化,它就无法工作。目前我只是尝试安排links/\),nodes和空格的地方,因此它无效。 有可能这样做吗?

1 个答案:

答案 0 :(得分:10)

首先,您必须定义允许递归遍历树节点的分层树结构(类)。如何实现它并不重要,只要它能提供描述性字符串并访问其左右子节点。

例如(我将其用于测试目的):

class TreeNode
{
   var value : Int
   var left  : TreeNode? = nil
   var right : TreeNode? = nil

   init(_ rootValue:Int)
   { value = rootValue }

   @discardableResult
   func addValue( _ newValue:Int) -> TreeNode
   {
      if newValue == value // exclude duplicate entries
      { return self }
      else if newValue < value
      { 
         if let newNode = left?.addValue(newValue)
         { return newNode }
         left = TreeNode(newValue)
         return left!
      }
      else
      {
         if let newNode = right?.addValue(newValue)
         { return newNode }
         right = TreeNode(newValue)
         return right!
      }
   }
}

然后,您可以创建递归函数以获取要打印的行。每一行都需要知道较低级别的行,因此需要从下往上构建行列表。递归是实现这种相互依赖的简单方法。

这是一个适用于任何二叉树类的泛型函数的示例。它期望根节点和函数(或闭包)访问节点的描述和左/右子节点:

public func treeString<T>(_ node:T, reversed:Bool=false, isTop:Bool=true, using nodeInfo:(T)->(String,T?,T?)) -> String
{
   // node value string and sub nodes
   let (stringValue, leftNode, rightNode) = nodeInfo(node)

   let stringValueWidth  = stringValue.count

   // recurse to sub nodes to obtain line blocks on left and right
   let leftTextBlock     = leftNode  == nil ? []
                         : treeString(leftNode!,reversed:reversed,isTop:false,using:nodeInfo)
                           .components(separatedBy:"\n")

   let rightTextBlock    = rightNode == nil ? []
                         : treeString(rightNode!,reversed:reversed,isTop:false,using:nodeInfo)
                           .components(separatedBy:"\n")

   // count common and maximum number of sub node lines
   let commonLines       = min(leftTextBlock.count,rightTextBlock.count)
   let subLevelLines     = max(rightTextBlock.count,leftTextBlock.count)

   // extend lines on shallower side to get same number of lines on both sides
   let leftSubLines      = leftTextBlock  
                         + Array(repeating:"", count: subLevelLines-leftTextBlock.count)
   let rightSubLines     = rightTextBlock 
                         + Array(repeating:"", count: subLevelLines-rightTextBlock.count)

   // compute location of value or link bar for all left and right sub nodes
   //   * left node's value ends at line's width
   //   * right node's value starts after initial spaces
   let leftLineWidths    = leftSubLines.map{$0.count}                             
   let rightLineIndents  = rightSubLines.map{$0.prefix{$0==" "}.count  } 

   // top line value locations, will be used to determine position of current node & link bars
   let firstLeftWidth    = leftLineWidths.first   ?? 0
   let firstRightIndent  = rightLineIndents.first ?? 0


   // width of sub node link under node value (i.e. with slashes if any)
   // aims to center link bars under the value if value is wide enough
   // 
   // ValueLine:    v     vv    vvvvvv   vvvvv
   // LinkLine:    / \   /  \    /  \     / \ 
   //
   let linkSpacing       = min(stringValueWidth, 2 - stringValueWidth % 2)
   let leftLinkBar       = leftNode  == nil ? 0 : 1
   let rightLinkBar      = rightNode == nil ? 0 : 1
   let minLinkWidth      = leftLinkBar + linkSpacing + rightLinkBar
   let valueOffset       = (stringValueWidth - linkSpacing) / 2

   // find optimal position for right side top node
   //   * must allow room for link bars above and between left and right top nodes
   //   * must not overlap lower level nodes on any given line (allow gap of minSpacing)
   //   * can be offset to the left if lower subNodes of right node 
   //     have no overlap with subNodes of left node                                                                                                                                 
   let minSpacing        = 2
   let rightNodePosition = zip(leftLineWidths,rightLineIndents[0..<commonLines])
                           .reduce(firstLeftWidth + minLinkWidth)
                           { max($0, $1.0 + minSpacing + firstRightIndent - $1.1) }


   // extend basic link bars (slashes) with underlines to reach left and right
   // top nodes.  
   //
   //        vvvvv
   //       __/ \__
   //      L       R
   //
   let linkExtraWidth    = max(0, rightNodePosition - firstLeftWidth - minLinkWidth )
   let rightLinkExtra    = linkExtraWidth / 2
   let leftLinkExtra     = linkExtraWidth - rightLinkExtra

   // build value line taking into account left indent and link bar extension (on left side)
   let valueIndent       = max(0, firstLeftWidth + leftLinkExtra + leftLinkBar - valueOffset)
   let valueLine         = String(repeating:" ", count:max(0,valueIndent)) 
                         + stringValue
   let slash             = reversed ? "\\" : "/"
   let backSlash         = reversed ? "/"  : "\\"
   let uLine             = reversed ? "¯"  : "_"
   // build left side of link line
   let leftLink          = leftNode == nil ? "" 
                         : String(repeating: " ", count:firstLeftWidth)
                         + String(repeating: uLine, count:leftLinkExtra)
                         + slash

   // build right side of link line (includes blank spaces under top node value) 
   let rightLinkOffset   = linkSpacing + valueOffset * (1 - leftLinkBar)                      
   let rightLink         = rightNode == nil ? ""
                         : String(repeating:  " ", count:rightLinkOffset)
                         + backSlash 
                         + String(repeating:  uLine, count:rightLinkExtra)

   // full link line (will be empty if there are no sub nodes)                                                                                                    
   let linkLine          = leftLink + rightLink

   // will need to offset left side lines if right side sub nodes extend beyond left margin
   // can happen if left subtree is shorter (in height) than right side subtree                                                
   let leftIndentWidth   = max(0,firstRightIndent - rightNodePosition) 
   let leftIndent        = String(repeating:" ", count:leftIndentWidth)
   let indentedLeftLines = leftSubLines.map{ $0.isEmpty ? $0 : (leftIndent + $0) }

   // compute distance between left and right sublines based on their value position
   // can be negative if leading spaces need to be removed from right side
   let mergeOffsets      = indentedLeftLines
                           .map{$0.count}
                           .map{leftIndentWidth + rightNodePosition - firstRightIndent - $0 }
                           .enumerated()
                           .map{ rightSubLines[$0].isEmpty ? 0  : $1 }


   // combine left and right lines using computed offsets
   //   * indented left sub lines
   //   * spaces between left and right lines
   //   * right sub line with extra leading blanks removed.
   let mergedSubLines    = zip(mergeOffsets.enumerated(),indentedLeftLines)
                           .map{ ( $0.0, $0.1, $1 + String(repeating:" ", count:max(0,$0.1)) ) }
                           .map{ $2 + String(rightSubLines[$0].dropFirst(max(0,-$1))) }

   // Assemble final result combining
   //  * node value string
   //  * link line (if any)
   //  * merged lines from left and right sub trees (if any)
   let treeLines = [leftIndent + valueLine]
                 + (linkLine.isEmpty ? [] : [leftIndent + linkLine])
                 + mergedSubLines

   return (reversed && isTop ? treeLines.reversed(): treeLines)
          .joined(separator:"\n")                                        
}

要实际打印,您需要提供具有类的节点和闭包的函数来访问节点描述以及左右子节点。

extension TreeNode
{       
   var asString:String { return treeString(self){("\($0.value)",$0.left,$0.right)}  }       
}

var root = TreeNode(7)

root.addValue(9)
root.addValue(5)
root.addValue(6)
root.addValue(1)

print(root.asString)

//     7
//    / \
//   5   9
//  / \
// 1   6
//

root = TreeNode(80)

root.addValue(50)
root.addValue(90)
root.addValue(10)
root.addValue(60)
root.addValue(30)
root.addValue(70)
root.addValue(55)
root.addValue(5)
root.addValue(35)
root.addValue(85)

print(root.asString)

//              80
//          ___/  \___
//        50          90
//     __/  \__      /
//   10        60  85
//  /  \      /  \
// 5    30  55    70
//        \
//         35
//

[编辑]改进逻辑,在树的右侧比左侧更深的地方使用更少的空间。清理代码并添加注释以解释它是如何工作的。

//
//       12
//      /  \
//    10    50
//   /   __/  \__
//  5  30        90
//       \      /
//        35  70
//           /  \
//         60    85
//        /
//      55
//

//                12
//               /  \
//             10    30
//            /        \
//           5          90
//                     /
//                   85
//                  /
//                70
//               /
//             55
//            /
//          48
//         /
//       45
//      /
//    40
//   /
// 35
//

[EDIT2]使函数具有通用性和适应性解释。

使用泛型函数,数据甚至不需要在实际的树结构中。

例如,您可以打印包含堆树的数组:

 extension Array
 {    
    func printHeapTree(reversed:Bool = false)
    {
       let tree = treeString( 0, reversed:reversed )
       {
          let left  = { $0 < self.count ? $0 : nil}($0 * 2 + 1)
          let right = { $0 < self.count ? $0 : nil}($0 * 2 + 2)
          return ( "\(self[$0])", left, right )
       }
       print(tree) 
    }
 }

let values = [7,5,9,1,6]
values.printHeapTree()

//     7
//    / \
//   5   9
//  / \
// 1   6

let family = [ "Me","Paul","Rosa","Vincent","Jody","John","Kate"]
family.printHeapTree()

//                Me
//            ___/  \___
//        Paul          Rosa
//        /  \          /  \
// Vincent    Jody  John    Kate

但是对于最后一个例子,一个家谱通常是颠倒的。所以,我调整了函数以允许打印反转树:

family.printHeapTree(reversed:true)

// Vincent    Jody  John    Kate
//        \  /          \  /
//        Paul          Rosa
//            ¯¯¯\  /¯¯¯
//                Me

[EDIT3]添加条件以根据Emm的请求从示例类(TreeNode)中的树中排除重复条目

[EDIT4]更改了mergedSubLines的公式,以便在实际项目中进行编译(在操场上测试)。

[EDIT5]对Swift4进行微调,增加了打印反向树的功能,将Array示例更改为堆树。