A tree is a (possibly non-linear) data structure made up of nodes or vertices and edges without having any cycle.
Tree is one of the most important data structure in algorithms
Like our current File System
Tree has many different types
Tree
B Tree
Trie Tree
Binary Tree
BST
RB Tree
AVL
A
B
C
D
E
F
G
J
I
H
class Node:
def __init__(self, data=None):
self.data = data
self.left = None
self.right = None
class BinaryTree:
def __init__(self, root_data):
self.root = Node(root_data)
A
B
D
E
H
I
J
G
F
C
PreOrder: A B D E H I C F G J
InOrder: D B H E I A F C G J
PostOrder: D H I E B F J G C A
We need to use recursion to traverse a tree
def preorder(root):
if root is not None:
# Visit the node by printing the node data
print(root.data, end=" ")
preorder(root.left)
preorder(root.right)
def inorder(root):
if root is not None:
inorder(root.left)
print(root.data, end=" ")
inorder(root.right)
def postorder(root):
if root is not None:
postorder(root.left)
postorder(root.right)
print(root.data, end=" ")
Can we use Stack to traverse the tree without recursion?
Pre-order traversal: use a stack to simulate recursion, and print the node when it is first encountered.
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
stack, res = [], []
while stack or root:
while root:
stack.append(root) # first time
res.append(root.val)
root = root.left
if stack:
top = stack.pop()
if top.right:
root = top.right
return res
1
/ \
2 3
/ \
4 5
InOrder: What we should do?
InOrder: We push the node, go to left
If there is no left, we pop, print and push the right
def inorderTraversal(self, root: TreeNode) -> List[int]:
stack = []
res = []
while root or stack:
while root:
stack.append(root)
root = root.left
if stack:
root = stack.pop()
res.append(root.val)
root = root.right
return res
PostOrder: The most difficult one
PostOrder: go to left, when there is no left, visit it. Then for the top of the stack, do we visit it now?
No. Because it is what InOrder does
We need to then go to the right until there is nowhere to go, we visit it and then pop
PostOrder: The most difficult one
Can we do the PostOrder just using the current structure?
No. We do not know the status of the top element in the stack. Has the right child been traversed or not?
We need an additional flag to store the status of the node
static class NodeWithFlag {
Node node;
boolean flag;
public NodeWithFlag(Node n, boolean value) {
node = n;
flag = value;
}
}
After we visit the right child, we update the flag;
Another way is to use a HashSet to store which node has been visited
First visit :HashSet.contains() == false -> put into set
Second visit: HashSet.contains == true -> output value
If we have a traversal result of a tree, can we construct the tree?
PreOrder: A B C D
A
B
D
C
A
B
D
C
No matter if you give a PreOrder, InOrder or PostOrder, it can construct different trees
What if we give two traversals?
PreOrder: A B C D
A
B
D
C
A
B
D
C
InOrder: B A D C
PreOrder: A B C D
A
B
D
C
PostOrder: B D C A
A
B
D
C
Conclusion: You need two traversals to construct a tree, and one of them must be a InOrder traversal
How to construct a tree with a PreOrder and an InOrder?
A
B
D
E
H
I
J
G
F
C
PreOrder: A B D E H I C F G J
InOrder: D B H E I A F C G J
A is root so A is the first in PreOrder, InOrder can use A to seperate left sub-tree and right sub-tree
Then we can do recursion
public TreeNode buildTree(int[] preorder, int[] inorder) {
final int preStart = 0;
final int preEnd = preorder.length - 1;
final int inStart = 0;
final int inEnd = inorder.length - 1;
return constructTree(preorder, preStart, preEnd, inorder, inStart, inEnd);
}
private TreeNode constructTree(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) {
// Base case: if there are no elements to construct the tree
if (preStart > preEnd || inStart > inEnd) {
return null;
}
// The first element of preorder is the root of the current subtree
final int rootValue = preorder[preStart];
TreeNode root = new TreeNode(rootValue);
// Find the position of the root in inorder to determine the boundaries of left and right subtrees
int inorderRootIndex = 0;
for (int i = inStart; i <= inEnd; i++) {
if (inorder[i] == rootValue) {
inorderRootIndex = i;
break;
}
}
// Number of nodes in the left subtree
final int leftSubtreeSize = inorderRootIndex - inStart;
// Recursively construct the left and right subtrees
root.left = constructTree(preorder, preStart + 1, preStart + leftSubtreeSize, inorder, inStart, inorderRootIndex - 1);
root.right = constructTree(preorder, preStart + leftSubtreeSize + 1, preEnd, inorder, inorderRootIndex + 1, inEnd);
return root;
}
def buildTree(preorder, inorder):
preStart = 0
preEnd = len(preorder) - 1
inStart = 0
inEnd = len(inorder) - 1
return construct(preorder, preStart, preEnd, inorder, inStart, inEnd)
def construct(preorder, preStart, preEnd, inorder, inStart, inEnd):
if preStart > preEnd or inStart > inEnd:
return None
val = preorder[preStart]
p = TreeNode(val)
k = 0
for i in range(inStart, inEnd + 1):
if val == inorder[i]:
k = i
break
p.left = construct(preorder, preStart + 1, preStart + (k - inStart), inorder, inStart, k - 1)
p.right = construct(preorder, preStart + (k - inStart) + 1, preEnd, inorder, k + 1, inEnd)
return p
A
B
D
E
H
I
J
G
F
C
PostOrder: D H I E B F J G C A
InOrder: D B H E I A F C G J
It is similar as PreOrder + InOrder, the only difference is the root now is the last one in PostOrder
public TreeNode buildTree(int[] inorder, int[] postorder) {
int inStart = 0;
int inEnd = inorder.length - 1;
int postStart = 0;
int postEnd = postorder.length - 1;
return build(inorder, inStart, inEnd, postorder, postStart, postEnd);
}
public TreeNode build(int[] inorder, int inStart, int inEnd, int[] postorder, int postStart, int postEnd) {
if (inStart > inEnd || postStart > postEnd)
return null;
int rootValue = postorder[postEnd];
TreeNode root = new TreeNode(rootValue);
int k = 0;
for (int i = inStart; i <=inEnd; i++) {
if (inorder[i] == rootValue) {
k = i;
break;
}
}
root.left = build(inorder, inStart, k - 1, postorder, postStart,
postStart + k - (inStart + 1));
root.right = build(inorder, k + 1, inEnd, postorder, postStart + k- inStart, postEnd - 1);
return root;
}
def buildTree(inorder, postorder):
inStart = 0
inEnd = len(inorder) - 1
postStart = 0
postEnd = len(postorder) - 1
return build(inorder, inStart, inEnd, postorder, postStart, postEnd)
def build(inorder, inStart, inEnd, postorder, postStart, postEnd):
if inStart > inEnd or postStart > postEnd:
return None
rootValue = postorder[postEnd]
root = TreeNode(rootValue)
k = 0
for i in range(inStart, inEnd + 1):
if inorder[i] == rootValue:
k = i
break
root.left = build(inorder, inStart, k - 1, postorder, postStart, postStart + k - (inStart + 1))
root.right = build(inorder, k + 1, inEnd, postorder, postStart + k - inStart, postEnd - 1)
return root
30
18
13
24
22
27
47
40
31
34
30
18
13
24
22
27
47
40
31
34
Find 24
Find 42
def find(value, root):
node = root
while node is not None:
if node.data > value:
node = node.leftNode
elif node.data < value:
node = node.rightNode
else:
return True
return False
30
18
13
24
22
27
47
40
31
34
Add 42
30
18
13
24
22
27
47
40
31
34
30
18
13
24
22
27
47
40
31
34
42
def add(value, root):
if root is None:
root = Node(value)
return True
node = root
while node is not None:
if node.data > value:
if node.leftNode is not None:
node = node.leftNode
else:
node.leftNode = Node(value)
return True
elif node.data < value:
if node.rightNode is not None:
node = node.rightNode
else:
node.rightNode = Node(value)
return True
else:
return False
return False
What is the time complexity for finding and adding some element into the tree?
30
18
13
24
22
27
47
40
31
34
Remove 27
Remove 30
P
Q
R
P
Q
R
P
Q
R
P
Q
R
P<R<Q
P<Q<R
R<Q<P
Q<R<P
Just use R to replace Q
P
Q
R1
R2
if we remove Q, which is the best substitute?
P
Q
R1
R2
Consider the inOrder traversal, the one just before Q and the one just after Q are the best candidates, since if they replace Q, when we do the inOrder again, the output is still an sorted array, which means the tree is still a BST.
So where are these two nodes?
P
Q
R1
R2
So for the element before Q, it is the largest node in sub-tree R1
The element after Q, it is the smallest node in sub-tree R2
Does that node have child? Canwe remove them directly
Maybe, but at most one, so we go back to condition 2
def remove(value, root):
if root is None:
return False
if root.data == value:
root = removeNode(root)
return True
node = root
while node is not None:
if node.data > value:
if node.leftNode is not None and node.leftNode.data != value:
node = node.leftNode
elif node.leftNode is None:
return False
else:
node.leftNode = removeNode(node.leftNode)
return True
elif node.data < value:
if node.rightNode is not None and node.rightNode.data != value:
node = node.rightNode
elif node.rightNode is None:
return False
else:
node.rightNode = removeNode(node.rightNode)
return True
else:
return False
return False
def removeNode(node):
if node.leftNode == None and node.rightNode == None:
return None
elif node.leftNode == None:
return node.rightNode
elif node.rightNode == None:
return node.leftNode
else:
node.data = findAndRemove(node)
return node
def findAndRemove(node):
result = 0
if node.leftNode.rightNode == None:
result = node.leftNode.data
node.leftNode = node.leftNode.leftNode
return result
node = node.leftNode
while node.rightNode.rightNode != None:
node = node.rightNode
result = node.rightNode.data
node.rightNode = node.rightNode.leftNode
return result
30
18
13
24
22
27
47
40
31
34
42
43
30
18
13
24
22
27
47
43
34
40
42
31
We know the search time is highly related to the height of the tree
If we keep add and remove elements in the tree, the tree will become unbalanced
So we have Red-black tree and AVL tree, they could use rotation and reconstruct to make the tree balance.
In computer science, a trie, also called digital tree and sometimes radix tree or prefix tree (as they can be searched by prefixes), is an ordered tree data structure that is used to store a dynamic set or associative array where the keys are usually strings.
In the insertion and finding notice that lowering a single level in the tree is done in constant time, and every time that the program lowers a single level in the tree, a single character is cut from the string.
we can conclude that every function lowers L levels on the tree and every time that the function lowers a level on the tree, it is done in constant time, then the insertion and finding of a word in a trie can be done in O(L) time.
The memory used in the tries depends on the methods to store the edges and how many words have prefixes in common.
Implement a trie with insert, search, and startsWith methods
We only use lowercase 'a' to 'z'
How many children does a node have?
class TrieNode:
def __init__(self):
self.isWord = False
self.children = [None] * 26
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
if word is None or len(word) == 0:
return
pNode = self.root
for i in range(len(word)):
c = word[i]
index = ord(c) - ord('a')
if pNode.children[index] is None:
newNode = TrieNode()
pNode.children[index] = newNode
pNode = pNode.children[index]
pNode.isWord = True
def search(self, word):
pNode = self.root
if word is None or len(word) == 0:
return True
for i in range(len(word)):
index = ord(word[i]) - ord('a')
pNode = pNode.children[index]
if pNode is None:
return False
return pNode.isWord
def startsWith(self, prefix):
pNode = self.root
if prefix is None or len(prefix) == 0:
return True
for i in range(len(prefix)):
index = ord(prefix[i]) - ord('a')
pNode = pNode.children[index]
if pNode is None:
return False
return True
Design an in-memory file system to simulate the following functions:
ls: Given a path in string format. If it is a file path, return a list that only contains this file's name. If it is a directory path, return the list of file and directory names in this directory. Your output (file and directory names together) should in lexicographic order.
mkdir: Given a directory path that does not exist, you should make a new directory according to the path. If the middle directories in the path don't exist either, you should create them as well. This function has void return type.
addContentToFile: Given a file path and file content in string format. If the file doesn't exist, you need to create that file containing given content. If the file already exists, you need to append given content to original content. This function has void return type.
readContentFromFile: Given a file path, return its content in string format.
We mentioned File System is a tree.
And file system can have many children under each node. So using Trie tree is a good way to represent it.
class TrieNode:
def __init__(self):
self.isFile = False
self.content = ""
self.children = {}
class FileSystem:
def __init__(self):
self.root = TrieNode()
def ls(self, path):
results = []
cur = self.root
routes = path.split("/")
for i in range(1, len(routes)):
cur = cur.children.get(routes[i])
if cur.isFile:
results.append(routes[-1])
else:
results.extend(cur.children.keys())
results.sort()
return results
def mkdir(self, path):
self.get_current_node(path)
class FileSystem:
def __init__(self):
self.root = TrieNode()
def addContentToFile(self, filePath, content):
cur = self.get_current_node(filePath)
cur.isFile = True
if cur.content is None:
cur.content = content
else:
cur.content += content
def readContentFromFile(self, filePath):
cur = self.get_current_node(filePath)
return cur.content
def get_current_node(self, path):
cur = self.root
routes = path.split("/")
for i in range(1, len(routes)):
if routes[i] not in cur.children:
cur.children[routes[i]] = TrieNode()
cur = cur.children[routes[i]]
return cur
Given an m x n board of characters and a list of strings words, return all words on the board.
Each word must be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.
Example:
Input:
board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]]
words = ["oath","pea","eat","rain"]
Output: ["eat","oath"]
class TrieNode:
def __init__(self):
self.children = [None] * 26
self.isWord = False
self.word = None
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
cur = self.root
for char in word:
c = ord(char) - ord('a')
if not cur.children[c]:
cur.children[c] = TrieNode()
cur = cur.children[c]
cur.isWord = True
cur.word = word
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
trie = Trie()
for word in words:
trie.insert(word)
m = len(board)
n = len(board[0])
visited = [[False for _ in range(n)] for _ in range(m)]
results = set()
def search(trienode, x, y):
nonlocal m, n
if trienode.isWord:
results.add(trienode.word)
visited[x][y] = True
for dx, dy in [(1, 0), (0, 1), (-1, 0), (0, -1)]:
nx, ny = x + dx, y + dy
if 0 <= nx < m and 0 <= ny < n and not visited[nx][ny]:
c = ord(board[nx][ny]) - ord('a')
if trienode.children[c]:
search(trienode.children[c], nx, ny)
visited[x][y] = False
for i in range(m):
for j in range(n):
c = ord(board[i][j]) - ord('a')
if trie.root.children[c]:
search(trie.root.children[c], i, j)
return list(results)
We used the tries to store words with lowercase letters, but the tries can be used to store many other things. We can use bits or bytes instead of lowercase letters and every data type can be stored in the tree, not only strings.