📘 PART 2: ADVANCED DATA STRUCTURES
🎯 General Objectives
- Master advanced data structures such as Stack, Queue, Binary Tree, Hash Table.
- Understand how to implement and apply each data structure.
- Know how to choose the appropriate data structure for each problem.
🧑🏫 Lesson 1: Stack
Concept of Stack
- Stack is a LIFO (Last In First Out) data structure.
- Basic operations: push (add to top), pop (remove from top), peek (view top).
Stack Implementation using Array
java
public class ArrayStack {
private int maxSize;
private int[] stackArray;
private int top;
// Initialize stack
public ArrayStack(int size) {
maxSize = size;
stackArray = new int[maxSize];
top = -1; // Stack is empty
}
// Add element to top of stack
public void push(int value) {
if (top == maxSize - 1) {
System.out.println("Stack is full!");
return;
}
stackArray[++top] = value;
}
// Remove element from top of stack
public int pop() {
if (top == -1) {
System.out.println("Stack is empty!");
return -1;
}
return stackArray[top--];
}
// View element at top without removing
public int peek() {
if (top == -1) {
System.out.println("Stack is empty!");
return -1;
}
return stackArray[top];
}
// Check if stack is empty
public boolean isEmpty() {
return (top == -1);
}
// Check if stack is full
public boolean isFull() {
return (top == maxSize - 1);
}
}Stack Implementation using Linked List
java
public class LinkedStack {
private class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
private Node top;
public LinkedStack() {
top = null;
}
// Add to top of stack
public void push(int value) {
Node newNode = new Node(value);
newNode.next = top;
top = newNode;
}
// Remove from top of stack
public int pop() {
if (isEmpty()) {
System.out.println("Stack is empty!");
return -1;
}
int value = top.data;
top = top.next;
return value;
}
// View top of stack
public int peek() {
if (isEmpty()) {
System.out.println("Stack is empty!");
return -1;
}
return top.data;
}
// Check if stack is empty
public boolean isEmpty() {
return top == null;
}
}Applications of Stack
Checking Balanced Parentheses
java
public boolean isBalanced(String expression) {
LinkedStack stack = new LinkedStack();
for (char c : expression.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else if (c == ')' || c == ']' || c == '}') {
if (stack.isEmpty()) {
return false;
}
char top = (char) stack.pop();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
}
}
return stack.isEmpty();
}Reversing a String
java
public String reverse(String str) {
LinkedStack stack = new LinkedStack();
// Push all characters to stack
for (char c : str.toCharArray()) {
stack.push(c);
}
// Pop each character from stack to create reversed string
StringBuilder reversed = new StringBuilder();
while (!stack.isEmpty()) {
reversed.append((char)stack.pop());
}
return reversed.toString();
}Converting Infix to Postfix Expression
java
public String infixToPostfix(String infix) {
StringBuilder postfix = new StringBuilder();
java.util.Stack<Character> stack = new java.util.Stack<>();
for (char c : infix.toCharArray()) {
// If operand, add to result string
if (Character.isLetterOrDigit(c)) {
postfix.append(c);
}
// If opening parenthesis, push to stack
else if (c == '(') {
stack.push(c);
}
// If closing parenthesis, pop from stack until opening parenthesis
else if (c == ')') {
while (!stack.isEmpty() && stack.peek() != '(') {
postfix.append(stack.pop());
}
stack.pop(); // Remove '('
}
// If operator, compare precedence with operator at top of stack
else {
while (!stack.isEmpty() && precedence(c) <= precedence(stack.peek())) {
postfix.append(stack.pop());
}
stack.push(c);
}
}
// Pop remaining operators in stack to result string
while (!stack.isEmpty()) {
postfix.append(stack.pop());
}
return postfix.toString();
}
// Determine operator precedence
private int precedence(char ch) {
switch (ch) {
case '+':
case '-':
return 1;
case '*':
case '/':
return 2;
case '^':
return 3;
}
return -1;
}🧑🏫 Lesson 2: Queue
Concept of Queue
- Queue is a FIFO (First In First Out) data structure.
- Basic operations: enqueue (add to rear), dequeue (remove from front), peek (view front).
Queue Implementation using Array (Circular Queue)
java
public class CircularQueue {
private int maxSize;
private int[] queueArray;
private int front;
private int rear;
private int nItems;
public CircularQueue(int size) {
maxSize = size;
queueArray = new int[maxSize];
front = 0;
rear = -1;
nItems = 0;
}
// Add to rear of queue
public void enqueue(int value) {
if (isFull()) {
System.out.println("Queue is full!");
return;
}
// Increment rear and wrap around if needed
rear = (rear + 1) % maxSize;
queueArray[rear] = value;
nItems++;
}
// Remove from front of queue
public int dequeue() {
if (isEmpty()) {
System.out.println("Queue is empty!");
return -1;
}
int temp = queueArray[front];
front = (front + 1) % maxSize;
nItems--;
return temp;
}
// View element at front of queue
public int peek() {
if (isEmpty()) {
System.out.println("Queue is empty!");
return -1;
}
return queueArray[front];
}
// Check if queue is empty
public boolean isEmpty() {
return (nItems == 0);
}
// Check if queue is full
public boolean isFull() {
return (nItems == maxSize);
}
// Current size of queue
public int size() {
return nItems;
}
}Queue Implementation using Linked List
java
public class LinkedQueue {
private class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
private Node front;
private Node rear;
public LinkedQueue() {
front = null;
rear = null;
}
// Add to rear of queue
public void enqueue(int value) {
Node newNode = new Node(value);
// If queue is empty
if (rear == null) {
front = newNode;
rear = newNode;
return;
}
// Add after rear and update rear
rear.next = newNode;
rear = newNode;
}
// Remove from front of queue
public int dequeue() {
if (isEmpty()) {
System.out.println("Queue is empty!");
return -1;
}
// Store value to return
int value = front.data;
// Move front
front = front.next;
// If front becomes null, update rear as well
if (front == null) {
rear = null;
}
return value;
}
// View element at front of queue
public int peek() {
if (isEmpty()) {
System.out.println("Queue is empty!");
return -1;
}
return front.data;
}
// Check if queue is empty
public boolean isEmpty() {
return front == null;
}
}Applications of Queue
BFS Algorithm (Breadth-First Search)
java
public void BFS(Graph graph, int startVertex) {
// Mark all vertices as not visited
boolean[] visited = new boolean[graph.getVertexCount()];
// Create queue
LinkedQueue queue = new LinkedQueue();
// Mark current vertex as visited
visited[startVertex] = true;
queue.enqueue(startVertex);
while (!queue.isEmpty()) {
// Dequeue a vertex and print it
int vertex = queue.dequeue();
System.out.print(vertex + " ");
// Get all adjacent vertices of the dequeued vertex
// If adjacent has not been visited, mark it visited and enqueue it
for (int adjVertex : graph.getAdjVertices(vertex)) {
if (!visited[adjVertex]) {
visited[adjVertex] = true;
queue.enqueue(adjVertex);
}
}
}
}Service Queue Simulation
java
public class CustomerService {
private LinkedQueue queue;
private int maxWaitTime;
public CustomerService() {
queue = new LinkedQueue();
maxWaitTime = 0;
}
// New customer arrival
public void customerArrival(int customerId, int arrivalTime) {
System.out.println("Customer " + customerId + " arrived at " + arrivalTime);
queue.enqueue(customerId);
}
// Serve customer
public void serveCustomer(int currentTime) {
if (!queue.isEmpty()) {
int customerId = queue.dequeue();
int waitTime = currentTime - customerId;
maxWaitTime = Math.max(maxWaitTime, waitTime);
System.out.println("Serving customer " + customerId + " at " + currentTime);
System.out.println("Wait time: " + waitTime);
} else {
System.out.println("No customers waiting.");
}
}
// Get max wait time
public int getMaxWaitTime() {
return maxWaitTime;
}
}Level-order traversal in Binary Tree
java
public void levelOrderTraversal(Node root) {
if (root == null) {
return;
}
LinkedQueue queue = new LinkedQueue();
queue.enqueue(root);
while (!queue.isEmpty()) {
Node current = (Node)queue.dequeue();
System.out.print(current.data + " ");
if (current.left != null) {
queue.enqueue(current.left);
}
if (current.right != null) {
queue.enqueue(current.right);
}
}
}🧑🏫 Lesson 3: Doubly and Circular Linked Lists
Doubly Linked List
- Each node contains data and two pointers: one to previous node, one to next node.
- Allows traversal in both directions.
java
public class DoublyLinkedList {
private class Node {
int data;
Node prev;
Node next;
public Node(int data) {
this.data = data;
this.prev = null;
this.next = null;
}
}
private Node head;
private Node tail;
public DoublyLinkedList() {
head = null;
tail = null;
}
// Insert at front
public void insertAtFront(int data) {
Node newNode = new Node(data);
// If list is empty
if (head == null) {
head = newNode;
tail = newNode;
} else {
newNode.next = head;
head.prev = newNode;
head = newNode;
}
}
// Insert at end
public void insertAtEnd(int data) {
Node newNode = new Node(data);
// If list is empty
if (tail == null) {
head = newNode;
tail = newNode;
} else {
tail.next = newNode;
newNode.prev = tail;
tail = newNode;
}
}
// Remove from front
public int removeFromFront() {
if (head == null) {
System.out.println("List is empty!");
return -1;
}
int data = head.data;
// If only one node
if (head == tail) {
head = null;
tail = null;
} else {
head = head.next;
head.prev = null;
}
return data;
}
// Remove from end
public int removeFromEnd() {
if (tail == null) {
System.out.println("List is empty!");
return -1;
}
int data = tail.data;
// If only one node
if (head == tail) {
head = null;
tail = null;
} else {
tail = tail.prev;
tail.next = null;
}
return data;
}
// Display list from front to end
public void displayForward() {
Node current = head;
System.out.print("null <-> ");
while (current != null) {
System.out.print(current.data + " <-> ");
current = current.next;
}
System.out.println("null");
}
// Display list from end to front
public void displayBackward() {
Node current = tail;
System.out.print("null <-> ");
while (current != null) {
System.out.print(current.data + " <-> ");
current = current.prev;
}
System.out.println("null");
}
}Circular Linked List
- Last node points to first node, forming a circle.
- Useful for applications needing continuous traversal.
java
public class CircularLinkedList {
private class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
private Node head;
private Node tail;
public CircularLinkedList() {
head = null;
tail = null;
}
// Insert at front
public void insertAtFront(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode;
tail = newNode;
tail.next = head; // Create circular link
} else {
newNode.next = head;
head = newNode;
tail.next = head; // Update circular link
}
}
// Insert at end
public void insertAtEnd(int data) {
Node newNode = new Node(data);
if (tail == null) {
head = newNode;
tail = newNode;
tail.next = head; // Create circular link
} else {
tail.next = newNode;
tail = newNode;
tail.next = head; // Update circular link
}
}
// Remove from front
public int removeFromFront() {
if (head == null) {
System.out.println("List is empty!");
return -1;
}
int data = head.data;
// If only one node
if (head == tail) {
head = null;
tail = null;
} else {
head = head.next;
tail.next = head; // Update circular link
}
return data;
}
// Display list
public void display() {
if (head == null) {
System.out.println("List is empty!");
return;
}
Node current = head;
do {
System.out.print(current.data + " -> ");
current = current.next;
} while (current != head);
System.out.println(" (back to " + head.data + ")");
}
}Applications of Doubly and Circular Linked Lists
Browser History (Doubly Linked List)
java
public class BrowserHistory {
private DoublyLinkedList history;
private Node currentPage;
private class Node {
String url;
Node prev;
Node next;
public Node(String url) {
this.url = url;
this.prev = null;
this.next = null;
}
}
public BrowserHistory(String homepage) {
currentPage = new Node(homepage);
}
// Visit new page
public void visit(String url) {
Node newPage = new Node(url);
newPage.prev = currentPage;
currentPage.next = newPage;
currentPage = newPage;
System.out.println("Visited: " + url);
}
// Go back
public String back() {
if (currentPage.prev != null) {
currentPage = currentPage.prev;
return currentPage.url;
} else {
return currentPage.url; // Cannot go back
}
}
// Go forward
public String forward() {
if (currentPage.next != null) {
currentPage = currentPage.next;
return currentPage.url;
} else {
return currentPage.url; // Cannot go forward
}
}
}Round Robin Scheduling (Circular Linked List)
java
public class RoundRobinScheduler {
private CircularLinkedList processes;
public RoundRobinScheduler() {
processes = new CircularLinkedList();
}
// Add new process
public void addProcess(int processId) {
processes.insertAtEnd(processId);
System.out.println("Added process " + processId);
}
// Run round robin scheduling
public void run(int timeQuantum) {
System.out.println("Starting Round-Robin scheduling with time quantum = " + timeQuantum);
// Simulate 10 scheduling cycles
for (int i = 0; i < 10; i++) {
int processId = processes.removeFromFront();
System.out.println("Running process " + processId + " for " + timeQuantum + "ms");
// After running, put process back to end of queue
processes.insertAtEnd(processId);
// Simulate running time
try {
Thread.sleep(timeQuantum * 10); // Simulate slow
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}🧑🏫 Lesson 4: Binary Tree
Concept of Tree
- Tree is a hierarchical data structure, consisting of nodes and edges.
- Binary Tree: each node has at most 2 children (left and right).
Binary Search Tree (BST)
- For every node: all values in left subtree < node value, all values in right subtree > node value.
- Allows fast search: O(log n) if balanced, O(n) in worst case.
java
public class BinarySearchTree {
private class Node {
int data;
Node left;
Node right;
public Node(int data) {
this.data = data;
this.left = null;
this.right = null;
}
}
private Node root;
public BinarySearchTree() {
root = null;
}
// Insert a value into BST
public void insert(int value) {
root = insertRec(root, value);
}
private Node insertRec(Node root, int value) {
// If tree is empty, return new node
if (root == null) {
return new Node(value);
}
// Otherwise, recur down the tree
if (value < root.data) {
root.left = insertRec(root.left, value);
} else if (value > root.data) {
root.right = insertRec(root.right, value);
}
return root;
}
// Search a value in BST
public boolean search(int value) {
return searchRec(root, value);
}
private boolean searchRec(Node root, int value) {
// Base condition: tree is empty or value found
if (root == null) {
return false;
}
if (root.data == value) {
return true;
}
// Value is smaller -> search left subtree
if (value < root.data) {
return searchRec(root.left, value);
}
// Otherwise, search right subtree
return searchRec(root.right, value);
}
// Delete a value from BST
public void delete(int value) {
root = deleteRec(root, value);
}
private Node deleteRec(Node root, int value) {
// Base condition
if (root == null) {
return root;
}
// Find node to delete
if (value < root.data) {
root.left = deleteRec(root.left, value);
} else if (value > root.data) {
root.right = deleteRec(root.right, value);
} else {
// Node with only one child or no child
if (root.left == null) {
return root.right;
} else if (root.right == null) {
return root.left;
}
// Node with two children: Get the inorder successor (smallest in the right subtree)
root.data = minValue(root.right);
// Delete the inorder successor
root.right = deleteRec(root.right, root.data);
}
return root;
}
private int minValue(Node root) {
int minValue = root.data;
while (root.left != null) {
minValue = root.left.data;
root = root.left;
}
return minValue;
}
// Inorder traversal: left -> root -> right
public void inorder() {
inorderRec(root);
System.out.println();
}
private void inorderRec(Node root) {
if (root != null) {
inorderRec(root.left);
System.out.print(root.data + " ");
inorderRec(root.right);
}
}
// Preorder traversal: root -> left -> right
public void preorder() {
preorderRec(root);
System.out.println();
}
private void preorderRec(Node root) {
if (root != null) {
System.out.print(root.data + " ");
preorderRec(root.left);
preorderRec(root.right);
}
}
// Postorder traversal: left -> right -> root
public void postorder() {
postorderRec(root);
System.out.println();
}
private void postorderRec(Node root) {
if (root != null) {
postorderRec(root.left);
postorderRec(root.right);
System.out.print(root.data + " ");
}
}
}Balancing Binary Tree (we will learn about balanced trees in later part)
- Balanced binary tree has height ~log(n).
- Types of balanced trees: AVL, Red-Black Tree, B-Tree.
Applications of Binary Tree
- Dictionaries and symbol tables.
- File systems.
- Expression evaluation.
- Huffman coding.
- Search and sort algorithms.
🧑🏫 Lesson 5: Hash Table
Concept of Hash Table
- Hash table is a data structure that stores key-value pairs.
- Uses a hash function to convert key into an index in an array.
- Allows insert, delete, search with O(1) complexity in average case.
Hash Function
- Converts key to index in array.
- Properties of a good hash function:
- Fast computation.
- Uniform distribution of keys across table range.
- Minimize collisions.
Collision Resolution
Chaining
- Each position in the table contains a linked list of elements with the same hash code.
Open Addressing
- Linear Probing: Find next position in table.
- Quadratic Probing: Find position by squared distance.
- Double Hashing: Use second hash function to calculate distance.
Hash Table Implementation using Chaining
java
public class HashTable {
private class HashNode {
String key;
int value;
HashNode next;
public HashNode(String key, int value) {
this.key = key;
this.value = value;
this.next = null;
}
}
private HashNode[] buckets;
private int numBuckets;
private int size;
public HashTable() {
this(10); // Default size
}
public HashTable(int capacity) {
numBuckets = capacity;
buckets = new HashNode[numBuckets];
size = 0;
}
// Hash function
private int hashFunction(String key) {
int hashCode = key.hashCode();
return Math.abs(hashCode) % numBuckets;
}
// Hash table size
public int size() {
return size;
}
// Check if hash table is empty
public boolean isEmpty() {
return size() == 0;
}
// Add element to hash table
public void put(String key, int value) {
// Find bucket index from key
int bucketIndex = hashFunction(key);
HashNode head = buckets[bucketIndex];
// Check if key already exists, then update value
while (head != null) {
if (head.key.equals(key)) {
head.value = value;
return;
}
head = head.next;
}
// Add new node to head of linked list
size++;
head = buckets[bucketIndex];
HashNode newNode = new HashNode(key, value);
newNode.next = head;
buckets[bucketIndex] = newNode;
}
// Get value from key
public int get(String key) {
// Find bucket index from key
int bucketIndex = hashFunction(key);
HashNode head = buckets[bucketIndex];
// Search key in linked list
while (head != null) {
if (head.key.equals(key)) {
return head.value;
}
head = head.next;
}
// Key not found
return -1;
}
// Remove element with specific key
public int remove(String key) {
// Find bucket index from key
int bucketIndex = hashFunction(key);
HashNode head = buckets[bucketIndex];
HashNode prev = null;
// Search key in linked list
while (head != null) {
if (head.key.equals(key)) {
break;
}
prev = head;
head = head.next;
}
// If key not found
if (head == null) {
return -1;
}
// Decrease size
size--;
// Remove node
if (prev != null) {
prev.next = head.next;
} else {
buckets[bucketIndex] = head.next;
}
return head.value;
}
// Display hash table
public void display() {
for (int i = 0; i < numBuckets; i++) {
System.out.print("Bucket " + i + ": ");
HashNode current = buckets[i];
while (current != null) {
System.out.print("(" + current.key + ":" + current.value + ") ");
current = current.next;
}
System.out.println();
}
}
}Applications of Hash Table
- Dictionaries and caches.
- Symbol tables in compilers.
- Database indexing.
- Implementing Sets and Maps.
- File storage and retrieval.
java
// Example using hash table as dictionary
public class Dictionary {
private HashTable hashTable;
public Dictionary() {
hashTable = new HashTable(100);
}
// Add word to dictionary
public void addWord(String word, int meaning) {
hashTable.put(word, meaning);
System.out.println("Added word \"" + word + "\" to dictionary.");
}
// Lookup word
public void lookupWord(String word) {
int meaning = hashTable.get(word);
if (meaning != -1) {
System.out.println("Meaning of \"" + word + "\": " + meaning);
} else {
System.out.println("Word \"" + word + "\" not found in dictionary.");
}
}
// Remove word
public void removeWord(String word) {
int result = hashTable.remove(word);
if (result != -1) {
System.out.println("Removed word \"" + word + "\" from dictionary.");
} else {
System.out.println("Word \"" + word + "\" not found in dictionary.");
}
}
// Display all words
public void displayDictionary() {
System.out.println("Dictionary:");
hashTable.display();
}
}🧪 FINAL PROJECT: Arithmetic Expression Evaluator
Problem Description
Write a program that allows users to:
- Input an arithmetic expression as a string (e.g., "3 + 4 * 2 - (6 / 3)")
- Convert expression from infix to postfix notation
- Evaluate expression and return result
- Handle exceptions like syntax errors, division by zero, etc.
- Support operators +, -, *, /, ^, (), with integers
Program Output (Example)
text
ARITHMETIC EXPRESSION EVALUATOR
-----------------------------------
Enter expression (type 'exit' to quit): 3 + 4 * 2 - (6 / 3)
Infix Expression: 3 + 4 * 2 - (6 / 3)
Postfix Expression: 3 4 2 * + 6 3 / -
Result: 9
Enter expression (type 'exit' to quit): 5 * (3 + 2) - 4 ^ 2
Infix Expression: 5 * (3 + 2) - 4 ^ 2
Postfix Expression: 5 3 2 + * 4 2 ^ -
Result: 9
Enter expression (type 'exit' to quit): 10 / (3 - 3)
Infix Expression: 10 / (3 - 3)
Postfix Expression: 10 3 3 - /
Error: Division by zero!
Enter expression (type 'exit' to quit): exit
Thank you for using the program!