Overview
This unit on Data Structures introduces the organized ways of storing and accessing data so programs run efficiently. It covers linear and non-linear structures: arrays, linked lists, stacks, queues, trees, heaps, hash tables and graphs, and explains abstract data types, memory representation, common operations (insert, delete, search, traverse) and performance measures. You also learn elementary algorithms for sorting and searching, collision resolution in hashing, and basic tree and graph traversals. Understanding data structures matters because the right choice can make a program faster, use less memory and be easier to design. For example, choosing an array vs a linked list affects how quickly you can insert or access elements. This unit also builds the ability to analyze time and space complexity, which helps predict program behaviour for large inputs. Practical skills include implementing structures in a language, tracing algorithms on small examples and choosing appropriate structures for real problems such as building symbol tables, priority queues or networks. By the end of the unit students will be able to explain, implement and compare common data structures and apply them to standard computing tasks, preparing them for algorithms, databases and software design topics in higher classes.
Learning Objectives
- Describe the concept of abstract data types and differentiate them from concrete implementations.
- Implement and manipulate arrays and linked lists to perform basic operations.
- Use stacks and queues to solve problems that require specific orderings (LIFO, FIFO).
- Construct and traverse trees and binary search trees and explain their properties.
- Apply heap structures to implement efficient priority queues.
- Explain and implement basic hashing techniques and collision resolution strategies.
- Represent graphs and perform depth-first and breadth-first traversals.
- Analyze time and space complexity for common operations on data structures using big-O notation.
- Choose suitable data structures for given computing problems and justify the choice.
Topics in this chapter
15 topics · tap a topic title to jump straight to it.
Introduction to Data Structures and ADT
What is a data structure? A data structure is a way to organise, store and manage data in memory so that different operations (like search, insertion, deletion and traversal) can be performed efficiently. Data structures are the practical forms that realise abstract models of collections of items. Choosing a suitable structure determines both the running time and memory use of a program.
Abstract Data Type (ADT) An ADT specifies the operations allowed on a collection of values and the expected behaviour or semantics of those operations. For example, a List ADT defines operations such as add, remove, get and size without specifying how these are implemented. The separation between the ADT (interface) and its implementation is central to modular software design.
Interface versus implementation Designing with ADTs lets code that uses a structure remain independent of implementation choices. For example, a List ADT can be implemented using an array or a linked list; both provide the same external operations but have different performance trade-offs. This separation makes programs easier to maintain and optimise later: swap an implementation without changing callers.
Basic operations and their costs Typical operations include traversal (visiting elements), insertion, deletion, lookup/search and update. Each operation incurs time and sometimes extra memory. We measure these costs asymptotically (big-O notation) to understand behaviour for large inputs. For example, arrays give constant-time random access but insertion in the middle may cost O(n); linked lists allow O(1) insertion at known positions but random access is O(n).
Memory organisation Some structures use contiguous memory (arrays) while others use nodes with pointers (linked lists, trees). Contiguous layouts benefit from CPU cache locality and simple address arithmetic; pointer-based structures excel when dynamic growth and frequent insert/delete operations are needed.
Trade-offs and selection No single data structure is best for every problem. Choosing a right structure depends on the expected operations and their frequency. For example, if many lookups by key are needed, a hash table or balanced search tree is appropriate; for smallest memory overhead with predictable size, an array may be better. Understanding these trade-offs is the core goal of this introductory topic and sets the stage for studying concrete structures in detail.
- Example: List ADT with operations add(item), remove(index) and get(index); compare array vs linked list implementations.
- Example: A Stack ADT exposes push, pop, and peek. It can be implemented using an array or linked list.
- Time complexity notation: O(1), O(log n), O(n), O(n log n), O(n^2).
- Space complexity: amount of memory used as a function of input size n.
Arrays and Multidimensional Arrays
Definition and storage An array is a collection of elements of the same data type stored in contiguous memory locations. Each element is accessible by an index, usually starting at 0. Because elements are adjacent in memory, the address of an element can be computed by a simple formula: base_address + index * size_of(element). This gives arrays the important advantage of constant-time random access: accessing A[i] is O(1).
One-dimensional arrays One-dimensional arrays model lists and tables of values with fixed capacity. You declare an array with a capacity and fill positions as needed. Traversing the entire array to perform an operation on each element costs O(n). Insertion at the end (if space exists) can be O(1), but insertion or deletion in the middle requires shifting elements, costing O(n) in the worst case.
Dynamic arrays Languages often provide resizable arrays (dynamic arrays or array lists) that grow automatically. When full, they allocate a larger block (commonly doubling capacity) and copy existing elements. Amortised cost of appending elements is O(1) despite occasional costly copies, because copies happen infrequently.
Multidimensional arrays Multidimensional arrays (e.g., 2D arrays or matrices) are arrays of arrays conceptually. A 2D array with m rows and n columns occupies m × n contiguous elements in one of two common orders: row-major (rows stored one after another) or column-major (columns stored consecutively). For performance, it is important to access elements in the memory order (row-major prefer iterating rows first) to use CPU caches effectively.
Operations and complexity Common operations include indexing (O(1)), traversal (O(n)), searching unsorted arrays (O(n)), and searching sorted arrays using binary search (O(log n)). Sorting an array costs O(n log n) with efficient algorithms. Arrays are ideal when you need fast random access, predictable memory layout and simple indexing arithmetic.
Limitations and use-cases Arrays have fixed capacity (unless dynamic) and resizing can be costly. They are poor at frequent insertions/deletions in the middle. Arrays are well-suited for implementing heaps, storing matrices in numerical computations, look-up tables, and cases where elements are accessed by position often.
Practical notes Choose array size carefully and prefer dynamic arrays for variable-size collections. Understand the memory layout (row-major/column-major) when working with multidimensional arrays to write cache-friendly loops and improve performance.
- Example: Accessing element at index 5 in an integer array arr[] is arr[5], costing O(1).
- Example: Inserting a new element at index 2 requires shifting elements from index 2 onward one position right; cost O(n).
- Example: Representing a 3x3 matrix and summing its diagonal elements by nested loops.
- Address of array[i] = base_address + i * size_of(element) (in contiguous storage).
- Binary search on sorted array: O(log n) time.
Singly and Doubly Linked Lists
Structure and node layout A linked list stores data in nodes where each node contains the data value and one or more pointers (references) to other nodes. In a singly linked list each node has a pointer to the next node; in a doubly linked list each node also stores a pointer to the previous node. The list maintains a head pointer to the first node and optionally a tail pointer to the last node. Nodes need not be contiguous in memory; they may be allocated individually and linked together.
Creation and traversal To traverse a singly linked list, start at head and follow next pointers until a null terminator is reached. Traversal visits all nodes and costs O(n). A doubly linked list allows traversal in both directions using next and prev pointers, which is helpful when operations need access to both neighbours.
Insertion and deletion Insertion at the beginning of a singly linked list is O(1): create a node, set its next to head and update head. Insertion at a given position requires locating the previous node (O(n) worst-case) and updating pointers. Deleting the head is O(1); deleting an arbitrary node in singly linked list requires access to its previous node to update its pointer. In doubly linked lists deletion of a node is simpler because prev pointer is available; updates adjust next and prev of neighbouring nodes. Maintaining a tail pointer allows O(1) insertion at the end.
Special forms and variants Circular linked lists connect the tail back to the head, making traversal wrap around; they are useful in round-robin scheduling. Sentinel or dummy nodes can simplify code by removing special-case checks for empty lists or head/tail operations.
Advantages and disadvantages Linked lists allow dynamic growth without copying and efficient insert/delete when node location is known. However, random access by index is slow (O(n)), and extra memory is used for pointers. Pointer manipulation requires careful handling to avoid segmentation faults, memory leaks or broken lists. Also linked lists have worse cache performance than arrays because nodes are scattered in memory.
Applications Linked lists are used to implement stacks and queues (pointer-based), adjacency lists for graph representation, dynamic memory pools, and when frequent insertions/deletions are required. They are also used internally in many library containers where iterator stability is important.
- Example: Insert a node at the start: new->next = head; head = new; (O(1)).
- Example: Delete a node after node p in singly linked list: p->next = p->next->next; free the removed node.
- Example: Convert a list to a doubly linked list by adding prev pointers and updating both next and prev during insert/delete.
- Time complexity: insertion at head O(1); insertion at position i O(n); search O(n).
Stacks and Applications
Definition and core operations A stack is an abstract data type that follows Last-In-First-Out (LIFO) order: the last element inserted is the first to be removed. The principal operations are push (place an element on top), pop (remove and return the top element), and peek/top (inspect the top element without removing it). Stacks can be implemented using arrays or linked lists; both provide constant-time push and pop when implemented correctly.
Array-based implementation An array-based stack maintains an index (top) that points to the current top position. To push, increment top and store the value at that index; to pop, read the value at top and decrement top. Array-based stacks have fixed capacity unless implemented with a dynamic resizing scheme. One must handle overflow (push on full stack) and underflow (pop on empty stack).
Linked-list implementation A stack implemented with a linked list uses the head of the list as the top. Push creates a new node and links it as the new head; pop removes the head node. This avoids fixed capacity limits and gives push/pop O(1) performance until memory is exhausted.
Applications in computing Stacks are central to many programming tasks: function call management (runtime call stack) stores return addresses and local variables; expression evaluation and parsing use stacks to manage operators and operands; implementing undo functionality in editors, depth-first search in graphs, backtracking algorithms and syntax checking (parentheses matching) all rely on stacks.
Expression parsing and evaluation Converting infix expressions to postfix (or prefix) uses stacks to reorder operators respecting precedence and associativity. Postfix expressions are evaluated easily by scanning and using a stack: push operands, and when encountering an operator pop operands, apply operator and push result. This avoids managing operator precedence at evaluation time.
Error conditions and performance In array-based stacks, overflow occurs when pushing into a full array; in linked-list stacks, pushing may fail if memory allocation fails. Underflow occurs when popping from an empty stack. Time complexity for push, pop and peek is O(1) and space required is O(n) to store n elements. Choose implementation based on expected maximum size, memory constraints and required performance properties.
- Example: Evaluate postfix expression 23*5+ using stack: push 2, push 3, '*' pops 3 and 2, pushes 6; push 5; '+' pops 5 and 6 and pushes 11.
- Example: Check balanced brackets '[({})]' by pushing openings and matching closings; result is balanced.
- Stack operations complexity: push O(1), pop O(1), peek O(1).
Queues and Variants (Circular, Priority)
Queue basics A queue is a linear data structure that follows First-In-First-Out (FIFO) ordering: elements are removed in the same order they were added. Main operations are enqueue (insert element at the rear) and dequeue (remove element from the front). Queues can be implemented using arrays or linked lists; with a linked list maintaining both head and tail pointers, enqueue and dequeue are O(1).
Circular queue A fixed-size array-based queue suffers from wasted space when front moves forward; a circular queue solves this by treating the array as circular and using modulo arithmetic for indices. Maintain front and rear indices and optionally a count of elements. Enqueue places element at rear and increments rear = (rear + 1) % capacity; dequeue removes element from front and updates front likewise. Correct empty/full checks and management of size are important to avoid confusion between full and empty states.
Double-ended queue (Deque) A deque (double-ended queue) supports insertion and deletion at both ends. It generalises stacks and queues and is useful for problems where both ends need efficient access, such as sliding-window algorithms and some scheduling problems. Deques can be implemented using circular arrays or doubly linked lists.
Priority queues Unlike ordinary queues, priority queues remove the element with the highest (or lowest) priority, not necessarily the earliest inserted. They are commonly implemented using heaps (binary heaps, binomial heaps, Fibonacci heaps). Binary heaps give O(log n) time for insertion and extract-max/extract-min; a priority queue is essential to algorithms like Dijkstra's and event-driven simulation.
Applications Queues model real-world waiting lines, CPU scheduling (ready queue), IO buffering and level-order traversal of trees (BFS uses a queue). Circular buffers are used in streaming or producer-consumer problems. Priority queues are used for scheduling by priority, shortest-path algorithms and any situation requiring efficient retrieval of the highest-priority item.
Edge cases and complexities For array-based queues of capacity m, operations are O(1) but the array can overflow if not resized. For priority queues implemented with heaps, insert and extract are O(log n). Dealing with underflow (dequeue on empty) and overflow (enqueue on full fixed array) are essential in robust implementations.
- Example: Implement a circular queue of capacity 5; show enqueue and dequeue sequences with wrapping indices.
- Example: Use a min-priority queue to schedule tasks by earliest deadline: insert tasks with deadlines as priority and extract the smallest deadline first.
- Circular index increment: next = (current + 1) % capacity.
- Priority queue using heap: insert and extract operations O(log n).
Trees: Terminology and Traversals
What is a tree? A tree is a hierarchical, non-linear data structure composed of nodes connected by edges. One node is designated the root; every other node has exactly one parent and zero or more children. Trees model hierarchical relationships such as file directories, organisation charts, and parsed expressions. Key concepts include root, parent, child, sibling, leaf (node with no children), depth (distance from root) and height (longest path to a leaf).
Binary trees and variants A binary tree restricts each node to at most two children commonly named left and right. Special binary tree shapes include full trees (each node has 0 or 2 children), perfect trees (all levels completely filled), and complete trees (all levels filled except possibly the last which is filled from left). Complete binary trees are convenient for array representation, while pointer-based binary trees are more flexible for dynamic operations.
Binary tree operations Common operations include insertion (often at specific positions depending on the tree type), deletion, searching and traversal. Traversal means systematic visiting of all nodes. Binary trees support several traversal orders with different uses and properties.
Depth-first traversals Preorder (root, left, right) is useful to copy a tree or produce prefix notation for expressions; inorder (left, root, right) gives symmetric order and for a binary search tree yields sorted keys; postorder (left, right, root) is useful when deleting or freeing nodes or producing postfix notation. These traversals are naturally implemented by recursion; iterative implementations use an explicit stack to avoid recursion depth limits.
Breadth-first (level-order) traversal Level-order visits nodes level by level from the root, using a queue to track the next nodes to visit. Level-order is practical for finding shortest path in trees, printing tree by levels, or when operations depend on tree height.
Implementation considerations Recursive code is concise but consumes stack space proportional to tree height. For tall trees, iterative methods that use explicit stacks or queues are safer. When storing a complete binary tree in an array, children indices of node at i are left = 2i + 1 and right = 2i + 2 (0-based), which simplifies many algorithms like heap operations.
Applications Trees are central to many data structures and algorithms: binary search trees for ordered data, expression trees for compilers, file-system directory trees, and specialised trees like tries, AVL and B-trees for efficient lookup and indexing. Mastering tree terminology and traversals is essential for understanding these applications.
- Example: Given a binary tree, produce its inorder, preorder and postorder traversals by recursive algorithms.
- Example: Level-order traversal using a queue: enqueue root, then for each node dequeue it and enqueue its children.
- Array indices for binary tree stored in array: left(i) = 2i + 1, right(i) = 2i + 2 (0-based).
Binary Search Trees (BST)
Definition and ordering property A binary search tree (BST) is a binary tree where every node satisfies the BST property: all keys in the left subtree are less than the node's key and all keys in the right subtree are greater (or follow a consistent <=/>= rule if duplicates are allowed). This ordering enables efficient searching, insertion and deletion by eliminating half the tree at each comparison in a well-balanced tree.
Search operation Searching for a key k starts at the root: compare k with node's key; if equal return success; if k is smaller follow the left child; if larger follow the right child. Repeat until found or a null child means not present. For a balanced BST average search time is O(log n); for a degenerate (skewed) BST it degrades to O(n).
Insertion To insert a key, search downwards to find the null child where the key should be placed and insert a new leaf there. No rebalancing occurs in a basic BST, so insertion order affects tree shape. Inserting sorted keys produces a chain (worst-case height n).
Deletion Deleting a node has three main cases: (1) node is a leaf — remove it directly; (2) node has one child — replace the node with its child; (3) node has two children — replace the node's key with its inorder successor (smallest key in right subtree) or predecessor and then delete that successor/predecessor node, which will fall into case (1) or (2). Correct pointer updates are essential to maintain structure.
Traversals and sorted output An inorder traversal of BST produces keys in sorted order, which is an important property used in sorting and in-order operations. Preorder and postorder remain useful for tree copying and destruction.
Performance and balancing Basic BST operations are O(h) where h is tree height. Balanced BST variants (AVL, Red-Black) maintain h = O(log n) and guarantee logarithmic worst-case times. When datasets are dynamic with arbitrary insertion orders, prefer self-balancing trees to avoid poor worst-case times. For fast average-case lookups without ordering guarantees, hash tables may be faster.
Applications BSTs support ordered sets, maps, symbol tables, and are foundational to many algorithms. Understanding insertion, deletion cases, and traversal properties is crucial for correctness and performance in implementations.
- Example: Insert keys 40, 20, 60, 10, 30, 50, 70 into an empty BST and draw the tree; show inorder traversal gives 10,20,30,40,50,60,70.
- Example: Delete node 40 (root) with two children: replace with inorder successor 50 and then remove 50's original node.
- Average height of balanced BST ~ O(log n); worst-case height O(n).
- In-order traversal of BST yields sorted sequence of keys.
Heaps and Priority Queues
Heap concept and heap property A heap is a specialised tree-based structure used to implement priority queues. In a max-heap every parent node's key is greater than or equal to its children's keys; in a min-heap every parent key is less than or equal to its children's keys. Heaps are typically complete binary trees: all levels are fully filled except possibly the last, which is filled from left to right. The completeness allows compact array representation.
Array representation and index arithmetic Because heaps are complete, they map neatly to arrays. For a node at index i (0-based), its left child is at 2i + 1 and right child at 2i + 2; parent is at floor((i - 1)/2). This arithmetic avoids explicit pointers and is cache-friendly, making heaps efficient in practice.
Heap operations Insertion places the new key at the end of array and performs sift-up (up-heap) comparing with parent and swapping as needed until heap property is restored; this is O(log n) time. Extract-max (or extract-min) removes the root, replaces it with the last element, and performs sift-down (down-heap) swapping with the larger (or smaller) child until the heap property holds; this is O(log n). Building a heap from an unsorted array can be done by repeated insertions (O(n log n)) or by bottom-up heapify in O(n) time using downward sifts starting from the last non-leaf node.
Heaps as priority queues A heap efficiently implements a priority queue where insert and extract operations run in O(log n). For algorithms such as Dijkstra's shortest-path or event simulation, heaps are the usual choice for managing the frontier of candidate vertices or events. However, direct search for an arbitrary key in a heap is O(n), so heaps are not suitable when fast arbitrary-key search is required.
HeapSort HeapSort sorts an array by building a heap and repeatedly extracting the root into the end of the array; in-place HeapSort runs in O(n log n) time and uses O(1) extra space. HeapSort is not stable and has less predictable memory access patterns than merge sort, but is useful when constant extra space is required.
Variants and trade-offs More advanced heap variants (Fibonacci heaps) support faster decrease-key operations amortised, which benefits graph algorithms, but are more complex to implement. Choose binary heaps for simplicity and good practical performance in most cases.
- Example: Insert keys [15, 10, 20, 17] into a max-heap step by step and show array representation after each insertion.
- Example: Show heapify process to build max-heap from array [3,5,1,2,4] using bottom-up sifting.
- Parent index: parent(i) = floor((i - 1) / 2).
- Children indices: left(i) = 2i + 1, right(i) = 2i + 2.
- Heap operations: insert O(log n), extract O(log n), build-heap O(n).
Hashing and Hash Tables
Motivation and idea Hashing is a technique to store and retrieve key-value pairs quickly by mapping keys to indices in an array (hash table) through a hash function. The aim is to achieve average-case constant-time operations for insert, search and delete. Hash tables are widely used for dictionaries, symbol tables, caches and sets.
Hash function design A hash function converts a key to an integer hash code and then maps that code to a table index, commonly by taking modulo table_size. A good hash function distributes keys uniformly across slots to minimise collisions. For integer keys simple arithmetic may suffice; for strings polynomial rolling or more sophisticated mixing functions are used. Choosing table size (often a prime) and hash function together improves distribution.
Collisions and resolution strategies A collision occurs when two keys map to the same index. Two main approaches resolve collisions: chaining and open addressing. In chaining, each table slot holds a linked list (or dynamic container) of elements hashing to that slot; insertion adds to the chain. Searching traverses the chain to find a key. In open addressing elements are stored directly in table slots; on collision a probing sequence (linear probing, quadratic probing, or double hashing) finds the next free slot. Open addressing requires careful handling of deletions using special markers to preserve probe sequences.
Load factor and resizing The load factor α = n/m (n elements, m slots) measures table occupancy. For chaining expected search time is O(1 + α); for open addressing performance degrades as α approaches 1 and clustering may occur. To maintain performance, hash tables are resized (rehashing) when α exceeds a threshold: allocate a larger table and reinsert existing keys using the hash function for the new size.
Practical issues and performance Average-case operations in well-designed hash tables are O(1), but worst-case (many collisions) can be O(n). For predictable worst-case times, balanced search trees are preferable. Hash tables do not preserve order of keys; if ordered iteration is required, choose tree-based maps or maintain auxiliary order structures.
Applications Hash tables implement fast lookups in compilers (symbol tables), databases (indexes), web caches and language runtime structures. Understanding hash function choice, collision handling and resizing policies is key to building robust table implementations.
- Example: Using modulo hash function h(k) = k % 7, insert keys [10, 22, 31, 4, 15] using chaining and show table.
- Example: Insert keys into an open-addressed table with linear probing and show probing steps for collisions.
- Load factor α = n / m where n is number of elements and m is table size.
- Simple hash: h(k) = k mod m (for integer keys).
Graphs: Representation, Traversals and Algorithms
Graph basics A graph is a set of vertices (nodes) connected by edges. Graphs may be directed (edges have direction) or undirected; edges can carry weights. Graphs model many real-world systems such as road maps, computer networks and social networks. Fundamental tasks include traversal, connectivity checking, shortest paths and spanning trees.
Representing graphs Two common representations are adjacency matrix and adjacency list. An adjacency matrix is an n×n matrix A where A[i][j] indicates presence (and possibly weight) of edge from i to j. It uses O(n^2) space and supports O(1) edge existence checks. An adjacency list stores for each vertex a list of neighbours and requires O(n + e) space for e edges, making it efficient for sparse graphs.
Breadth-First Search (BFS) BFS explores vertices level by level from a source vertex using a queue. It discovers shortest paths in unweighted graphs and computes distances (in number of edges) from the source to each reachable vertex. BFS runs in O(n + e) time using adjacency lists and is useful for connectivity, shortest unweighted path and bipartiteness checking.
Depth-First Search (DFS) DFS explores as far as possible along each branch before backtracking, implemented by recursion or a stack. DFS also runs in O(n + e) and is used for topological sorting of DAGs, cycle detection, and finding connected components. DFS discovery and finish times help reason about edge types and graph structure.
Shortest-path algorithms For weighted graphs with non-negative weights, Dijkstra's algorithm finds shortest paths from a source using a priority queue to pick the nearest unvisited vertex and relaxing adjacent edges; complexity with binary heap is O((n + e) log n). For graphs with arbitrary weights, Bellman-Ford handles negative weights (but no negative cycles) at O(n e) time. For all-pairs shortest paths, Floyd-Warshall is a dynamic programming approach with O(n^3) time.
Minimum spanning tree (MST) An MST connects all vertices with minimum total edge weight. Kruskal's algorithm sorts edges and uses a disjoint-set (union-find) structure to add edges that connect different components, O(e log e) time. Prim's algorithm grows the tree from a source using a priority queue, similar to Dijkstra.
Choosing representation and algorithm For dense graphs adjacency matrix is acceptable; for sparse graphs adjacency lists save space and time. Algorithm choice depends on edge weights, directed vs undirected, and required output (single-source vs all-pairs). Mastery of BFS/DFS and basic weighted algorithms is essential for many advanced problems in networks and optimization.
- Example: Represent a graph with 5 vertices and some edges as both adjacency matrix and adjacency lists and compare space.
- Example: Perform BFS from vertex A on a simple graph and list vertices in order visited; track queue contents.
- Example: Run Dijkstra on a small weighted graph from source S and compute distances step by step using a min-priority queue.
- Adjacency matrix space: O(n^2); adjacency list space: O(n + e).
- BFS and DFS time complexity: O(n + e) for n vertices and e edges.
- Dijkstra complexity with binary heap: O((n + e) log n).
Searching Algorithms: Linear and Binary Search
Linear search Linear (sequential) search checks each element in a list one by one until the target is found or the list ends. It works on unsorted collections and is simple to implement. The worst-case and average-case time complexity is O(n) because the element may be absent or at the end. For small or unsorted datasets, linear search is often acceptable.
Binary search Binary search is designed for sorted arrays. It repeatedly compares the target value to the middle element of the current search interval and halves the interval based on comparison results. Because the range halves each step, binary search takes O(log n) time. Binary search requires random access (e.g., arrays) and sorted order; it is not efficient on linked lists because finding the middle element takes O(n).
Implementational details Binary search can be implemented iteratively or recursively. Care is needed with index arithmetic to avoid overflow in fixed-width integer types; use mid = low + (high - low) / 2. For duplicate keys, binary search variants find first or last occurrence by adjusting the search boundaries when a match is found.
When to use which Use linear search for small arrays or unsorted data where sorting would be more expensive than repeated linear searches. Use binary search when the array is sorted and many searches are required; maintaining sorted order with frequent insertions may be costly, so consider balanced BSTs or other structures if updates are frequent. For associative lookups without ordering, hash tables provide average-case O(1) lookup.
Limitations and edge cases Binary search assumes random access; on linked lists its advantage disappears. For floating-point comparisons, incorporate tolerance due to precision errors. When using binary search to find insertion positions, design the loop boundaries carefully to return correct index for insertion.
Practical value Understanding these search algorithms helps in algorithm selection and understanding the trade-offs between preprocessing (sorting) and query time. Binary search is foundational and appears as a subroutine in many algorithmic solutions beyond simple lookups.
- Example: Linear search for key 17 in array [5,12,17,9] inspects elements sequentially until it finds 17 at index 2.
- Example: Binary search for 17 in sorted array [3,7,12,17,20] uses mid indices to reduce search range and finds 17 in O(log n) steps.
- Linear search worst-case time: O(n).
- Binary search worst-case time: O(log n).
Sorting Algorithms: Simple and Efficient Methods
Purpose of sorting Sorting arranges elements in order (ascending or descending). Sorted data simplifies searching, enables binary search and improves performance of other algorithms. This topic covers simple methods useful for learning (selection, insertion, bubble) and efficient divide-and-conquer methods (merge sort, quick sort) used in practice.
Selection sort Selection sort repeatedly finds the minimum element from the unsorted portion and swaps it into its final position. It performs O(n^2) comparisons regardless of initial order and O(n) swaps. It is in-place but not stable by default. Selection sort is easy to trace and useful for small datasets or teaching loop invariants.
Insertion sort Insertion sort builds the sorted list one element at a time by shifting larger elements to the right to make room for the next key. Its worst-case time is O(n^2) when input is reversed, but its best-case is O(n) for nearly-sorted input. Insertion sort is stable and adaptive, making it a good choice for small arrays and nearly-sorted data. It is often used as the base case for divide-and-conquer sorts.
Bubble sort Bubble sort repeatedly compares adjacent elements and swaps them if out of order. Each pass moves the largest unsorted element to its final position. Basic bubble sort is O(n^2) but an optimized version stops early when no swaps occur. Bubble sort is stable but rarely used in practice due to inefficiency.
Merge sort Merge sort is a divide-and-conquer algorithm that splits the array into halves, recursively sorts each half and merges the sorted halves. Merging two sorted lists takes linear time, and because the recurrence divides work evenly, merge sort runs in O(n log n) time in all cases. Merge sort is stable but requires O(n) additional space for merging when implemented on arrays. It is excellent for external sorting and linked lists.
Quick sort Quick sort picks a pivot, partitions elements into those less than and greater than the pivot, and recursively sorts partitions. Average-case time is O(n log n) and quick sort is typically fast in practice due to low constant factors and good cache behaviour. Worst-case time is O(n^2) with poor pivot choices (e.g., sorted input with naive pivot). Randomised pivot selection or median-of-three heuristics reduce the chance of worst-case. Quick sort can be implemented in-place and usually uses O(log n) stack space.
Choosing a sort For small arrays or nearly-sorted data, insertion sort is often best. For stable guaranteed O(n log n) behaviour, use merge sort. For typical in-memory array sorting, quick sort (with good pivot rules) is often preferable due to speed and low extra memory. Understand stability, space requirements and worst-case behaviours when selecting an algorithm.
- Example: Show selection sort on [64,25,12,22,11], step-by-step selecting minimum and swapping.
- Example: Apply insertion sort to [5,2,4,6,1,3], showing shifts for each insertion.
- Example: Apply merge sort to [38,27,43,3,9,82,10] showing division and merge steps.
- Time complexity: Selection O(n^2), Insertion O(n^2) worst-case and O(n) best-case, Bubble O(n^2).
- Merge sort time complexity: O(n log n) worst/average/best.
- Quick sort average time: O(n log n); worst-case: O(n^2).
Algorithm Analysis and Big-O Notation
Why analyse algorithms? Algorithm analysis estimates the resources an algorithm requires as input size grows. The main goals are to compare algorithms, predict performance on large inputs and guide selection of appropriate methods and data structures. Analysis focuses on time (number of steps) and space (extra memory), abstracting away machine-specific details.
Asymptotic notations Big-O notation provides an upper bound on growth: f(n) = O(g(n)) means for large n, f does not exceed a constant times g. Big-Omega (Ω) gives a lower bound; Theta (Θ) means tight bound when both O and Ω apply. Common complexity classes include O(1), O(log n), O(n), O(n log n), O(n^2) and exponential classes like O(2^n).
Counting operations To estimate time, count elementary operations (comparisons, assignments) as functions of input size n. For loops multiply iterations by cost per iteration. For nested loops multiply costs unless inner loop length depends on outer index. For divide-and-conquer recurrences use techniques like recursion trees or Master Theorem. For example, merge sort satisfies T(n) = 2T(n/2) + O(n), giving T(n) = O(n log n).
Best, worst and average cases Worst-case time gives an upper guarantee irrespective of input and is useful for reliable performance bounds. Best-case is the minimum resource usage and average-case is expected usage over a distribution of inputs; average-case often requires probabilistic models. For critical systems prefer worst-case bounds; for average performance consider distributions and amortised analysis for sequences of operations.
Space complexity Measures additional memory used beyond input storage. Recursive algorithms also use call stack proportional to recursion depth. In-place algorithms use O(1) extra space. Trade-offs exist between time and space: caching results can speed up computations at cost of extra memory.
Practical guidance Big-O ignores constants and lower-order terms; for small n constants matter. Use asymptotic analysis to reason about scalability: an O(n) algorithm will outperform O(n^2) for large n even if constants differ. Consider both algorithmic complexity and implementation details like cache behaviour and parallelisability for practical performance.
- Example: Analyze time for a loop that runs n times with O(1) work per iteration: total O(n).
- Example: Analyze nested loops where inner loop runs n times for each of n outer iterations: O(n^2).
- If T(n) = a T(n/b) + f(n) for divide-and-conquer recurrences, Master Theorem gives asymptotic bounds depending on f(n).
- Common growth orders: O(1) < O(log n) < O(n) < O(n log n) < O(n^2) < O(2^n).
File Structures and External Data Structures
Why external data structures? When data sizes exceed main memory, efficient algorithms must minimise disk I/O because reading from or writing to disk is many orders of magnitude slower than memory access. External data structures are designed to reduce expensive block transfers and seeks by organising data in blocks and optimising access patterns.
Block-oriented access model Disk and external storage read and write blocks (pages) of fixed size. Algorithms thus aim to maximise useful work per block transfer. Performance measures for external algorithms count number of block transfers (I/Os) rather than CPU operations. Buffering, caching and batched I/O reduce the effective cost of disk operations.
B-Trees and B+ Trees Balanced multi-way trees such as B-Trees and B+ Trees are tailored for disks: nodes contain many keys and child pointers so each node fits a block. This high branching factor keeps tree height small, reducing the number of disk reads per search or update. B-Trees store keys and records in internal nodes; B+ Trees store records only at leaves and link leaves for efficient range queries and sequential scans. These trees are widely used in databases and file systems where large indexes must be searched and updated with few disk accesses.
Hashing on disk External hashing schemes organise buckets as disk blocks; overflow chaining or dynamic hashing techniques (extensible hashing, linear hashing) adapt the table size to growth while limiting reorganisation cost. Maintaining low load factor per block keeps I/O per operation small.
External sorting External merge sort is the standard method to sort data larger than memory. It divides data into runs that fit in memory, sorts each run in-memory, writes runs to disk, and then merges runs in multi-way passes reading blocks sequentially. Multi-way merging minimises the number of passes and I/Os by using available memory as buffers.
Design principles Minimise random disk seeks, favour sequential reads/writes, use block-sized nodes/records, maintain small tree height, and tune buffer sizes to match hardware. Many real systems combine in-memory caching with external structures for high throughput. Understanding these external structures is essential for database design, large-scale search engines and any application managing huge datasets.
- Example: Show B-Tree node layout with multiple keys per node and small tree height for millions of records.
- Example: External merge sort: split data into runs that fit in memory, sort each run, then merge runs in passes.
- I/O complexity is measured in number of block transfers rather than CPU steps; aim to minimise I/O operations.
- B-Tree height is O(log_t N) where t is minimum degree (branching factor) and N is number of keys.
Special Trees: AVL, Red-Black and Tries
Need for balancing Ordinary binary search trees can become skewed depending on insertion order, leading to O(n) operations. Self-balancing trees maintain height O(log n) by performing rotations or structural adjustments during insertions and deletions. Balanced trees combine ordering with guaranteed performance.
AVL trees An AVL tree maintains a balance factor at each node equal to height(left subtree) − height(right subtree), which must be -1, 0 or +1. Insertions or deletions may violate this property; restoring balance requires single or double rotations (left, right, left-right, right-left) around unbalanced nodes. AVL trees are strictly balanced, providing fast lookups with worst-case O(log n) time, but may perform more rotations than other balanced trees during updates.
Red-Black trees Red-Black trees store a colour bit (red or black) at each node and enforce properties that guarantee the longest path is at most twice the shortest, keeping height O(log n). Insertions and deletions involve recolouring and possible rotations. Red-Black trees are less rigid than AVL trees, typically requiring fewer rotations in practice and are used in many standard library implementations of ordered maps and sets.
Tries (prefix trees) Tries are tree structures for storing strings where each edge corresponds to a character and keys are represented by paths from the root to terminal nodes. Lookup time is proportional to the length of the key (O(m)) independent of the number of keys. Tries excel at prefix searches, autocomplete, spell checking and IP routing tables. Raw tries can consume large memory; compressed tries (radix trees) and suffix trees reduce space usage.
Trade-offs and applications AVL trees provide stricter balance and faster lookups; Red-Black trees offer good average update performance with simpler amortised costs; tries offer very fast string operations but can use more memory. Choice depends on required operations: ordered associative maps favour AVL or Red-Black, while prefix-based string tasks favour tries.
- Example: Insert sequence of keys into an AVL tree and show rotations needed to restore balance.
- Example: Build a trie for words {cat, car, dog} and show shared prefix nodes and terminal markers.
- AVL height is O(log n); Red-Black tree height ≤ 2 log2(n+1).
- Trie lookup time O(m) where m is length of key string.
Key Concepts
- Abstract Data Type (ADT)
- A specification of operations and behaviour for a data collection, independent of implementation.
- Array
- A contiguous block of memory storing elements of the same type accessible by index.
- Linked List
- A sequence of nodes where each node contains data and a pointer to the next (and possibly previous) node.
- Stack
- A LIFO structure supporting push and pop operations.
- Queue
- A FIFO structure supporting enqueue and dequeue operations.
- Binary Search Tree (BST)
- A binary tree where left subtree keys are less and right subtree keys are greater than the node key.
- Heap
- A complete binary tree satisfying heap property used to implement priority queues.
- Hash Table
- A structure using a hash function to map keys to table indices for fast access.
- Graph
- A set of vertices connected by edges, possibly directed or weighted.
- Breadth-First Search (BFS)
- A traversal that explores graph vertices level by level using a queue.
- Depth-First Search (DFS)
- A traversal that explores as far as possible along each branch before backtracking, using recursion or a stack.
- Big-O Notation
- A mathematical notation describing an upper bound on the growth rate of a function as input size increases.
- Load Factor
- In hashing, the ratio of number of elements to table size (n/m).
- AVL Tree
- A self-balancing BST that maintains height difference of at most 1 at every node.
- Trie
- A prefix tree for storing strings where paths represent prefixes of keys.
- Priority Queue
- An abstract data type where each element has a priority and removal returns the highest-priority element.
- Disjoint Set (Union-Find)
- A data structure for keeping track of a partition of elements into disjoint sets supporting union and find operations.
- External Sorting
- Sorting methods designed to handle data that do not fit into main memory, minimising disk I/O.
Practice Questions
-
Explain the difference between an Abstract Data Type and a data structure. / एक Abstract Data Type और एक डेटा संरचना के बीच क्या अंतर है?
Show answer
An Abstract Data Type (ADT) defines the set of operations and their behaviour for a data collection without specifying how these operations are implemented; a data structure is a concrete way of organising data in memory to realise an ADT. / एक Abstract Data Type (ADT) उन ऑपरेशनों और उनके व्यवहार को परिभाषित करता है जो किसी डेटा संग्रह के लिए उपलब्ध होते हैं, लेकिन यह नहीं बताता कि इन ऑपरेशनों को कैसे लागू किया जाता है; एक डेटा संरचना मेमोरी में डेटा को संगठित करने का ठोस तरीका है जो किसी ADT को कार्यान्वित करती है।
-
Given array A = [2, 4, 6, 8, 10], show steps to insert 5 at position 2 and state time complexity. / दिया गया array A = [2, 4, 6, 8, 10], स्थान 2 पर 5 डालने के चरण दिखाएँ और समय जटिलता बताइए।
Show answer
To insert 5 at index 2: shift elements at indices 2..4 right: [2,4,6,8,10] -> [2,4,6,6,8] -> [2,4,5,6,8,10] after placing 5; time complexity O(n) due to shifting. / 5 को index 2 पर डालने के लिए index 2..4 के तत्वों को दाएँ शिफ्ट करें: [2,4,6,8,10] -> [2,4,6,6,8] -> 5 रखने के बाद [2,4,5,6,8,10]; शिफ्ट करने के कारण समय जटिलता O(n) है।
-
Write the inorder, preorder and postorder traversals of the BST formed by inserting keys 50,30,70,20,40,60,80 in that order. / उन कुंजियों 50,30,70,20,40,60,80 को दिए क्रम में डालकर बने BST के inorder, preorder और postorder traversal लिखिए।
Show answer
The BST structure gives inorder: 20,30,40,50,60,70,80; preorder: 50,30,20,40,70,60,80; postorder: 20,40,30,60,80,70,50. / BST के अनुसार inorder: 20,30,40,50,60,70,80; preorder: 50,30,20,40,70,60,80; postorder: 20,40,30,60,80,70,50।
-
Describe how a hash table using chaining handles collisions and state expected search time in terms of load factor α. / चेनिंग का उपयोग करने वाला एक हैश तालिका टकरावों को कैसे संभालता है और लोड फैक्टर α के संदर्भ में अपेक्षित खोज समय बताइए।
Show answer
In chaining each table bucket stores a linked list of elements that hash to that index; on collision the new element is appended to the list. Expected search time is O(1 + α) where α = n/m (n elements, m buckets). / चेनिंग में प्रत्येक बकेट में उन तत्वों की एक लिंक्ड सूची रहती है जो उसी इंडेक्स पर हैश होती हैं; टकराव होने पर नया तत्व सूची में जोड़ा जाता है। अपेक्षित खोज समय O(1 + α) होता है जहाँ α = n/m है।
-
Perform one pass of bubble sort on array [5,1,4,2,8] and show the array after that pass. / array [5,1,4,2,8] पर bubble sort का एक पास करें और उस पास के बाद array दिखाइए।
Show answer
Compare pairs and swap if out of order: (5,1)->(1,5): [1,5,4,2,8]; (5,4)->(4,5): [1,4,5,2,8]; (5,2)->(2,5): [1,4,2,5,8]; (5,8) no swap: [1,4,2,5,8]. This is array after one full pass. / जोड़े की तुलना कर गलत क्रम होने पर स्वैप करें: (5,1)->(1,5): [1,5,4,2,8]; (5,4)->(4,5): [1,4,5,2,8]; (5,2)->(2,5): [1,4,2,5,8]; (5,8) पर कोई स्वैप नहीं: [1,4,2,5,8]. यही एक पूरा पास के बाद का array है।
-
Explain why binary search cannot be efficiently applied on a linked list. / बताइए कि लिंक्ड सूची पर बाइनरी सर्च को प्रभावी रूप से लागू क्यों नहीं किया जा सकता।
Show answer
Binary search requires random access to the middle element in O(1) time; linked lists provide only sequential access so finding the middle takes O(n), nullifying binary search's O(log n) advantage. Thus overall time becomes O(n) per step leading to no improvement. / बाइनरी सर्च को मध्य तत्व तक O(1) में पहुँचने की आवश्यकता होती है; लिंक्ड सूची केवल क्रमिक पहुँच देती है, इसलिए मध्य तत्व ढूँढने में O(n) लगते हैं, जिससे बाइनरी सर्च का O(log n) लाभ समाप्त हो जाता है। अतः कोई सुधार नहीं होता।
-
Describe Dijkstra's algorithm idea and state one precondition on edge weights. / Dijkstra के एल्गोरिथ्म का विचार बताइए और किन्हीं एक पूर्व शर्त का उल्लेख कीजिए जो किनारे के भारों पर लगती है।
Show answer
Dijkstra grows shortest-path tree from a source by repeatedly selecting the unvisited vertex with smallest tentative distance (using a min-priority queue), relaxing its outgoing edges and updating distances. Precondition: all edge weights must be non-negative; with negative edges Dijkstra is invalid. / Dijkstra स्रोत से सबसे छोटे अनुमानित दूरी वाले न अवलोकित शीर्ष को चुनकर शॉर्टेस्ट-पाथ ट्री बढ़ाता है, उसके बाहरी एजों को relax कर दूरी अपडेट करता है और मिन-प्रायोरिटी क्यू का उपयोग करता है। पूर्व शर्त: सभी किनारे के भार गैर-नकारात्मक होने चाहिए; नकारात्मक भार पर Dijkstra मान्य नहीं है।
-
Show how a max-heap array [40, 30, 20, 10, 25] changes when 35 is inserted. / एक max-heap array [40,30,20,10,25] में 35 डालने पर यह कैसे बदलता है दिखाइए।
Show answer
Insert 35 at end: [40,30,20,10,25,35]; parent of 35 at index 2 is 20 (index 2), since 35>20 swap: [40,30,35,10,25,20]; parent of 35 now at index 1 is 30, 35>30 swap: [40,35,30,10,25,20]; parent of 35 at index 0 is 40, 35<=40, stop. Final heap [40,35,30,10,25,20]. / 35 को अंत में डालें: [40,30,20,10,25,35]; 35 के पिता (index 2) पर 20 है, 35>20 तो स्वैप करें: [40,30,35,10,25,20]; अब पिता (index 1) पर 30 है, 35>30 स्वैप: [40,35,30,10,25,20]; अब पिता (index 0) 40 है, 35<=40 इसलिए रोकें। अंतिम heap [40,35,30,10,25,20]।
-
What is the load factor of a hash table with 120 elements and 200 buckets, and what does it indicate? / 120 तत्व और 200 बकेट वाली हैश तालिका का लोड फैक्टर क्या है और यह क्या संकेत करता है?
Show answer
Load factor α = n/m = 120/200 = 0.6. It indicates on average 0.6 elements per bucket; higher α means more collisions and longer chains or probing sequences, so performance may degrade. / लोड फैक्टर α = n/m = 120/200 = 0.6। यह औसतन हर बकेट में 0.6 तत्व होने का संकेत देता है; α अधिक होने पर अधिक टकराव और लंबी चेन या probing होगी, जिससे प्रदर्शन घट सकता है।
-
Give one advantage of AVL trees over basic BSTs and one disadvantage. / बेसिक BST की तुलना में AVL ट्री का एक लाभ और एक हानि बताइए।
Show answer
Advantage: AVL trees maintain strict balance so operations (search, insert, delete) have guaranteed O(log n) worst-case time. Disadvantage: maintaining balance requires extra rotations and storing height information, increasing complexity and overhead. / लाभ: AVL ट्री कड़ा संतुलन बनाए रखते हैं इसलिए ऑपरेशनों के लिए श worst-case O(log n) सुनिश्चित होता है। हानि: संतुलन बनाए रखने के लिए अतिरिक्त rotations और ऊँचाई जानकारी की आवश्यकता होती है, जिससे जटिलता और ओवरहेड बढ़ता है।
Related Laws & Principles
Explore allFoundational laws & principles connected to this chapter — tap to open in the Laws Explorer.