|
| 1 | +from itertools import product |
| 2 | +from pprint import pprint |
| 3 | + |
| 4 | +# Default configuration constants |
| 5 | +DEFAULT_UNIT_SIZE = 3 |
| 6 | +DEFAULT_FILL_CHAR = " " |
| 7 | + |
| 8 | + |
| 9 | +class Node: |
| 10 | + """ |
| 11 | + Represents a node in a binary tree. |
| 12 | + |
| 13 | + Attributes: |
| 14 | + val: The value stored in the node |
| 15 | + left: Reference to the left child node |
| 16 | + right: Reference to the right child node |
| 17 | + """ |
| 18 | + def __init__(self, val=0, left=None, right=None): |
| 19 | + self.val = val |
| 20 | + self.left = left |
| 21 | + self.right = right |
| 22 | + |
| 23 | + def __str__(self): |
| 24 | + return str(self.val) |
| 25 | + |
| 26 | + def __repr__(self): |
| 27 | + return str(self) |
| 28 | + |
| 29 | + |
| 30 | +def center(val, unitSize=None, fillChar=None): |
| 31 | + """ |
| 32 | + Centers a value within a fixed width string. |
| 33 | + |
| 34 | + Args: |
| 35 | + val: The value to center |
| 36 | + unitSize: The total width of the output string (uses DEFAULT_UNIT_SIZE if None) |
| 37 | + fillChar: The character to use for padding (uses DEFAULT_FILL_CHAR if None) |
| 38 | + |
| 39 | + Returns: |
| 40 | + A centered string representation of val |
| 41 | + """ |
| 42 | + if unitSize is None: |
| 43 | + unitSize = DEFAULT_UNIT_SIZE |
| 44 | + if fillChar is None: |
| 45 | + fillChar = DEFAULT_FILL_CHAR |
| 46 | + return str(val).center(unitSize, fillChar) |
| 47 | + |
| 48 | + |
| 49 | +def getDepth(node: Node): |
| 50 | + """ |
| 51 | + Calculates the depth (height) of a binary tree. |
| 52 | + |
| 53 | + Args: |
| 54 | + node: The root node of the tree |
| 55 | + |
| 56 | + Returns: |
| 57 | + The depth of the tree (number of levels from root to deepest leaf) |
| 58 | + """ |
| 59 | + if node == None: |
| 60 | + return 0 |
| 61 | + return 1 + max(getDepth(node.left), getDepth(node.right)) |
| 62 | + |
| 63 | + |
| 64 | +def register(node: Node, fillChar=None, unitSize=None, code="", mem=None): |
| 65 | + """ |
| 66 | + Recursively registers all nodes in a tree with their binary path codes. |
| 67 | + |
| 68 | + Each node is assigned a binary code representing its position: |
| 69 | + - Empty string "" for root |
| 70 | + - "0" appended for left child |
| 71 | + - "1" appended for right child |
| 72 | + |
| 73 | + Args: |
| 74 | + node: The current node being processed |
| 75 | + fillChar: Character used for padding (uses DEFAULT_FILL_CHAR if None) |
| 76 | + unitSize: Size for centering values (uses DEFAULT_UNIT_SIZE if None) |
| 77 | + code: The binary path code for the current node |
| 78 | + mem: Dictionary mapping binary codes to centered node values |
| 79 | + """ |
| 80 | + if mem is None: |
| 81 | + mem = {} |
| 82 | + if node: |
| 83 | + mem[code] = center(node.val, unitSize=unitSize, fillChar=fillChar) |
| 84 | + register(node.left, fillChar=fillChar, unitSize=unitSize, code=code + "0", mem=mem) |
| 85 | + register(node.right, fillChar=fillChar, unitSize=unitSize, code=code + "1", mem=mem) |
| 86 | + return mem |
| 87 | + |
| 88 | + |
| 89 | +def nodeToMat(node: Node, depth=-1, fillChar=None, unitSize=None, removeEmpty=True): |
| 90 | + """ |
| 91 | + Converts a binary tree into a 2D matrix representation for visualization. |
| 92 | + |
| 93 | + The matrix includes: |
| 94 | + - Even rows (0, 2, 4...): Node values |
| 95 | + - Odd rows (1, 3, 5...): Connection lines (/ and \\) |
| 96 | + |
| 97 | + Args: |
| 98 | + node: The root node of the tree to visualize |
| 99 | + depth: The depth of the tree (-1 for auto-calculation) |
| 100 | + fillChar: Character for padding (uses DEFAULT_FILL_CHAR if None) |
| 101 | + unitSize: Size for centering (uses DEFAULT_UNIT_SIZE if None) |
| 102 | + removeEmpty: Whether to remove empty leading columns |
| 103 | + |
| 104 | + Returns: |
| 105 | + A 2D list (matrix) representing the tree structure |
| 106 | + """ |
| 107 | + if unitSize is None: |
| 108 | + unitSize = DEFAULT_UNIT_SIZE |
| 109 | + if fillChar is None: |
| 110 | + fillChar = DEFAULT_FILL_CHAR |
| 111 | + |
| 112 | + if depth == -1: |
| 113 | + depth = getDepth(node) |
| 114 | + |
| 115 | + # Register all nodes with their binary path codes |
| 116 | + tree = register(node, fillChar=fillChar, unitSize=unitSize, code="", mem={}) |
| 117 | + |
| 118 | + # Create matrix: (2*depth - 1) rows x (2^depth - 1) columns |
| 119 | + mat = [[center("", unitSize=unitSize, fillChar=fillChar) for _ in range(2 ** depth - 1)] for _ in range(2 * depth - 1)] |
| 120 | + |
| 121 | + side = "left" |
| 122 | + leftBound, rightBound = 0, 2 ** depth - 2 |
| 123 | + |
| 124 | + # Start with all even column indices (where values can be placed) |
| 125 | + valueIndexes = [i for i in range(2 ** depth - 1) if i % 2 == 0] |
| 126 | + |
| 127 | + # Build matrix from bottom to top |
| 128 | + for level in range(2 * (depth - 1), -1, -1): |
| 129 | + # Odd levels: place connection characters (/ and \) |
| 130 | + if level % 2 != 0: |
| 131 | + for i, index in enumerate(valueIndexes): |
| 132 | + mat[level][index] = [center("/", unitSize=unitSize, fillChar=fillChar), center("\\", unitSize=unitSize, fillChar=fillChar)][i % 2] |
| 133 | + |
| 134 | + # Calculate parent positions (midpoints between child pairs) |
| 135 | + newIndexes = [] |
| 136 | + for i in range(0, len(valueIndexes) - 1, 2): |
| 137 | + newIndexes.append((valueIndexes[i] + valueIndexes[i + 1]) // 2) |
| 138 | + valueIndexes = newIndexes |
| 139 | + continue |
| 140 | + |
| 141 | + # Even levels: place node values |
| 142 | + # Generate all binary codes for current level |
| 143 | + codes = list(product(*["01" for _ in range(level // 2)])) |
| 144 | + codes = ["".join(code) for code in codes] |
| 145 | + |
| 146 | + for i, index in enumerate(valueIndexes): |
| 147 | + mat[level][index] = tree.get(codes[i], center("", unitSize=unitSize, fillChar=fillChar)) |
| 148 | + |
| 149 | + # Remove empty leading columns if requested |
| 150 | + if removeEmpty: |
| 151 | + for i in range(2 ** depth - 1): |
| 152 | + remove = False |
| 153 | + if all( |
| 154 | + mat[j][i] in [ |
| 155 | + center("", unitSize=unitSize, fillChar=fillChar), |
| 156 | + center("/", unitSize=unitSize, fillChar=fillChar), |
| 157 | + center("\\", unitSize=unitSize, fillChar=fillChar) |
| 158 | + ] for j in range(2 * depth - 1) |
| 159 | + ): |
| 160 | + remove = True |
| 161 | + if not remove: |
| 162 | + break |
| 163 | + for j in range(2 * depth - 1): |
| 164 | + mat[j][i] = "" |
| 165 | + |
| 166 | + return mat |
| 167 | + |
| 168 | + |
| 169 | +def nodeToString(node: Node, depth=-1, fillChar=None, unitSize=None, removeEmpty=True): |
| 170 | + """ |
| 171 | + Converts a binary tree into a string representation for visualization. |
| 172 | + |
| 173 | + Args: |
| 174 | + node: The root node of the tree to visualize |
| 175 | + depth: The depth of the tree (-1 for auto-calculation) |
| 176 | + fillChar: Character for padding (uses DEFAULT_FILL_CHAR if None) |
| 177 | + unitSize: Size for centering (uses DEFAULT_UNIT_SIZE if None) |
| 178 | + removeEmpty: Whether to remove empty leading columns |
| 179 | + |
| 180 | + Returns: |
| 181 | + A string representation of the tree with each row on a new line |
| 182 | + """ |
| 183 | + mat = nodeToMat(node, depth=depth, fillChar=fillChar, unitSize=unitSize, removeEmpty=removeEmpty) |
| 184 | + return "\n".join("".join(row) for row in mat) |
| 185 | + |
| 186 | + |
| 187 | +# Example usage |
| 188 | +if __name__ == "__main__": |
| 189 | + myNode = Node(1) |
| 190 | + # myNode.left = Node(2) |
| 191 | + myNode.right = Node(3) |
| 192 | + # myNode.left.left = Node(4) |
| 193 | + # myNode.left.right = Node(5) |
| 194 | + myNode.right.left = Node(6) |
| 195 | + myNode.right.right = Node(7) |
| 196 | + |
| 197 | + mat = nodeToMat(myNode) |
| 198 | + pprint(mat) |
| 199 | + for row in mat: |
| 200 | + print(*row) |
| 201 | + |
| 202 | + print("\n--- Using nodeToString ---") |
| 203 | + print(nodeToString(myNode)) |
| 204 | + |
| 205 | + print("\n--- Custom unit size ---") |
| 206 | + print(nodeToString(myNode, unitSize=1)) |
0 commit comments