Data Structures Basics: Stack, Queue, Heap, Array, Tree, Graph
Data structures are fundamental concepts in computer science that organize and manage data. Choosing the right data structure significantly influences the efficiency of algorithms.
What are Data Structures?
Data structures define how data is stored, organized, and accessed. They determine the complexity of operations such as insertion, deletion, and searching.
Classification of Data Structures
- Linear Structures: Array, Stack, Queue, Linked List
- Hierarchical Structures: Tree, Heap
- Network Structures: Graph
- Hash-based Structures: Hash Table, Hash Set
Array (Field)
Basics
Arrays are the simplest data structure with direct index access.
public class ArrayDemo {
public static void main(String[] args) {
// Static Array
int[] numbers = new int[5];
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
numbers[3] = 40;
numbers[4] = 50;
// Dynamic Array (ArrayList)
List<Integer> dynamicArray = new ArrayList<>();
dynamicArray.add(10);
dynamicArray.add(20);
dynamicArray.add(30);
// Array Operations
System.out.println("Element at index 2: " + numbers[2]); // O(1)
// Linear Search
int index = linearSearch(numbers, 30); // O(n)
System.out.println("30 found at index: " + index);
// Sorting with Binary Search
Arrays.sort(numbers); // O(n log n)
int sortedIndex = Arrays.binarySearch(numbers, 30); // O(log n)
System.out.println("30 (sorted) at index: " + sortedIndex);
}
// Linear search in array
public static int linearSearch(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i;
}
}
return -1; // Not found
}
}
Dynamic Array Implementation
public class DynamicArray<T> {
private Object[] data;
private int size;
private int capacity;
public DynamicArray() {
this.capacity = 10;
this.data = new Object[capacity];
this.size = 0;
}
public void add(T element) {
if (size >= capacity) {
resize();
}
data[size++] = element;
}
@SuppressWarnings("unchecked")
public T get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
return (T) data[index];
}
public void set(int index, T element) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
data[index] = element;
}
public void remove(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
// Shift elements to the left
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
size--;
}
private void resize() {
capacity *= 2;
data = Arrays.copyOf(data, capacity);
}
public int size() { return size; }
@Override
public String toString() {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < size; i++) {
sb.append(data[i]);
if (i < size - 1) sb.append(", ");
}
sb.append("]");
return sb.toString();
}
}
Stack (Stack)
LIFO Principle
Stacks follow the Last-In-First-Out (LIFO) principle.
public class StackDemo {
public static void main(String[] args) {
// Java Stack
Stack<Integer> stack = new Stack<>();
// Push Operations
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println("Stack: " + stack);
System.out.println("Top Element: " + stack.peek()); // 30
// Pop Operation
int popped = stack.pop(); // Removes 30
System.out.println("Popped: " + popped);
System.out.println("Stack after pop: " + stack);
// Practical Application: Parenthesis Checking
String expression = "{[()]}";
boolean isBalanced = isBalancedParentheses(expression);
System.out.println("Expression balanced: " + isBalanced);
}
// Parenthesis checking with stack
public static boolean isBalancedParentheses(String expression) {
Stack<Character> stack = new Stack<>();
for (char c : expression.toCharArray()) {
switch (c) {
case '(':
case '[':
case '{':
stack.push(c);
break;
case ')':
if (stack.isEmpty() || stack.pop() != '(') return false;
break;
case ']':
if (stack.isEmpty() || stack.pop() != '[') return false;
break;
case '}':
if (stack.isEmpty() || stack.pop() != '{') return false;
break;
}
}
return stack.isEmpty();
}
}
Stack Implementation
public class ArrayStack<T> {
private Object[] data;
private int top;
private int capacity;
public ArrayStack() {
this.capacity = 10;
this.data = new Object[capacity];
this.top = -1;
}
public void push(T element) {
if (isFull()) {
resize();
}
data[++top] = element;
}
@SuppressWarnings("unchecked")
public T pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
return (T) data[top--];
}
@SuppressWarnings("unchecked")
public T peek() {
if (isEmpty()) {
throw new EmptyStackException();
}
return (T) data[top];
}
public boolean isEmpty() {
return top == -1;
}
public boolean isFull() {
return top == capacity - 1;
}
private void resize() {
capacity *= 2;
data = Arrays.copyOf(data, capacity);
}
public int size() {
return top + 1;
}
}
Queue (Queue)
FIFO Principle
Queues follow the First-In-First-Out (FIFO) principle.
public class QueueDemo {
public static void main(String[] args) {
// Java Queue with LinkedList
Queue<Integer> queue = new LinkedList<>();
// Enqueue Operations
queue.offer(10);
queue.offer(20);
queue.offer(30);
System.out.println("Queue: " + queue);
System.out.println("Front Element: " + queue.peek()); // 10
// Dequeue Operation
int dequeued = queue.poll(); // Removes 10
System.out.println("Dequeued: " + dequeued);
System.out.println("Queue after dequeue: " + queue);
// Practical Application: Breadth-First Search
Graph graph = createSampleGraph();
List<Integer> bfsResult = bfs(graph, 0);
System.out.println("BFS Traversal: " + bfsResult);
}
// BFS with Queue
public static List<Integer> bfs(Graph graph, int startNode) {
List<Integer> result = new ArrayList<>();
boolean[] visited = new boolean[graph.getVertexCount()];
Queue<Integer> queue = new LinkedList<>();
visited[startNode] = true;
queue.offer(startNode);
while (!queue.isEmpty()) {
int current = queue.poll();
result.add(current);
for (int neighbor : graph.getNeighbors(current)) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.offer(neighbor);
}
}
}
return result;
}
private static Graph createSampleGraph() {
// Implementation for sample graph
return new Graph(6);
}
}
Queue Implementation
public class ArrayQueue<T> {
private Object[] data;
private int front;
private int rear;
private int size;
private int capacity;
public ArrayQueue() {
this.capacity = 10;
this.data = new Object[capacity];
this.front = 0;
this.rear = -1;
this.size = 0;
}
public void enqueue(T element) {
if (isFull()) {
resize();
}
rear = (rear + 1) % capacity;
data[rear] = element;
size++;
}
@SuppressWarnings("unchecked")
public T dequeue() {
if (isEmpty()) {
throw new NoSuchElementException();
}
T element = (T) data[front];
front = (front + 1) % capacity;
size--;
return element;
}
@SuppressWarnings("unchecked")
public T peek() {
if (isEmpty()) {
throw new NoSuchElementException();
}
return (T) data[front];
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == capacity;
}
private void resize() {
int newCapacity = capacity * 2;
Object[] newData = new Object[newCapacity];
for (int i = 0; i < size; i++) {
newData[i] = data[(front + i) % capacity];
}
data = newData;
front = 0;
rear = size - 1;
capacity = newCapacity;
}
public int size() {
return size;
}
}
Heap (Heap)
Priority Queue
Heaps are special tree structures for Priority Queues.
public class HeapDemo {
public static void main(String[] args) {
// Min-Heap (Priority Queue)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(50);
minHeap.offer(20);
minHeap.offer(30);
minHeap.offer(10);
minHeap.offer(40);
System.out.println("Min-Heap: " + minHeap);
System.out.println("Smallest element: " + minHeap.peek()); // 10
System.out.println("Elements in order:");
while (!minHeap.isEmpty()) {
System.out.print(minHeap.poll() + " "); // 10 20 30 40 50
}
// Max-Heap
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
maxHeap.offer(50);
maxHeap.offer(20);
maxHeap.offer(30);
maxHeap.offer(10);
maxHeap.offer(40);
System.out.println("\nMax-Heap: " + maxHeap);
System.out.println("Largest element: " + maxHeap.peek()); // 50
// Heap Sort
int[] array = {50, 20, 30, 10, 40};
heapSort(array);
System.out.println("Heap Sort result: " + Arrays.toString(array));
}
// Heap Sort Algorithm
public static void heapSort(int[] array) {
int n = array.length;
// Build max heap
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(array, n, i);
}
// Extract elements from heap
for (int i = n - 1; i > 0; i--) {
// Move current root to end
int temp = array[0];
array[0] = array[i];
array[i] = temp;
// heapify root element
heapify(array, i, 0);
}
}
private static void heapify(int[] array, int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && array[left] > array[largest]) {
largest = left;
}
if (right < n && array[right] > array[largest]) {
largest = right;
}
if (largest != i) {
int swap = array[i];
array[i] = array[largest];
array[largest] = swap;
heapify(array, n, largest);
}
}
}
Min-Heap Implementation
public class MinHeap<T extends Comparable<T>> {
private List<T> heap;
public MinHeap() {
this.heap = new ArrayList<>();
}
public void insert(T element) {
heap.add(element);
heapifyUp(heap.size() - 1);
}
public T extractMin() {
if (heap.isEmpty()) {
throw new NoSuchElementException();
}
T min = heap.get(0);
T last = heap.remove(heap.size() - 1);
if (!heap.isEmpty()) {
heap.set(0, last);
heapifyDown(0);
}
return min;
}
public T peek() {
if (heap.isEmpty()) {
throw new NoSuchElementException();
}
return heap.get(0);
}
private void heapifyUp(int index) {
while (index > 0) {
int parentIndex = (index - 1) / 2;
if (heap.get(index).compareTo(heap.get(parentIndex)) >= 0) {
break;
}
swap(index, parentIndex);
index = parentIndex;
}
}
private void heapifyDown(int index) {
int size = heap.size();
while (true) {
int leftChild = 2 * index + 1;
int rightChild = 2 * index + 2;
int smallest = index;
if (leftChild < size && heap.get(leftChild).compareTo(heap.get(smallest)) < 0) {
smallest = leftChild;
}
if (rightChild < size && heap.get(rightChild).compareTo(heap.get(smallest)) < 0) {
smallest = rightChild;
}
if (smallest == index) {
break;
}
swap(index, smallest);
index = smallest;
}
}
private void swap(int i, int j) {
T temp = heap.get(i);
heap.set(i, heap.get(j));
heap.set(j, temp);
}
public boolean isEmpty() {
return heap.isEmpty();
}
public int size() {
return heap.size();
}
}
Tree Structures
Binary Search Tree
public class BinarySearchTree<T extends Comparable<T>> {
private class Node {
T data;
Node left;
Node right;
Node(T data) {
this.data = data;
this.left = null;
this.right = null;
}
}
private Node root;
public void insert(T data) {
root = insert(root, data);
}
private Node insert(Node node, T data) {
if (node == null) {
return new Node(data);
}
if (data.compareTo(node.data) < 0) {
node.left = insert(node.left, data);
} else if (data.compareTo(node.data) > 0) {
node.right = insert(node.right, data);
}
return node;
}
public boolean search(T data) {
return search(root, data);
}
private boolean search(Node node, T data) {
if (node == null) {
return false;
}
if (data.compareTo(node.data) == 0) {
return true;
} else if (data.compareTo(node.data) < 0) {
return search(node.left, data);
} else {
return search(node.right, data);
}
}
public void inorderTraversal() {
inorderTraversal(root);
System.out.println();
}
private void inorderTraversal(Node node) {
if (node != null) {
inorderTraversal(node.left);
System.out.print(node.data + " ");
inorderTraversal(node.right);
}
}
public void preorderTraversal() {
preorderTraversal(root);
System.out.println();
}
private void preorderTraversal(Node node) {
if (node != null) {
System.out.print(node.data + " ");
preorderTraversal(node.left);
preorderTraversal(node.right);
}
}
public void postorderTraversal() {
postorderTraversal(root);
System.out.println();
}
private void postorderTraversal(Node node) {
if (node != null) {
postorderTraversal(node.left);
postorderTraversal(node.right);
System.out.print(node.data + " ");
}
}
public int height() {
return height(root);
}
private int height(Node node) {
if (node == null) {
return 0;
}
int leftHeight = height(node.left);
int rightHeight = height(node.right);
return Math.max(leftHeight, rightHeight) + 1;
}
public T findMin() {
if (root == null) {
throw new NoSuchElementException();
}
Node current = root;
while (current.left != null) {
current = current.left;
}
return current.data;
}
public T findMax() {
if (root == null) {
throw new NoSuchElementException();
}
Node current = root;
while (current.right != null) {
current = current.right;
}
return current.data;
}
}
Tree Traversal Demo
public class TreeTraversalDemo {
public static void main(String[] args) {
BinarySearchTree<Integer> bst = new BinarySearchTree<>();
// Insert
bst.insert(50);
bst.insert(30);
bst.insert(70);
bst.insert(20);
bst.insert(40);
bst.insert(60);
bst.insert(80);
System.out.print("In-Order: ");
bst.inorderTraversal(); // 20 30 40 50 60 70 80
System.out.print("Pre-Order: ");
bst.preorderTraversal(); // 50 30 20 40 70 60 80
System.out.print("Post-Order: ");
bst.postorderTraversal(); // 20 40 30 60 80 70 50
System.out.println("Tree height: " + bst.height()); // 3
System.out.println("Minimum: " + bst.findMin()); // 20
System.out.println("Maximum: " + bst.findMax()); // 80
// Search
System.out.println("40 found: " + bst.search(40)); // true
System.out.println("90 found: " + bst.search(90)); // false
}
}
Graphs
Graph Representations
public class Graph {
private int vertexCount;
private List<List<Integer>> adjacencyList;
public Graph(int vertexCount) {
this.vertexCount = vertexCount;
this.adjacencyList = new ArrayList<>();
for (int i = 0; i < vertexCount; i++) {
adjacencyList.add(new ArrayList<>());
}
}
// Directed graph
public void addEdge(int source, int destination) {
if (source >= 0 && source < vertexCount &&
destination >= 0 && destination < vertexCount) {
adjacencyList.get(source).add(destination);
}
}
// Undirected graph
public void addUndirectedEdge(int source, int destination) {
addEdge(source, destination);
addEdge(destination, source);
}
public List<Integer> getNeighbors(int vertex) {
if (vertex >= 0 && vertex < vertexCount) {
return new ArrayList<>(adjacencyList.get(vertex));
}
return new ArrayList<>();
}
public int getVertexCount() {
return vertexCount;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < vertexCount; i++) {
sb.append(i).append(": ");
for (int neighbor : adjacencyList.get(i)) {
sb.append(neighbor).append(" ");
}
sb.append("\n");
}
return sb.toString();
}
}
Graph Algorithms
public class GraphAlgorithms {
// Depth-First Search (DFS)
public static List<Integer> dfs(Graph graph, int startNode) {
List<Integer> result = new ArrayList<>();
boolean[] visited = new boolean[graph.getVertexCount()];
dfsHelper(graph, startNode, visited, result);
return result;
}
private static void dfsHelper(Graph graph, int node, boolean[] visited, List<Integer> result) {
visited[node] = true;
result.add(node);
for (int neighbor : graph.getNeighbors(node)) {
if (!visited[neighbor]) {
dfsHelper(graph, neighbor, visited, result);
}
}
}
// Breadth-First Search (BFS)
public static List<Integer> bfs(Graph graph, int startNode) {
List<Integer> result = new ArrayList<>();
boolean[] visited = new boolean[graph.getVertexCount()];
Queue<Integer> queue = new LinkedList<>();
visited[startNode] = true;
queue.offer(startNode);
while (!queue.isEmpty()) {
int current = queue.poll();
result.add(current);
for (int neighbor : graph.getNeighbors(current)) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.offer(neighbor);
}
}
}
return result;
}
// Dijkstra's Algorithm (shortest paths)
public static Map<Integer, Integer> dijkstra(int[][] graph, int startNode) {
int n = graph.length;
int[] distances = new int[n];
boolean[] visited = new boolean[n];
Arrays.fill(distances, Integer.MAX_VALUE);
distances[startNode] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
pq.offer(new int[]{startNode, 0});
while (!pq.isEmpty()) {
int[] current = pq.poll();
int node = current[0];
int distance = current[1];
if (visited[node]) continue;
visited[node] = true;
for (int neighbor = 0; neighbor < n; neighbor++) {
if (graph[node][neighbor] > 0 && !visited[neighbor]) {
int newDistance = distance + graph[node][neighbor];
if (newDistance < distances[neighbor]) {
distances[neighbor] = newDistance;
pq.offer(new int[]{neighbor, newDistance});
}
}
}
}
Map<Integer, Integer> result = new HashMap<>();
for (int i = 0; i < n; i++) {
if (distances[i] != Integer.MAX_VALUE) {
result.put(i, distances[i]);
}
}
return result;
}
// Topological Sort
public static List<Integer> topologicalSort(Graph graph) {
int n = graph.getVertexCount();
int[] inDegree = new int[n];
// Calculate in-degree
for (int i = 0; i < n; i++) {
for (int neighbor : graph.getNeighbors(i)) {
inDegree[neighbor]++;
}
}
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}
List<Integer> result = new ArrayList<>();
while (!queue.isEmpty()) {
int current = queue.poll();
result.add(current);
for (int neighbor : graph.getNeighbors(current)) {
inDegree[neighbor]--;
if (inDegree[neighbor] == 0) {
queue.offer(neighbor);
}
}
}
return result;
}
}
Graph Demo
public class GraphDemo {
public static void main(String[] args) {
// Create graph
Graph graph = new Graph(6);
// Add edges
graph.addEdge(0, 1);
graph.addEdge(0, 2);
graph.addEdge(1, 3);
graph.addEdge(2, 4);
graph.addEdge(3, 5);
graph.addEdge(4, 5);
System.out.println("Graph:");
System.out.println(graph);
// Traversals
System.out.println("DFS: " + GraphAlgorithms.dfs(graph, 0));
System.out.println("BFS: " + GraphAlgorithms.bfs(graph, 0));
// Dijkstra's Algorithm
int[][] weightedGraph = {
{0, 4, 2, 0, 0, 0},
{4, 0, 1, 5, 0, 0},
{2, 1, 0, 8, 10, 0},
{0, 5, 8, 0, 2, 6},
{0, 0, 10, 2, 0, 3},
{0, 0, 0, 6, 3, 0}
};
Map<Integer, Integer> shortestPaths = GraphAlgorithms.dijkstra(weightedGraph, 0);
System.out.println("Shortest paths from node 0: " + shortestPaths);
// Topological sort for DAG
Graph dag = new Graph(4);
dag.addEdge(0, 1);
dag.addEdge(0, 2);
dag.addEdge(1, 3);
dag.addEdge(2, 3);
List<Integer> topologicalOrder = GraphAlgorithms.topologicalSort(dag);
System.out.println("Topological order: " + topologicalOrder);
}
}
Complexity Analysis
Time Complexity Comparison
| Data Structure | Access | Insert | Delete | Search |
|---|---|---|---|---|
| Array | O(1) | O(n) | O(n) | O(n) |
| Dynamic Array | O(1) | O(1)* | O(n) | O(n) |
| Linked List | O(n) | O(1)* | O(1)* | O(n) |
| Stack | O(1) | O(1) | O(1) | O(n) |
| Queue | O(1) | O(1) | O(1) | O(n) |
| Binary Search Tree | O(log n)* | O(log n)* | O(log n)* | O(log n)* |
| Heap | O(1) | O(log n) | O(log n) | O(n) |
| Hash Table | O(1)* | O(1)* | O(1)* | O(1)* |
*Amortized complexity or average case
Space Complexity
| Data Structure | Memory | Remarks |
|---|---|---|
| Array | O(n) | Fixed size |
| Dynamic Array | O(n) | With overhead |
| Linked List | O(n) | Additional pointers |
| Binary Search Tree | O(n) | Recursive structure |
| Graph | O(V + E) | V = vertices, E = edges |
Best Practices
1. Choose the right data structure
// Frequent search operations -> HashSet/HashMap
Set<String> uniqueNames = new HashSet<>();
Map<String, Integer> wordCount = new HashMap<>();
// Ordered data -> TreeSet/TreeMap
Set<Integer> sortedNumbers = new TreeSet<>();
Map<String, Integer> sortedWordCount = new TreeMap<>();
// FIFO processing -> Queue
Queue<Task> taskQueue = new LinkedList<>();
// LIFO processing -> Stack
Stack<Operation> undoStack = new Stack<>();
// Priority processing -> PriorityQueue
PriorityQueue<Event> eventQueue = new PriorityQueue<>();
2. Performance Considerations
// Bad: O(n²) for large datasets
List<Integer> list = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
for (int j = 0; j < list.size(); j++) {
// O(n²) operation
}
}
// Good: O(n) with HashSet
Set<Integer> set = new HashSet<>();
for (int item : list) {
set.add(item); // O(1) on average
}
Exam-Relevant Questions
Typical Tasks
- Implement a Stack with Array
- Compare BFS and DFS
- Explain Heap Sort
- Implement Binary Search
- Calculate the complexity of algorithms
Important Concepts
- Amortized Analysis: Average cost over operations
- Best-Case/Worst-Case/Average-Case: Different scenarios
- Space-Time Tradeoff: Memory vs. speed
- Recursion vs Iteration: Implementation variants
Summary
Data structures are fundamental for efficient algorithms:
- Arrays: Direct access, fixed size
- Stack/Queue: Special access orders
- Heaps: Priority queues, efficient min/max operations
- Trees: Hierarchical organization, efficient searching
- Graphs: Network relationships, complex algorithms
Choosing the right data structure is crucial for performance and significantly affects the complexity of operations.