Welcome, Explorer! Today, we will delve deeper into the fascinating world of tree-based data structures. Building upon our comprehensive understanding of these structures, we're ready to enhance our knowledge further. Today's lesson focuses on Binary and Non-Binary Trees: their basic structure, implementation, complexity analyses, and the core operations performed on them.
As a reminder, tree data structures possess an impressive versatility that allows them to tackle many complex problems. For instance, managing hierarchies of employees in a large organization or efficiently storing words in a spell-checking system — these real-world scenarios naturally form tree-like structures!
Starting with a brief overview, a tree in computer science is a non-linear data structure representing a hierarchical and connected arrangement of entities known as nodes. A binary tree is a specific type of tree data structure where each node has, at most, two children: one left child and one right child.
On the other hand, a non-binary tree, also known as a multi-way tree, can have more than two children per node.
Before we jump into tree implementation, let's familiarize ourselves with key concepts and facts about tree data structures essential for beginners learning about trees.
Terminology:
Tree properties:
Now that we've refreshed our understanding of what binary and non-binary trees are let's illustrate how to implement them using Python. In Python, tree structures can be constructed using class-based representations. A class is essentially a blueprint for creating objects. Objects have member variables and exhibit behaviors associated with them.
Consider the binary tree. Below is the Node
class, representing a single node in a binary tree. Each Node
object can hold a value and has two pointers, left
and right
, initially set to None
.
Python1class Node: 2 def __init__(self, value): 3 self.value = value 4 self.left = None 5 self.right = None
For a non-binary tree, we can use a list to hold the links to the child nodes since their number isn't fixed.
Python1class Node: 2 def __init__(self, value): 3 self.value = value 4 self.children = []
We can create individual nodes, link them as children or parents, and construct our desired trees.
In order to gain a practical understanding of the concepts presented so far, let's take a look at some examples of binary and non-binary trees along with their traversals.
For our binary tree, let's use a simple structure with three levels of vertices.
Here is how we define it in Python:
Python1# Creating nodes for the tree. 2root = Node(1) 3root.left = Node(2) 4root.right = Node(3) 5root.left.left = Node(4) 6root.left.right = Node(5) 7root.right.right = Node(6)
As for the non-binary tree, we demonstrate a simple tree with three levels.
Here is how we define it in Python:
Python1# Creating nodes for the tree. 2root = Node(1) 3root.children = [Node(2), Node(3), Node(4)] 4root.children[0].children = [Node(5), Node(6)] 5root.children[2].children = [Node(7)]
Trees are dynamic data structures permitting several operations, such as insertion (adding a new node), deletion (removing an existing node), and traversal (accessing or visiting all nodes in a specific order).
Traversal of the binary tree is a process of visiting all nodes of a tree and possibly printing their values. Since all nodes are connected via edges (links), we always start from the root (head) node. We cannot randomly access a node in a tree. There are three ways to traverse a tree:
Here is how the in-order traversal implementation may look like:
Python1def in_order_traversal(node): 2 if node is None: 3 return 4 in_order_traversal(node.left) 5 print(str(node.value) + ' -> ', end='') 6 in_order_traversal(node.right) 7 8in_order_traversal(root) 9# Output: 4 -> 2 -> 5 -> 1 -> 3 -> 6 ->
Considering the binary tree from the example above, the in-order traversal will be 4 -> 2 -> 5 -> 1 -> 3 -> 6
.
Usually, information is inserted into a tree as a node. In a binary tree, a new node is inserted as the left or the right child of an existing node. An algorithm for inserting a node can be established by identifying an appropriate location for the new node. Deleting a node from a tree structure requires identifying the node, studying its properties, and subsequently transforming the tree structure.
Here's how our tree definitions look with these operations implemented:
Python1class TreeNode: 2 def __init__(self, value): 3 self.value = value 4 self.children = [] 5 6 def add_child(self, child_node): 7 self.children.append(child_node) 8 9 def remove_child(self, child_node): 10 self.children = [child for child in self.children if child is not child_node]
In these examples, the add_child
and remove_child
methods enable us to add a node and remove a node in the tree, respectively.
For binary trees, the worst-case time complexity for searching, insertion, or deletion is , where is the number of nodes. This complexity arises because, in the worst case, you might have to traverse all nodes. However, in ideal circumstances (where the tree is perfectly balanced), operations on binary trees run in time.
Comparatively, for non-binary trees, searching for or deleting a node can still be , but insertion may be more efficient — — if we keep track of where the next insertion should happen; if we don't, the complexity is the same as in binary tree.
Congratulations on reaching the end of this lesson! We've delved into binary and non-binary trees, their basic structures, and complexities and even implemented them in Python. We've also learned about the various operations that can be performed on trees and how they affect the time complexity.
Excellent work! Now, we're going to reinforce your understanding through some practical exercises. These questions are designed to strengthen your command of tree data structures and familiarize you with applicable situations. Arm yourself with the power of binary trees and tackle these tasks head-on! See you in the next class!