BST (Binary Search Tree)
A Binary Search Tree is a type of binary tree where every node has at most two children, and these children follow the binary search property.
Property
- The value of all the nodes in the left subtree is less than the value of the current node.
- The value of all the nodes in the right subtree is greater than the value of the current node.
This property allows for efficient searching, insertion, and deletion operations in O(log n) time, where n
is the number of nodes in the tree, provided the tree is balanced.
Operations like searching for a value, inserting a new value, or deleting an existing node can be performed efficiently due to the BST property.
Using Array
Binary Search Trees are typically implemented using pointers, as they allow for dynamic memory allocation. However, it is possible to represent a complete binary tree as an array, which requires prior knowledge of the tree’s maximum size.
Using Linked
The linked approach to BSTs is more common and allows for dynamic memory allocation and flexibility.
Finding Height, Depth, Number of Nodes, and Leaves
The height of a tree is the length of the longest path from the root to a leaf. Depth is the distance from the root node to the target node.
Node Deletion in a BST
- Node is a leaf (no children): Simply remove the node from the tree.
- Node has one child: Remove the node and link its child directly to the node’s parent.
- Node has two children: Find the in-order successor (the smallest node in the right subtree), replace the node’s value with the in-order successor’s value, and then delete the in-order successor.
Info
The helper function
minValueNode
finds the in-order successor of a node, which is used in the deletion process when the node has two children.
-
Case 1: Node is a Leaf (No Children)
When the node to be deleted has no children, we simply free the memory allocated for that node and returnNULL
to its parent, effectively removing the node from the tree. -
Case 2: Node Has One Child
When the node has only one child, we remove the node and directly connect its only child to the parent of the node. If the left child isNULL
, the right child is linked to the parent. Conversely, if the right child isNULL
, the left child is linked. -
Case 3: Node Has Two Children
When the node has two children, we:- Find the in-order successor (the smallest node in the right subtree) using the
minValueNode
function. - Copy the in-order successor’s value to the node to be deleted.
- Delete the in-order successor from the right subtree.
The in-order successor is chosen because it maintains the BST property after deletion.
- Find the in-order successor (the smallest node in the right subtree) using the
In this example, we delete the node with the value 10
and then perform an inorder traversal to see the remaining structure of the tree.
Traversal Using Iteration
BST traversal using iteration can be done using Inorder, Preorder, and Postorder traversals. Here’s an example of an iterative Inorder traversal using a stack.
Traversal using Recursion
Expression Tree
An Expression Tree is a binary tree in which each internal node represents an operator (e.g., +
, -
, *
, /
), and each leaf node represents an operand (a constant or variable). Expression Trees are useful for evaluating mathematical expressions in a structured manner.
Important
In an expression tree:
- Each leaf node represents a number or variable.
- Each internal node represents an operator.
- The evaluation of the tree involves traversing it in postorder (left, right, root) to apply operators in the correct order.
To build and evaluate an expression tree, we’ll define:
- A Node structure representing an operand or operator.
- Functions to build an expression tree from postfix notation.
- A postorder traversal function for evaluation.
In this example, the postfix expression "23*54*+9-"
is converted to an expression tree, and the result is evaluated.
Threaded Binary Tree
A Threaded Binary Tree is a binary tree variant that enables efficient inorder traversal without using a stack or recursion. In a threaded binary tree:
Important
- Left threads point to the inorder predecessor if the left child is absent.
- Right threads point to the inorder successor if the right child is absent.
This approach reduces memory usage and allows traversal without extra storage by utilizing null pointers as links.
For simplicity, here’s an implementation of a single-threaded binary tree (right-threaded):
In this example, we build a right-threaded binary tree and perform an inorder traversal. Right threads help in accessing the inorder successor efficiently.
These implementations provide a foundation for Expression Trees and Threaded Binary Trees, with efficient traversal and manipulation techniques.
Heap Tree
Info
A Heap Tree is a specialized binary tree that satisfies the heap property, which can be one of the following:
- Max-Heap: Each parent node is greater than or equal to its child nodes. The maximum element is at the root.
- Min-Heap: Each parent node is less than or equal to its child nodes. The minimum element is at the root.
Properties of a Heap
- Shape Property: A heap is a complete binary tree, meaning all levels are fully filled except possibly the last, which is filled from left to right.
- Heap Property: For a max-heap, each parent node is greater than or equal to its children, while for a min-heap, each parent node is less than or equal to its children.
- Array Representation: Heaps are typically stored as arrays, making them efficient for accessing parent and child nodes.
Example
- For a node at index
i
:
- Parent is at index
(i - 1) / 2
- Left child is at index
2 * i + 1
- Right child is at index
2 * i + 2
Heap Construction Using Array
Bottom-Up Heap Construction (Heapify)
In bottom-up heap construction, we start from the last non-leaf node and apply the heapify
operation to ensure each subtree satisfies the heap property. This method is efficient with a time complexity of (O(n))
.
Top-Down Heap Construction (Insert)
In top-down heap construction, we add elements one by one, placing them at the end of the heap array, then “bubble up” to ensure the heap property is maintained. This method is useful when building a heap incrementally.
Priority Queue Using Min and Max Heap
A Priority Queue is a data structure where each element has a priority, and the element with the highest priority is served before others. Priority queues can be implemented using heaps:
Important
- Max-Heap for maximum priority queues (highest element at the root).
- Min-Heap for minimum priority queues (smallest element at the root).
Max-Heap Priority Queue (Insertion and Deletion)
For a max-heap, the maximum element is always at the root. We insert by adding to the end of the array and bubble up to maintain the heap property. To remove the maximum, we replace the root with the last element and heapify down.
Min-Heap Priority Queue (Insertion and Deletion)
For a min-heap, the minimum element is always at the root. We insert by adding to the end of the array and bubble up to maintain the heap property. To remove the minimum, we replace the root with the last element and heapify down.
This example demonstrates basic insertions and deletions in min-heap and max-heap priority queues.
Balanced Trees
Info
A Balanced Tree is a type of binary tree where the height difference between the left and right subtrees of any node is within a certain limit, often a single level. Balanced trees ensure that the tree does not become skewed, which maintains efficient operations, particularly for insertion, deletion, and search, which operate in (O(\log n)) time.
Why Balance Matters
Without balancing, a binary search tree (BST) can degrade to a linked list with (O(n)) operations. Balanced trees, like AVL trees, prevent this by automatically adjusting the structure during insertions and deletions.
AVL Trees
An AVL Tree (named after its inventors, Adelson-Velsky and Landis) is a self-balancing binary search tree where the height difference between the left and right subtrees (known as the balance factor) of any node is at most 1. This property ensures logarithmic height, making AVL trees efficient for searching, insertion, and deletion.
Properties of AVL
- Binary Search Tree Property: Left child values are smaller than the parent, and right child values are larger.
- Balance Factor: The difference in height between the left and right subtrees of a node is calculated as:
balance_factor = height(left_subtree) - height(right_subtree)
- Self-Balancing: After each insertion or deletion, the tree checks balance factors and performs rotations if needed to maintain the balance.
AVL Tree Rotations
AVL tree rotations are used to maintain the balance of the tree by rearranging nodes. There are four types of rotations to correct specific imbalance cases:
1. Left Rotation (Single Rotation)
Left Rotation is used to correct a Right-Heavy imbalance (when the right subtree of a node is higher than the left). It is commonly used in Right-Right (RR) Imbalance situations.
2. Right Rotation (Single Rotation)
Right Rotation is used to correct a Left-Heavy imbalance (when the left subtree of a node is higher than the right). It is commonly used in Left-Left (LL) Imbalance situations.
3. Left-Right Rotation (Double Rotation)
Left-Right Rotation is a combination of a left rotation followed by a right rotation. It is used to correct a Left-Right (LR) Imbalance, where a node’s left subtree is right-heavy.
4. Right-Left Rotation (Double Rotation)
Right-Left Rotation is a combination of a right rotation followed by a left rotation. It is used to correct a Right-Left (RL) Imbalance, where a node’s right subtree is left-heavy.
Insertion in an AVL Tree
After inserting a new node, the AVL tree checks for imbalances by calculating balance factors and performs the necessary rotations to restore balance.
In this example, the tree remains balanced after each insertion, thanks to rotations, maintaining efficient performance.
Splay Tree
A Splay Tree is a self-adjusting binary search tree that performs splaying operations on nodes to maintain a roughly balanced structure. Whenever a node is accessed (searched, inserted, or deleted), it is brought to the root through a series of rotations known as splaying. This ensures that recently accessed elements are quicker to reach, making splay trees particularly useful for applications with non-uniform access patterns.
Properties of a Splay Tree
- Self-Adjusting: The tree adjusts its structure by splaying nodes upon access, moving them to the root.
- No Balance Factor or Height Constraints: Unlike AVL or Red-Black trees, splay trees don’t enforce strict balance criteria.
- Amortized (O(log n)) Time Complexity: While individual operations may take longer, the amortized time for access, insertion, or deletion is (O(log n)), making splay trees efficient over time.
Splay Operations (Rotations)
There are three types of rotations in a splay tree to move a node to the root, depending on its position relative to its parent and grandparent nodes. Each rotation type is aimed at moving the target node up the tree to eventually reach the root.
1. Zig Rotation (Single Rotation)
Zig is a single rotation performed when the target node is a child of the root. This is similar to a single left or right rotation in a binary search tree.
- Right Zig: Performed when the target node is the left child of the root.
- Left Zig: Performed when the target node is the right child of the root.
2. Zig-Zig Rotation (Double Rotation)
Zig-Zig is a double rotation used when the target node and its parent are both left or both right children of their respective parents.
- Right Zig-Zig: Performed when the target node is the left child of its parent, and the parent is also the left child of the grandparent.
- Left Zig-Zig: Performed when the target node is the right child of its parent, and the parent is also the right child of the grandparent.
3. Zig-Zag Rotation (Double Rotation)
Zig-Zag is a double rotation used when the target node is a left child, and its parent is a right child, or vice versa.
- Right Zig-Zag: The target node is the right child of the parent, and the parent is the left child of the grandparent.
- Left Zig-Zag: The target node is the left child of the parent, and the parent is the right child of the grandparent.
Splay Operation
The splay function brings a target node to the root of the tree. The function recursively applies the appropriate rotations based on the node’s position relative to its parent and grandparent.
Operations on a Splay Tree
Insertion
When inserting a new node, we first splay the tree to bring the closest node to the root. Then we insert the new node in the appropriate position and make it the new root.
Deletion
For deletion, we first splay the target node to the root. If the node exists, we then split the tree into two subtrees (left and right). The left subtree becomes the new root, with the right subtree reattached to it.
Searching
In a splay tree, searching for a node involves splaying it to the root if it exists. If the key is found, it’s returned as the new root.
In this example, the tree reorganizes itself with each operation, keeping frequently accessed nodes closer to the root.
Graphs
NOTE
A graph is a collection of nodes (or vertices) connected by edges (or arcs). Graphs are used to model pairwise relationships between objects, making them fundamental in representing networks, social graphs, and more.
- Vertices (Nodes): The fundamental units or points in a graph.
- Edges (Links): The connections between vertices, which can be directed or undirected.
- Degree: The number of edges incident to a vertex.
- Weight: A value associated with each edge, typically used in weighted graphs to represent the cost or distance between nodes.
- Connectedness: A graph is connected if there is a path between every pair of vertices.
- Directed and Undirected: In a directed graph (digraph), edges have a direction; in an undirected graph, edges do not.
- Cycle: A cycle is a path that starts and ends at the same vertex without traversing any edge more than once.
Types of Graphs, Applications, and Representations
- Undirected Graph: No direction is associated with edges (edges represent bidirectional relationships).
- Directed Graph (Digraph): Edges have a direction, represented as arrows.
- Weighted Graph: Edges have weights (values), commonly used for problems like shortest paths.
- Bipartite Graph: The graph’s vertices can be divided into two disjoint sets such that no two vertices within the same set are adjacent.
- Complete Graph: Every pair of distinct vertices is connected by a unique edge.
Example
- Social Networks: Modeling relationships and interactions.
- Routing Algorithms: Used in GPS systems, internet traffic management.
- Recommendation Systems: Suggesting products or services based on user behavior.
- Computer Networks: Representing communication between devices.
Graphs are commonly used to model network topologies, where the nodes represent devices (e.g., computers, routers) and the edges represent communication links. Graph traversal techniques such as DFS and BFS are used to explore the network, find the shortest path, or detect loops.
Representing Graphs
Info
Graphs can be represented in two primary ways:
- Adjacency Matrix: A 2D matrix where each cell (i, j) represents an edge between vertices i and j.
- Adjacency List: A collection of lists where each vertex has a list of adjacent vertices.
Using Adjacency Matrix
An adjacency matrix for a graph with (V) vertices is a ( V times V ) matrix where:
matrix[i][j] = 1
if there is an edge between vertexi
and vertexj
.matrix[i][j] = 0
otherwise.
For example, the adjacency matrix for the following graph:
Would look like this (undirected graph):
Using Adjacency List
An adjacency list stores for each vertex a list of its adjacent vertices. For the same graph, the adjacency list representation would be:
Graph Traversal Algorithms
Graph traversal is the process of visiting each vertex in the graph. This is crucial for searching, pathfinding, or analysis tasks.
Depth First Search (DFS)
DFS explores as far as possible along each branch before backtracking. It uses a stack (or recursion) to explore a vertex and then move to the deepest possible vertex before backtracking.
DFS Algorithm:
- Choose a starting vertex and mark it as visited.
- Visit all adjacent vertices that haven’t been visited.
- Recursively visit the neighbors of the current vertex.
- If all neighbors are visited, backtrack to the previous vertex.
DFS Example:
For a graph:
Starting from vertex 1
, the traversal is: 1 -> 2 -> 4 -> 3 -> 5
.
Breadth First Search (BFS)
BFS explores the graph level by level, visiting all neighbors of a vertex before moving to the next depth level. It uses a queue to manage the vertices to visit.
BFS Algorithm:
- Insert the starting vertex into the queue and mark it as visited.
- While the queue is not empty:
- Dequeue a vertex.
- Visit all unvisited adjacent vertices and enqueue them.
- Repeat until the queue is empty.
BFS Example:
For the same graph:
Starting from vertex 1
, the traversal is: 1 -> 2 -> 3 -> 4 -> 5
.
Connectivity of a Graph
A graph is connected if there is a path between every pair of vertices. If a graph is not connected, it consists of multiple disconnected components.
Check for connectivity:
- Perform a DFS or BFS from an arbitrary vertex.
- If all vertices are visited, the graph is connected.
- If some vertices are unreachable, the graph is disconnected.
Finding Path in a Network
To find a path between two vertices in a graph (e.g., in a network), BFS is typically used, as it guarantees the shortest path in an unweighted graph.
Path Finding Example: For the graph:
To find a path between 1
and 3
, use BFS, and the shortest path would be 1 -> 2 -> 3
.