Data Structure
Data Structure
A data structure is a specialized format for organizing, processing, storing, and retrieving data. It defines the relationship between data
elements and the operations that can be performed on them (for different programming languages).
Think of it this way: Imagine a library with books as data elements and the library shelves as the data structures. It is easier to browse the
vast number of books when they are organized/ categorised into different sections (fiction, non-fiction, etc.). Similarly, data structures
provide a systematic way to store and access data based on its type and intended use.
Data structures are fundamental to computer science and programming because they provide a means to manage large amounts
of data efficiently.
In simple terms, data structure definition is- it is a collection of data values and the relation between them, along with the
functions or operations that can be applied to the data.
They play a critical role in various applications, from databases and operating systems to artificial intelligence and machine learning.
However, you must choose the appropriate data structure based on the programming needs and programming languages to write efficient
and optimized code.
1. Data Storage: Data structures provide a way to store data in an organized manner, making it easier to manage and access the
data efficiently.
2. Data Organization: They organize data in a structured format, such as linear (arrays, linked lists) or non-linear (trees, graphs),
facilitating easier data manipulation.
3. Data Processing: They enable efficient data manipulation, including operations like insertion, deletion, and traversal, which
are optimized for performance based on the data structure used.
4. Memory Management: They manage memory usage effectively, with some data structures like linked lists and trees
dynamically allocating memory as needed.
5. Homogeneous Data: Many data structures store homogeneous data elements, meaning all elements are of the same type,
ensuring consistency and predictability.
6. Indexing: Certain data structures, such as arrays, allow for efficient indexing, providing direct access to elements based on
their index positions.
7. Sequential Access: Linear data structures like arrays and linked lists allow for sequential access to elements, facilitating
ordered data processing.
8. Dynamic Size: Some data structures, like linked lists and dynamic arrays, can adjust their size dynamically to accommodate
varying amounts of data.
9. Hierarchy Representation: Non-linear data structures, such as trees and graphs, represent hierarchical relationships between
elements, useful for modeling complex systems.
10. Support for Multiple Operations: Data structures support a variety of operations, including searching, sorting, merging, and
splitting, making them versatile tools for data manipulation.
11. Ease of Implementation: Many data structures are supported by standard libraries in programming languages, providing easy-
to-use implementations for common data structures.
Term Explanation
Node A basic unit of a data structure, such as linked lists and trees, contains data and pointers.
Pointer A variable that holds the memory address of another variable and is often used in linked structures.
Index The position of an element within an array, typically starting from 0.
Hash Table A data structure that maps keys to values for efficient data retrieval.
Traversal The process of visiting all the nodes or elements in a data structure.
Dynamic
Allocating memory at runtime, often used in structures like linked lists.
Allocation
Child Node A node that descends from another node in a tree structure.
Parent Node A node that has one or more child nodes in a tree structure.
Sibling Nodes Nodes that share the same parent in a tree structure.
A collection of lists used to represent a graph, where each list corresponds to a vertex and contains a list of
Adjacency List
adjacent vertices.
Adjacency A 2D array used to represent a graph, where each element indicates whether there is an edge between a pair of
Matrix vertices.
Self-Referential A data structure that includes pointers to instances of the same data structure, such as a node in a linked list
Structure pointing to the next node.
In other words, the data is represented in a sequential or linear order, where elements are arranged one after another, and access typically
happens in a specific order (e.g., by index). Its subtypes include- Arrays, Linked Lists, Stacks, and Queues.
Hash Tables: A data structure that stores key-value pairs, with keys mapped to unique indices using a hash function.
Hash Maps: Implementation of associative arrays that map keys to values.
Hash Sets: Collection of unique elements where each element is hashed to a unique bucket.
Binary Heaps: Complete binary tree where every parent node has a value less/greater than or equal to its child nodes.
Priority Queues: Abstract data type where elements are retrieved in order of priority.
Hashed and heaped types are specialized data structures that don't neatly fit into linear or non-linear classifications. They often serve
specific purposes and optimize for particular access patterns.
Some data structures, like arrays, have a fixed size at allocation and are hence known as static data structures.
Others, like linked lists, are dynamic and can grow or shrink as needed and are hence known as dynamic data structures.
So, static and dynamic data structures differ in managing memory allocation and must be chosen for efficient storage given the
requirements.
Static Data Structures: Have fixed sizes allocated at compile-time, and their size cannot change during runtime. Examples
include arrays and static linked lists.
Dynamic Data Structures: Allocate memory as needed during runtime, allowing them to grow or shrink dynamically.
Examples include dynamic arrays (ArrayLists in Java), dynamic linked lists, and trees that dynamically adjust their size based
on insertions and deletions.
1. Arrays: A collection of elements of the same data type stored at contiguous memory locations. Access is fast using an index
(position) within the array. Useful for storing fixed-size collections where random access is needed. There are three primary
types of arrays: one-dimensional arrays, two-dimensional arrays and multi-dimensional arrays .
2. Linked List Data Structure : A linear data structure where elements (nodes) are not stored contiguously in memory. However,
each node contains data and a reference (link) to the next/ subsequent node in the sequence. This allows for dynamic resizing
(growing or shrinking the list) and efficient insertions/deletions from specific points within the list.
3. Stack Data Structure: It is a linear data structure that follows the LIFO (Last In, First Out) principle, i.e., elements are added
(pushed) and removed (popped) from the top of the stack. Think of a stack of plates - you can only add or remove plates from
the top.
Useful for function calls (keeping track of nested function calls), implementing undo/redo functionality, and expression
evaluation (processing operators in the correct order).
4. Queue Data Structures: This linear data structure follows the FIFO (First In, First Out) principle, i.e., elements are added
(enqueued) at the back and removed (dequeued) from the front. Imagine a queue at a bus stop - the first person in line gets
served first. Useful for scheduling tasks (processing tasks in the order they are received), managing waiting lists, and
processing data in the order it arrives.
They provide more flexibility by allowing for data representation with inherent non-linearity, like networks, family trees, or geographical
maps. However, accessing elements in non-linear data structures often involves following these relationships or using keys to locate
specific elements.
1. Tree Data Structures: They are hierarchical structures with a root node, child nodes, central node, structural nodes, and
potentially sub-tress.
1. Efficient Data Management: Data structures allow for efficient data storage, retrieval, and manipulation. They provide ways
to organize data such that it optimizes various operations, such as insertion, deletion, searching, and updating. This translates to
quicker execution times and a smoother user experience.
2. Enhanced Performance: The choice of an appropriate data structure can significantly affect the performance of a program.
For example, using a hash table for fast data lookup, a binary search tree for sorted data access, or a queue for managing tasks
in a specific order can lead to more efficient and faster programs.
3. Memory Utilization: Data structures help in optimal memory utilization. By using the right data structure, you can minimize
memory wastage and manage memory allocation efficiently, ensuring that the program runs smoothly without unnecessary
consumption of resources.
4. Simplification of Complex Problems: Data structures break down complex problems into manageable parts. They provide a
way to model real-world problems in a structured manner, making it easier to implement and understand algorithms that
operate on these structures.
5. Reusability and Scalability: Well-designed data structures can be reused across different programs and applications. This
saves development time and promotes consistency in your codebase. They also provide a scalable way to handle increasing
amounts of data, ensuring that the program remains efficient and manageable as the data grows.
6. Data Integrity and Security: Proper data structures help maintain data integrity and security. Structures like linked lists and
trees ensure that data is linked logically, preventing accidental corruption or loss. Additionally, structures like cryptographic
hash tables enhance data security.
7. Improved Problem-Solving Skills: Understanding and implementing data structures enhance a programmer's problem-solving
skills. It provides a deeper understanding of how data is organized and manipulated, leading to better algorithm design and
more effective coding practices.
8. Foundation for Advanced Topics: Data structures form the foundation for many advanced computer science topics, such as
databases, operating systems, networking, and artificial intelligence. A solid grasp of data structures is essential for
understanding and excelling in these areas.
In summary, data structures are indispensable tools in programming. They provide the backbone for efficient data management, improve
program performance, and enable the implementation of complex algorithms.
Listed below are some primary operations that can be performed on data structures to maintain and manipulate the data:
1. Insertion: This operation refers to adding a new element to the data structure. For example, you can add a new item to the list
of elements you want to buy on a shopping cart (linked list).
2. Deletion Operation: This entails removing an existing element from the data structure. For example, you can remove an item
you no longer need from the shopping cart (linked list).
3. Traversal: This refers to traversing/ browsing through or visiting and accessing all elements in the data structure, often in a
specific order. For example, when you print a list of the grocery items you want to buy or iterate through all nodes in a linked
list or tree in a pre-order, in-order, or post-order fashion.
4. Access: The process of retrieving the value of an element based on its position (index) or key is referred to as access. For
example, accessing a specific item's price in a shopping list (array) based on its index (say, the middle element).
5. Search: The operation refers to searching for and finding a specific element based on a search criterion (e.g., value, key). Like
accessing the elements and browsing for something specific. For example, you might search the inventory (array or a hash
table) of an online store for a specific product by its name.
6. Sorting: This process entails arranging the elements of a data structure in a specific order (ascending, descending, based on
custom criteria). For example, sorting a list of student names alphabetically (array or linked list) or sorting the products in an
online store on the basis of their prices from lowest to highest.
7. Merging: It refers to combining two or more sorted data structures into a single sorted data structure. For example, combining
two different shopping lists to make one (linked list or array).
8. Reversing: This process entails reversing the order of elements in the data structure. For example, re-arranging or reversing a
list of student names that has been sorted from A to Z.
These are just some core operations; specific implementations can vary depending on the data structure. It is important to understand these
common operations to pick the right one for your specific needs
1. Database Management Systems (DBMS): Data structures like B-trees and hash tables are crucial for indexing and organizing
database data. They optimize data retrieval and ensure efficient storage.
2. Compiler Design: Compilers use data structures such as symbol tables and abstract syntax trees (ASTs) to analyze and
manage source code during compilation. These structures aid in parsing, semantic analysis, and code optimization.
3. Operating Systems: Operating systems utilize data structures like queues, stacks, and scheduling algorithms to manage system
resources, process scheduling, and memory allocation efficiently.
4. Networking: Data structures such as graphs and adjacency matrices are used to model and analyze network topologies, routing
algorithms, and connectivity in computer networks.
5. Artificial Intelligence and Machine Learning: In AI and ML applications, data structures like decision trees, graphs, and
hash tables are used for efficient data representation, searching, and processing large datasets.
6. Web Development: Data structures like arrays, linked lists, and hash maps are essential for managing dynamic content,
handling sessions, caching, and optimizing web applications for performance.
7. Spatial and Geographic Information Systems (GIS): GIS applications use data structures like spatial indexes and grids to
efficiently store and query spatial data, manage geographical information, and perform spatial analysis.
8. Cryptocurrency and Blockchain: Blockchain technology relies on data structures such as Merkle trees for secure and
efficient transaction verification and validation across distributed networks.
9. Bioinformatics: Data structures like sequence alignment algorithms, suffix trees, and graphs are used to analyze biological
data, DNA sequences, and protein structures in bioinformatics research.
Social Media Feeds: When you scroll through your social media feed, the posts are likely organized using a combination of
data structures. For example, heaps or priority queues might be used to prioritize posts based on factors like engagement or
relevance.
Online Shopping: Adding items to your shopping cart, searching for products by category or price, and filtering search results
rely on data structures. For example, arrays or hash tables might efficiently store product information.
Navigation Apps: Finding the quickest route on a map involves traversing a graph data structure. Nodes represent locations,
and edges represent the roads or connections between them.
Music Streaming Services: When you create a playlist or shuffle your music library, data structures (besides the music) are at
play. That is, linked lists might be used to represent the order of songs in a playlist.
Computer Games: Efficiently rendering complex game worlds, managing character inventories, and handling user input also
involve data structures. For example, trees or graphs could represent the game world's layout and connections between
different areas.
Web Browsers: Keeping track of browsing history, managing open tabs, and implementing back and forward buttons rely on
data structures. The stack data structure is the perfect way to fulfil these functionalities with its LIFO (Last In, First Out)
principle.
These are just a few examples, and the applications of data structures are vast and diverse.
Traversal is straightforward and typically Traversal can be more complex, often requiring algorithms
Traversal involves iterating from one end to another (e.g., like depth-first search (DFS) or breadth-first search (BFS) to
arrays, linked lists). explore nodes (e.g., trees, graphs).
Memory allocation is usually contiguous, and Memory allocation can be more scattered or hierarchical,
Memory
elements are stored in a continuous block of depending on the structure (e.g., trees may use pointers for
Allocation
memory. node connections).
Example Arrays, Linked Lists, Stacks, Queues Tress, Graphs, Hash Tables
What Are Algorithms? The Difference Between Data Structures & Algorithms
An algorithm is a well-defined set of instructions (step-by-step procedure) on how to take an input, process it and perform a specific task or
solve a particular problem.
They can be simple, like sorting a list of numbers, or complex, like finding the shortest path in a network.
Algorithms are evaluated based on their efficiency and performance, typically considering factors such
as time complexity (how fast an algorithm runs) and space complexity (how much memory an algorithm
uses).
These evaluations help determine the best algorithm to use for a given problem.
Imagine trying to follow a complex recipe - that's essentially what an algorithm is like for computers.
Think of data structures as the ingredients and containers used in a recipe (the data and how it's
organized).
Algorithms are the actual instructions (the recipe itself) that tell you how to combine and process those
ingredients to achieve a desired outcome
Organize and store data as per specific Provide step-by-step instructions for
Purpose
formats solving problems/ performing tasks
Efficiency Evaluated based on memory usage and Evaluated based on time complexity and
Measurement access time space complexity
Asymptotic notation is a mathematical tool used to describe the efficiency of algorithms in terms of their time or space complexity,
focusing on their behavior as the input size grows, in worst or best cases.
In the world of algorithms, efficiency is a crucial factor in determining performance. Asymptotic notation provides a mathematical
framework to analyze the time and space complexity of algorithms, helping us understand how they behave as input size grows. By using
notations like Big-O, Omega (Ω), and Theta (Θ), we can compare different algorithms and choose the most optimal one for a given
problem.
Comparing algorithms efficiently – Instead of calculating the exact runtime, we can compare algorithms based on how their
runtimes grow.
Predicting performance for large inputs – As input size increases, minor details become negligible, and the overall growth
rate becomes more important.
Providing a hardware-independent measure – Since it abstracts machine-dependent factors, it allows fair comparisons of
algorithms across different systems.
Thus, asymptotic notation helps in choosing the right algorithm by providing a clear idea of how the algorithm behaves as n becomes
large
Exact Runtime Analysis Vs. Asymptotic Analysis
Example:
Imagine an algorithm with an exact runtime function:
T(n) = 5n^2 + 3n + 7
Exact runtime analysis would evaluate this function precisely for different values of n.
Asymptotic analysis simplifies it by focusing on the highest order term, making it O(n²).
Mathematically, an algorithm is O(f(n)) if there exist positive constants c and n₀ such that:
This means that for sufficiently large inputs, the algorithm’s runtime does not grow faster than f(n), up to a constant factor c.
Complexity
Notation Example Algorithm Explanation
Class
Merge Sort,
O(n log Log-Linear
QuickSort (best Efficient sorting algorithms.
n) Time
case)
Mathematically, an algorithm is Ω(f(n)) if there exist positive constants c and n₀ such that:
In other words, Omega notation provides a lower limit on how much time or space the algorithm will take, ensuring that the algorithm's
performance will always be at least as good as the specified lower bound.
Ω(1) Constant Time Accessing an array element Best case remains constant, no matter the input size.
Ω(log n) Logarithmic Time Binary Search (best case) Best case involves finding the target early in the search.
Ω(n) Linear Time Traversing a linked list Must visit all elements in the list, even in the best case.
Ω(n²) Quadratic Time Bubble Sort (best case) Best case occurs when the list is already sorted.
Mathematically, an algorithm is Θ(f(n)) if there exist positive constants c₁, c₂, and n₀ such that:
Θ(1) Constant Time Array access (best case) Time remains constant for any input size.
Θ(log n) Logarithmic Time Binary Search (average case) Performance improves logarithmically as input grows.
Θ(n) Linear Time Traversing a linked list Must visit all elements in the list.
Θ(n log n) Log-Linear Time Merge Sort, QuickSort Sorting algorithms with logarithmic overhead.
Θ(n²) Quadratic Time Bubble Sort (average case) Time increases quadratically with input size
Mathematically, an algorithm is o(f(n)) if for all positive constants c, there exists an n₀ such that:
Mathematically, an algorithm is ω(f(n)) if for all positive constants c, there exists an n₀ such that:
Little-o (o) Strictly smaller than a given function o(n²), o(n log n)
Little-omega (ω) Strictly greater than a given function ω(n), ω(n log n)
Therefore:
Big-O vs Omega: Big-O is for the worst-case analysis (upper bound), while Omega is for the best-case analysis (lower bound).
Big-O vs Theta: Big-O provides an upper bound only, whereas Theta provides both upper and lower bounds, giving a tighter
and more exact representation of the algorithm’s growth rate.
Little-o vs Little-omega: Both provide non-tight bounds, but Little-o is for an upper bound that is strictly smaller, and Little-
omega is for a lower bound that is strictly larger.
1. Sorting Algorithms
Sorting is one of the most common tasks in computer science, and understanding the asymptotic behavior of sorting algorithms is key to
selecting the right algorithm for different scenarios.
For example:
Merge Sort and QuickSort: Both of these algorithms have Θ(n log n) time complexity on average. They are used when
dealing with large data sets, as their performance is more efficient than Θ(n²) algorithms like Bubble Sort or Selection Sort.
Bubble Sort and Insertion Sort: With time complexity of O(n²), these algorithms are often used for small datasets or as part
of hybrid algorithms. For example, Insertion Sort can be used in algorithms like Timsort for small partitions of data.
Example Application:
In scenarios like e-commerce websites where sorting products based on price, rating, or availability is required, QuickSort or Merge
Sort are ideal because of their efficient sorting capabilities. Using Big-O notation, we can compare the worst-case performance of
different sorting algorithms to decide the most efficient one for a particular use case.
2. Searching Algorithms
Efficient searching is essential when working with large datasets. Understanding the asymptotic notation helps determine the most
efficient search algorithm based on the input size.
Binary Search: If the data is sorted, Binary Search performs in Θ(log n) time, making it very efficient for large datasets
compared to a simple linear search, which takes Θ(n) time.
Example Application:
In databases or file systems, searching for records in a large dataset (e.g., searching for a customer in a customer database) is highly
optimized with algorithms like Binary Search. This helps companies save time and resources when querying large datasets.
3. Data Structures
The performance of different data structures can be evaluated using asymptotic notation to determine how efficiently operations
like insertion, deletion, searching, and accessing can be performed.
Hash Tables: Typically have O(1) time complexity for lookup and insertion, making them extremely fast for operations like
checking if a record exists.
Linked Lists: For operations like traversal, Θ(n) is the typical complexity, while operations like insertion and deletion at the
beginning can take Θ(1).
Example Application:
In caching systems or memory management, Hash Tables are commonly used to store and retrieve frequently accessed data efficiently.
The O(1) time complexity of hash tables ensures fast lookups, improving the performance of applications like web servers, operating
systems, and databases.
Dijkstra’s Algorithm: This shortest path algorithm has a time complexity of O(E log V), where E is the number of edges
and V is the number of vertices in the graph.
Bellman-Ford Algorithm: It runs in O(VE) time, which is slower than Dijkstra’s but can handle negative edge weights.
Example Application:
In telecommunication networks or cloud computing environments, routing protocols like Dijkstra's Algorithm are critical for
determining the most efficient paths for data transmission. By understanding the asymptotic behavior of these algorithms, engineers can
optimize network traffic and avoid bottlenecks.
K-means clustering: The time complexity of O(nk) for each iteration, where n is the number of data points and k is the
number of clusters, is important for large datasets.
Gradient Descent: The time complexity depends on the number of iterations and the number of parameters,
typically O(nk) or O(n²) for large-scale problems.
Example Application:
In data science and AI-driven applications, algorithms like K-means or Neural Networks are commonly used for clustering or
classification. Asymptotic notation helps determine which algorithm will scale better with large amounts of training data, enabling faster
training and prediction times.
6. Web Development and User Interfaces
Asymptotic analysis is also valuable in web development, where we need to optimize the performance of web pages, especially with
dynamic content and large datasets.
Rendering a webpage: The time complexity of rendering HTML, CSS, and JavaScript code on a webpage can be analyzed.
For example, O(n) time complexity might be expected for iterating through elements on a page, but inefficient algorithms can
increase the time it takes to load and render pages.
Lazy Loading: With large datasets, implementing lazy loading (loading data as the user scrolls) can help reduce loading
times. The time complexity for this could be O(n) for fetching data from a server, but optimizations like pagination can lower
this to O(1).
Example Application:
For e-commerce websites with thousands of products, lazy loading and pagination strategies ensure that users only load small chunks of
data, making the user experience smoother and faster. By understanding the asymptotic behavior of different techniques, web developers
can improve site performance.
Conclusion
In this article, we explored the different types of asymptotic notations—Big-O (O), Omega (Ω), Theta (Θ), Little-o (o), and Little-
omega (ω)—which are essential tools in the analysis of algorithms. These notations allow us to describe and compare an algorithm’s
efficiency in terms of its time and space complexity, helping developers and computer scientists understand how algorithms will perform
as the input size grows.
Big O notation describes an algorithm's efficiency in terms of time and space complexity as input size grows. It helps compare algorithms
by focusing on their worst-case, best-case, or average-case performance trends.
When analyzing algorithms, efficiency is a key factor. We often need to determine how an algorithm's performance scales as the input size
grows. This is where Big O Notation comes in—it provides a standardized way to describe the time and space complexity of algorithms.
In this article, we'll explore what Big O Notation is, why it's important, and how different complexities impact algorithm performance.
We'll also look at common Big O complexities with examples to help you understand their real-world implications.
Understanding Big O Notation
Big O Notation is a mathematical concept used in computer science to describe the efficiency of algorithms in terms of time and space. It
provides a standardized way to express how the execution time or memory usage of an algorithm grows as the input size increases. Instead
of focusing on exact execution times, Big O focuses on the growth rate, helping us compare algorithms independently of hardware and
implementation details.
For example, if an algorithm takes 5 milliseconds for an input of size n = 100 and 20 milliseconds for n = 200, we need a way to describe
this growth pattern. Big O helps by giving us a general formula, such as O(n) or O(log n), to express this behavior.
Bubble Sort has a time complexity of O(n²), meaning the execution time grows quadratically as input size increases.
Merge Sort has a time complexity of O(n log n), which grows more efficiently.
If we sort 1,000 elements, Bubble Sort takes roughly 1,000,000 steps (1,000²), while Merge Sort takes around 10,000 steps (1,000 ×
log₂(1,000)). Clearly, Merge Sort is the better choice for larger datasets.
By understanding Big O Notation, we can make informed decisions about which algorithms to use in different scenarios, ensuring optimal
performance for various applications.
Definition: The execution time remains the same, regardless of the input size.
Example: Accessing an element in an array using its index.
Definition: The execution time grows logarithmically, meaning it increases slowly as input size increases.
Example: Binary Search (which repeatedly divides the search space in half).
int binarySearch(int arr[], int left, int right, int key) {
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == key) return mid;
else if (arr[mid] < key) left = mid + 1;
else right = mid - 1;
}
return -1;
}
3. O(n) – Linear Time
Definition: The execution time grows directly with the input size.
Example: Looping through an array.
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
4. O(n log n) – Linearithmic Time
Definition: The execution time grows slightly faster than linear time but much slower than quadratic time.
Example: Merge Sort and Quick Sort.
void mergeSort(int arr[], int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right); // Merging takes O(n)
}
}
5. O(n²) – Quadratic Time
Definition: The execution time grows proportionally to the square of the input size.
Example: Nested loops in Bubble Sort.
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cout << i << j << " ";
}
}
6. O(2ⁿ) – Exponential Time
Definition: The execution time doubles with each additional input element.
Example: Recursive Fibonacci sequence.
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
7. O(n!) – Factorial Time
Definition: The execution time grows at an extremely fast rate, making it impractical for large inputs.
Example: Generating all permutations of a set.
void permute(string s, int l, int r) {
if (l == r) cout << s << endl;
else {
for (int i = l; i <= r; i++) {
swap(s[l], s[i]);
permute(s, l + 1, r);
swap(s[l], s[i]); // Backtrack
}
}
}
Summary Table
1. Fixed part – Memory required for variables, constants, and program instructions.
2. Variable part – Memory required for dynamic allocation (e.g., arrays, recursion stack).
Like time complexity, space complexity is expressed using Big O Notation to describe how memory usage grows with input size n.
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
Complexity: Each call spawns two new calls, leading to O(2ⁿ) growth.
Definition: The minimum time an algorithm takes when given the most favorable input.
Example: Searching for an element at the first position in an array.
Notation Used: Ω (Omega) represents the lower bound.
Example: Linear Search
Definition: The maximum time an algorithm takes for the worst possible input.
Example: Searching for an element that is not in an array.
Notation Used: O (Big O) represents the upper bound.
Example: Linear Search
Worst case complexity: O(n) (if the element is not found or at the last index).
Definition: The expected time an algorithm takes over all possible inputs.
Example: Searching for an element that is equally likely to be anywhere in the array.
Notation Used: Θ (Theta) represents the tight bound (average case).
Example: Linear Search
Average case complexity: Θ(n/2) ≈ Θ(n) (assuming uniform distribution of search queries).
Real-World Significance of These Cases
Best Case (Ω) Ideal scenario Finding an element at the first index in Linear Search
Worst Case (O) Guarantees an upper limit Searching for a non-existent element in an array
Average Case (Θ) Most practical scenario Searching for an element randomly positioned in an array
Why does this matter?
1. Algorithm Analysis
Determines which data structure (e.g., arrays, linked lists, hash tables) is best suited for a given
problem.
Example: Hash tables provide O(1) lookup, while binary trees provide O(log n) lookup.
4. Scalability Testing
Used in indexing and searching techniques (e.g., O(log n) binary search in databases).
Helps in query optimization for faster data retrieval.
7. Artificial Intelligence & Machine Learning
In real-world applications, Big O notation plays a crucial role in scalability, data structure selection, competitive programming, database
optimization, and even artificial intelligence. Whether we are improving search algorithms, optimizing network routing, or enhancing
software performance, Big O provides a clear framework for measuring efficiency.
By mastering Big O notation, we gain the ability to write faster, more efficient code, ensuring our applications run smoothly even as data
grows.
Time Complexity Of Algorithms: Types, Notations, Cases, and More
This article is a valuable read for developers, offering insights into time complexity, including notations
like Big O, Omega, and Theta, as well as its types such as constant, logarithmic, linear, quadratic, &
exponential.
We overlook those external constant factors and are solely concerned about the number of times a particular statement is now being
executed about the size of the input data. The amount of time it takes for an algorithm to complete its task is contingent not only on the
computing speed of such a system that you are now using but also on the amount of time it takes the algorithm to process the data it
receives. Let's say the amount of actual time needed to execute a single statement is one second; in such a case, how much unit time is
needed to execute statements? This should take seconds for it to finish.
The time complexity of an algorithm is a metric that may be used to quantify the amount of time required for an algorithm to run as
just a function of the length of the input. The space complexity of the algorithm is the measurement of how much memory or storage
space it requires to run, and it is expressed as binary search functions of the amount of such data that is being processed.
A linked list is an example of a dynamic data structure where the total number of nodes within the list is not predetermined; rather, the list
can expand or contract according to the needs of the application.
3. BFS algorithm
The “Breadth-First Search” (BFS) algorithm is used to traverse graphs. It begins its journey across the graph at the node that serves as its
origin and continues to investigate all of the nodes that border it. It chooses the node on the boundary and then navigates to all of the nodes
that have not been explored previously. The algorithm repeats the same procedure for every neighboring node up until the point where it
reaches the ambitious state.
Depth-first search technique is utilized in topological sorting, scheduling issues, cycle identification in graphs, and the resolution of puzzles
with just one solution, including mazes and sudoku puzzles. Other uses include the careful analysis of networks, such as determining
whether or not a graph has a bipartite structure.
Best case: This is the time limit at which an algorithm can be run. We must be familiar with the
scenario that results in the fewest amount of procedures being carried out. In the illustration, our
array is [1, 2, 3, 4, 5], and the question we are attempting to answer is whether or not the number
"1" is contained within the array. Now that you've made only one comparison, you should be able
to deduce that the element in question is indeed contained within the array. Therefore, this
represents the best possible outcome of your program.
Average case: We first determine the amount of time required to complete the task based on each
of the many inputs that could be used, then we add up all of the results, and finally, we divide the
total by the total number of inputs. We must be aware of (or can forecast) the distribution of
cases.
Worst case: This is the upper bound on the amount of time it takes for an algorithm to complete
its task. We must identify the scenario that results in the highest possible number of arithmetic
operations being carried out. In the context of this discussion, the worst possible scenario would
be if the array that was provided to us was [1, 2, 3, 4, 5], and we tried to determine whether or not
the element "6" was included in the array. Following the completion of the if-condition of our
loop's iteration five times, the algorithm will provide the value "0" as the final result.
Time Complexity: Different Types Of Asymptotic Notations
Asymptotic notations are the mathematical tools to determine the time complexity of an algorithm. The following are the three different
types of time complexity asymptotic notations:
Theta encloses functions from both the lower and upper sides. Theta notation is used to describe the average time of the algorithm as it
describes the lowers and the upper bound of the function.
Θ (g(n)) = {f(n): there exist positive constants c1, c2 and n0 such that 0 ≤ c1 * g(n) ≤ f(n) ≤ c2 * g(n) for all n ≥ n0}
Note: Θ(g) is a set
O(g(n)) = { f(n): there exist positive constants c and n0 such that 0 ≤ f(n) ≤ cg(n) for all n ≥ n0 }
Omega notation gives the lower bound or the best-case running time of an algorithm's time complexity. In simple words, it gives the
minimum time required by the algorithm.
Ωg(n) = { f(n): there exist positive constants c and n0 such that 0 ≤ c*g(n) ≤ f(n) for all n ≥ n0}
Note: Ω (g) is a set
Consider the example of selection sort where it takes O(1) in the best case and O(n^2) in the worst case. Now the Omega will be the O(1) as
it is the lower bound of the algorithm.
The following steps are followed for calculating the Big - Omega (Ω) for any program:
1. First, take the program and break them into smaller segments.
2. Find the number of operations performed for each segment (in terms of the input size) assuming
the given input is such that the program takes the least amount of time.
3. Add up all the operations and calculate the time. Say it comes out to be f(n).
4. Remove all the constants and choose the term having the least order or any other function which is
always less than f(n) when n tends to infinity. Let's say it is g(n) then, Big - Omega (Ω) of f(n) is
Ω(g(n)).
How To Calculate Time Complexity?
Good knowledge of calculating different types of time complexity lets us make informed decisions regarding algorithm design,
optimization, and performance analysis.
In simple terms, the process of calculating the time complexity of an algorithm involves analyzing the number of operations performed as a
function of the input size, enabling us to estimate its efficiency and scalability.
For Example: When we search for an element in an array (say arr[]) with arr[1] so it's a constant time complexity.
Here C is a constant term (pre-defined in the code and not taken as input from the user)
for(int i=0;i<C;i++){
//having O(1) time complexity
}
Linear Time Complexity: O(n)
Linear time complexity denoted as O(n), involves analyzing the number of operations performed in proportion to the input size (n).
Calculating the time complexity of an algorithm with linear time complexity involves identifying the key operations, counting iterations, and
observing how the algorithm's runtime grows in a linear fashion as the input size increases.
//Here c is a constant
for(int i=0;i<n;i+=c){
//Here n is a variable
}
// In recursive Algorithm
void recursion(int n ){
if(n==1) return;
else{
// constant time expression only
}
recursion(n-1)
}
Explanation:
In the first snippet, we have a for loop that runs in linear time complexity. The loop iterates from 0 to n, incrementing the loop variable by a
constant amount (c) in each iteration. Since the loop variable changes by a constant value, the time complexity is O(n). This means the
runtime of the loop increases linearly with the input size (n).
While the time complexity of the recursive algorithm in the second snippet can vary depending on the presence and dominance of constant
time expressions.
for(int i=0;i<n;i++){
//here n is variable
//linear loop taking 0(n) time
for(int j=0;j<n;j++){
//loop inside a loop it also takes 0(n)
}
}
Explanation:
In the code, we have an outer for loop that iterates from 0 to n, with n being a variable. Inside the outer loop, there is an inner for loop that
also iterates from 0 to n. This creates a nested loop structure where the inner loop is executed multiple times for each iteration of the outer
loop.
The time complexity of the outer loop itself is O(n) because it runs linearly with the input size (n). Similarly, the inner loop also has a time
complexity of O(n) as it iterates n times.
When we have a nested loop structure like this, the time complexity is determined by multiplying the time complexities of the individual
loops. In this case,
O(n)*O(n) = o(n^2)
This multiplication is only done because it is a case of nested loop (loop inside a loop).
Similarly, if there are ‘m’ loops defined in the function, then the order is given by O (n ^ m), which are called polynomial time
complexity functions.
//here c is a constant
for(int i =1;i<n;i*=c){
//some O(1) expression
}
for(int i=1;i<n;i/=c){
// some O(1) expression
}
Explanation:
In the First loop, we are multiplying the variable ''with c in every iteration so it becomes 1,c,c^2,c^3,...…,c^k.
If we put k equals to Logcn, we get cLogcn which is n.
So here our Input data size shrinks after every iteration as -
1st Iteration i=1 && i becomes i*2 = 2
2nd Iteration i=2 && becomes I*2 = 4;
3rd Iteration i=4 && becomes i*2 = 8
and...so on
This means that after every iteration, our input data size shrinks so the algorithm is knowns as logarithms.
Exponential Time Complexity: O(2^n)
An exponential time algorithm refers to an algorithm that increases rapidly in magnitude as the input data grows. This type of algorithm is
commonly found in recursive algorithms, where at each step, there are two possible options or paths to explore.
Through each iteration, the algorithm progressively divides the problem space until it reaches a stopping point defined by a base condition.
The exponential growth arises from the branching nature of the algorithm, as the number of possibilities doubles with each iteration.
The Fibonacci series would be a great example of explaining the exponential time complexity. Below is the code of the Fibonacci series:
Explanation:
In the fib function, we work with a recursive approach to calculate the Fibonacci sequence. To begin, we define a base condition that acts as
the stopping point for the recursion. For any given input, we make recursive calls to fib(n-1) and fib(n-2) to calculate the values of the
previous two elements in the Fibonacci sequence. The problem is repeatedly divided into smaller subproblems, and when we reach the base
condition, the recursion ends and the final Fibonacci number is calculated.
When we have 'c' nested loops, the time complexity is determined by multiplying the time complexities of each loop together. This can be
expressed as -
//here C is a constant
for(int i=0i<n;i+=C){
//this loop take O(n)
for(int j=0;j<n;j*=C){
//this loop takes O(logn) as discussed above
}
}
Explanation:
So here we have two loops - the outer loop takes O(n) and the Inner loop takes O(logn) so the time complexity becomes:-
In this scenario, the outer loop iterates through the input in a linear manner, meaning its execution time grows linearly with the input size
(n). Inside this outer loop, there is an inner loop that operates with a time complexity of O(log n). This implies that the inner loop's execution
time increases logarithmically with the input size. This means the overall time complexity of the code, considering both the linear outer loop
and the logarithmic inner loop, is expressed as O(n log n).
1. Selection Sort
It has O(n^2) time complexity in the best case, the average case as well as in the worst case.
2. Merge Sort
It has O(NlogN) time complexity in the best case, the average case as well as in the worst case.
3. Bubble Sort
This is the simplest sorting algorithm. it has an O(n^2) time complexity in both the worst as well as best-case.
4. Quick Sort
It has O(NlogN) complexity in the best and average case. But in the worst case, it becomes O(N^2).
5. Heap Sort
It has O(NlogN) time complexity in the best case, the average case as well as in the worst case.
6. Bucket Sort
It has O(N+k) complexity in the best and average case. But in the worst case, it becomes O(N^2).
7. Insertion Sort
The time complexity of the insertion sort algorithm in the best case is O(n) and in the worst case, it is O(n^2).
Example 1:
The reason why this code always has a time complexity of O(n^2) is that for every outer loop, you have to travel the inner loop. There is no
way that you can skip the inner loop condition.
However, there are some sorting algorithms that are more efficient than this. For example, Merge Sort is considered one of the fastest and
more optimized sorting algorithms.
Example 2:
Merge Sort is a sorting algorithm that divides the array into smaller subarrays, sorting them and merging them in the final step so you get a
sorted array in the final result. This code starts with the function mergeSort which is called from the main function. You are given low and
high, then you find the mid and divide the given array into two subarrays. This continues until you are left with a single element.
Then function merge is called to merge the elements in sorted order (i.e. ascending order), ensuring efficient sorting with a time complexity
of O(nlogn).
However, people often get confused about space complexity with the term auxiliary space. Auxiliary space is just the temporary or extra
space required by the algorithm.
A linked list, on the other hand, is a data structure that can perform insertions and deletions in constant time, O(1), but requires linear time,
O(n), to search for an element. A linked list also requires less memory than a hash table, as it only stores the data and a pointer to the next
node.
Conclusion
The concept of time complexity refers to the quantification of the length of time it takes a set of instructions/ algorithm to process or run as
just a function of the total quantity of data that is fed into the system. To put it another way, time complexity refers to a native program
function's efficiency as well as the amount of time it takes for the function to process a certain input. The quantity of memory that an
algorithm requires to carry out its instructions and generate the desired output is referred to as its "space complexity".
Array In C++ | Define, Types, Access & More (Detailed Examples)
Arrays in C++ are a collection of elements stored in contiguous memory locations. All elements in an
array are of the same data type and are accessed using an index. The first element has an index of 0, the
second element has an index of 1, and so on.
30 mins read
An array in C++ is a collection of elements of the same data type arranged in consecutive memory locations. This powerful data structure
allows us to manage and organize data efficiently, providing easy access to each element using an index.
In this article, we'll explore the different aspects of arrays in C++. We'll start by understanding how to declare and initialize arrays, followed
by discussing their advantages, limitations, and common use cases.
In the diagram below, you can see an array with 7 elements. The syntax for this is given at the top of the image.
The line of code in the image above shows how to declare an array of integers with 7 elements. That is-
The expression int myArray[7] signifies that the elements will be of integer data type. The name
of the array is myArray, and it has 7 elements. We will discuss this in greater detail in the next
section.
Each empty box in the image above corresponds to a component of the array. These values are of
the type int in this instance. The numbers 0 through 6 represent the position of the elements, with
0 being the first and 6 being the last. The index for the first element of the array in C++ is always
zero.
How To Declare An Array In C++?
Whenever declaring an array in C++, you must typically provide both the size and the type of elements it will contain. In short, we must
write the data type, the array name, and a bracket [ ]( index notation) indicating the number of elements the array holds.
type arrayName[arraySize];
Here,
The term type refers to the data type of elements in the array.
The arrayName refers to the name given to the array being created.
And the arraySize refers to the number of elements that will be there in the entire collection or
array.
Example: string names[2];
The snippet above, when used in a code, will declare an array with two string elements, just like a normal variable.
1. Specify the Data Type: We must specify the data type of the elements that the array will hold beforehand. The data type can be
any fundamental data type (e.g., int, double, char) or user-defined data type (e.g., structures or classes). It is also important to
note that an array in C++ can hold elements of a single data type only.
2. Choose a Valid Identifier: The name of the array must be a valid identifier, following the rules of C++ naming conventions
(e.g., cannot start with a digit, cannot contain spaces, etc.). The name should be meaningful and reflect its purpose.
3. Use Square Brackets: Square brackets [] are used to declare an array. You specify the size of the array within these brackets. If
the size is not known at compile-time, you must use a constant expression or a literal. Example: int numbers[5];
4. Zero-Based Indexing: In C++, array indexing is zero-based, meaning the first element is accessed using index 0, the second
using index 1, and so on. The last element is accessed using the index arraySize - 1. We will discuss the method of accessing
elements in a section later on.
5. Initialization (Optional): You can optionally initialize the array elements at the time of declaration. Initialization can be done
using an initializer list, enclosing the values within curly braces {} and separating them with commas. Example: int numbers[5]
= {1, 2, 3, 4, 5};
6. Fixed Size: The size of the array is fixed at compile time, and it cannot be changed during the program's execution. If you need a
dynamically sized collection, you might have to consider using a dynamic data structure like a vector or allocating memory
dynamically using pointers. Example: int dynamicArray[]; // Not allowed, size must be specified
2. Initializing An Array Without Specifying Size (Implicit Size, Inline Initialization Of Arrays In C++)
We can initialize an array without explicitly specifying its size. In this case, the compiler automatically determines the size based on the
number of elements provided.
int arr[5];
for (int i = 0; i < 5; i++) {
arr[i] = i + 1;
}
In this method, we declare the array first and then use a loop to assign values to each element. For instance, the above loop initializes arr[0]
with 1, arr[1] with 2, and so forth. This approach is highly flexible, allowing us to modify the loop conditions or the values assigned to meet
specific requirements.
Now let's take a look at an example to see how this is implemented in practice.
Code:
#include <iostream>
int main() {
// Initialize an array during the declaration
int myArray[] = {10, 20, 30};
return 0;
}
Output:
1. In the code above, we declare and initialize an array called myArray with three elements: 10, 20,
and 30.
2. During the declaration, we provide the initial values of the elements within curly braces {}, as
mentioned in the code comment.
3. The size of the array is automatically determined based on the number of elements in the
initializer list.
4. Next, we start a for loop, which iterates through the array and prints its elements.
5. We use sizeof(myArray) / sizeof(myArray[0]) to calculate the number of elements in the array,
which ensures that the loop iterates through all the elements.
6. Finally, we access and print individual elements of the array using index notation along with the
cout command. (For e.g., myArray[0] for the first element, myArray[1] for the second element, and
so on).
7. The output shows the initialized array and the values of its elements.
Syntax:
arrayName[index]
Here:
#include <iostream>
int main() {
int arr[5] = {10, 20, 30, 40, 50}; // Array declaration and initialization
return 0;
}
Output:
Element at index 0: 10
Element at index 3: 40
Explanation:
1. Inside the main() function, we declare and initialize an integer array arr of size 5 with the
values {10, 20, 30, 40, 50}.
2. We then access and print specific elements of the array directly using their index values.
We access the element at index 0 and print its value, which is 10.
Similarly, we access the element at index 3 and print its value, which is 40.
Finally, the main() function returns 0, indicating that the program has completed successfully.
Syntax:
Code:
#include <iostream>
int main() {
int arr[5] = {10, 20, 30, 40, 50}; // Array declaration and initialization
return 0;
}
Output:
Array elements:
Element at index 0: 10
Element at index 1: 20
Element at index 2: 30
Element at index 3: 40
Element at index 4: 50
Explanation:
1. Inside the main() function, we declare and initialize an integer array arr of size 5 with the
values {10, 20, 30, 40, 50}.
2. To display the elements of the array, we use a for loop that iterates through the array indices
from 0 to 4.
3. During each iteration of the loop, we print the index and the value of the element at that index
using cout.
4. Finally, the main() function returns 0, indicating that the program executed successfully.
How To Update/ Change Elements In An Array In C++?
Updating or changing elements in an array in C++ is a straightforward process. Each element in an array is accessed using its index, starting
from 0 for the first element. To modify an element, we simply assign a new value to the specific index. This can be done individually for
each element or in bulk using loops.
Code:
#include <iostream>
using namespace std;
int main() {
int arr[5] = {10, 20, 30, 40, 50};
return 0;
}
Output:
Explanation:
1. Inside the main() function, we declare an integer array arr of size 5 and initialize it with the values {10, 20, 30, 40, 50}.
2. Next, we update the third element of the array (arr[2]). Since array indices start at 0, arr[2] corresponds to the third element. We
change its value from 30 to 100.
3. To display the updated array, we use a for loop that iterates through each element of the array.
4. We use cout to print each element of the array, followed by a space for readability.
5. Finally, the main() function returns 0, indicating successful execution of the program.
How To Insert & Print Elements In An Array In C++?
To insert and print elements in an array in C++, we first declare an array with a fixed size and initialize its elements. To insert a new
element, we choose the position for insertion, ensuring there's enough space in the array. We then use a loop to input values and store them
in the array. After inserting the element, we use another loop to print the array’s contents. This method helps us manage array indices and
avoid out-of-bounds errors while ensuring that our array operations are smooth and effective.
Code:
#include <iostream>
using namespace std;
int main() {
int arr[6], i, elem;
return 0;
}
Output:
Explanation:
1. Inside the main() function, we declare an integer array arr of size 6, and two integer variables: i for loop iteration and elem to
hold the new element we want to insert.
2. Next, we prompt the user to enter 5 array elements and use a for loop to read these elements from the user and store them in the
array arr.
3. After that, we again ask the user to enter an additional element. We insert this new element into the 6th position of the array (i.e.
index 5), which was previously unassigned.
4. Finally, we print a message indicating the updated array, then use a for loop to print all 6 elements of the array.
Code:
#include <iostream>
int main() {
int arr[5] = {1, 2}; // Partially initialized array with 5 elements
return 0;
}
Output:
Array elements:
Element 0: 1
Element 1: 2
Element 2: 0
Element 3: 0
Element 4: 0
Explanation:
1. Inside the main() function, we declare an integer array arr of size 5, but we only provide two initial values {1, 2}. The remaining
empty elements of the array are automatically initialized to 0.
2. We then use a for loop to print each element of the array. The loop runs from index 0 to 4, covering all the indices of the array.
3. For each iteration, we print the index and the corresponding element value. Since only the first two elements were explicitly
initialized, the rest of the empty elements are 0 by default.
4. Finally, the main() function returns 0, signaling the successful completion of the program.
Size Of An Array In C++
The size of an array in C++ refers to the total number of elements that the array can hold. This size is fixed at the time of array declaration
and cannot be changed during runtime. The size of array determines the number of memory locations allocated for storing its elements, and
it's crucial to know this size when performing operations like traversing or accessing elements.
The size can be calculated using the sizeof operator, which returns the total memory occupied by the array in bytes. To determine the
number of elements, you can divide the total size of the array by the size of a single element.
Syntax:
sizeof(arrayName) / sizeof(arrayName[0])
Here,
#include <iostream>
int main() {
int arr[5] = {10, 20, 30, 40, 50}; // Array declaration with 5 elements
std::cout << "The size of the array is: " << size << std::endl; // Output: 5
return 0;
}
Output:
1. Inside the main() function, we declare and initialize an integer array arr with 5 elements,
specifically {10, 20, 30, 40, 50}.
2. To calculate the size of the array, we use the sizeof operator.
The sizeof(arr) gives us the total memory size of the array in bytes,
while sizeof(arr[0]) gives us the size of a single element.
By dividing these two values, we obtain the number of elements in the array, which we
store in the variable size.
We then print the calculated size of the array, which is 5.
How To Pass An Array To A Function In C++?
In C++, you can pass an array to a function by either directly passing the array as an argument or passing a pointer to the array. When you
pass an array to a function, the function can access and modify the elements of the array. Here are the two common methods to pass an array
to a function:
Code:
#include <iostream>
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int size = sizeof(numbers) / sizeof(numbers[0]);
// Calling the function and passing the array and its size
printArray(numbers, size);
return 0;
}
Output:
10 20 30 40 50
Method 2: Pass Array As A Pointer
In this method, you pass a pointer to the array as a function parameter. This method is equivalent to passing the array by reference. Again,
any changes made to the array inside the function will affect the original array. Look at the example below for a better understanding.
Code:
#include <iostream>
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int size = sizeof(numbers) / sizeof(numbers[0]);
// Calling the function and passing a pointer to the array and its size
printArray(numbers, size);
return 0;
}
Output:
10 20 30 40 50
What Does Out Of Bounds Array In C++ Mean?
Accessing elements of an array in C++ outside its defined bounds leads to undefined behavior. It is commonly referred to as an "array out of
bounds" or "array index out of range" error in programming. Let's understand what happens when you access elements out of bounds and
how to avoid such errors.
When you access an array element using an index, the C++ compiler does not perform any built-in bounds checking to ensure that the index
is within the valid range. Instead, it assumes that you, as the programmer, have provided a valid index, and will proceed to read or write to
the memory location corresponding to that index.
Code:
#include <iostream>
int main() {
int numbers[5] = {10, 20, 30, 40, 50};
return 0;
}
Output:
numbers[0] = 10
numbers[1] = 20
numbers[2] = 30
numbers[3] = 40
numbers[4] = 50
Attempting to access numbers[10] = 1310551306
Explanation:
1. In the code above, we first declare an integer array arr with 5 elements. The elements of the array
are initialized with the values 10, 20, 30, 40, and 50.
2. We then use a for loop to access and print each element of the array within its valid bounds, which
are indices 0 to 4. The loop iterates over each index, and we output the index along with the
corresponding value from the array.
3. After accessing the elements within bounds, we attempt to access an element outside the bounds of
the array by setting an index variable to 10, which is greater than the maximum valid index (4).
We then try to access numbers[10] and print its value.
4. Accessing an element outside the bounds of the array, like numbers[10], is undefined behavior in
C++. This can lead to unexpected results or program crashes, as the index is out of the array's
valid range.
5. Note that every time you run the code, the output for index 10, will change.
C++ Array Types
The cpp arrays can be divided into several types depending on their size, storage duration, and how they are created. Listed below are some
of the most common types of C++ arrays:
Syntax:
type array-Name [ x ][ y ];
Here,
Within the realm of C++, a collection of arrays with numerous dimensions is known as a multidimensional array. It is more commonly
referred to as an array of arrays. An array that has more than one dimension can take on various forms, such as a two-dimensional array or
2D array, a three-dimensional array or 3D array, or an N-level dimensional array. In the above forms mentioned, two-dimensional and three-
dimensional arrays are usually used in C++.
type name[size1][size2]...[sizeN];
Here,
#include <iostream>
using namespace std;
int main() {
// Declare and initialize a 3D array of size 2x3x4
int arr[2][3][4] = {
{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
},
{
{13, 14, 15, 16},
{17, 18, 19, 20},
{21, 22, 23, 24}
}
};
return 0;
}
Output:
1234
5678
9 10 11 12
13 14 15 16
17 18 19 20
21 22 23 24
Explanation:
1. We declare a three-dimensional or 3D array integer array named arr inside the main() function
and give a starting value range of 1 to 24.
2. Then, we access each element of the array and display it on the console using three nested loops.
3. Due to the array's two layers, the outer loop goes from 0 to 1. There are 3 rows in each layer
which is why the center loop goes from 0 to 2.
4. Since each layer has four columns, the inner loop goes from 0 to 3.
5. The cout statement is used to display each element of the array inside the innermost loop.
6. The return statement is then utilized to end the program.
Pseudo-Multidimensional Array In C++
C++ pseudo multidimensional arrays are special arrays composed of just one block of allocated memory, yet they behave as if they were
multidimensional due to their use of pointer arithmetic for component access. By utilizing this technique, you could represent even large
two-dimensional arrays with just one single-dimensional structure.
When it comes time to retrieve elements from this array, simply rely on the formula: a[i * n + j]. This will enable quick and easy access
based on row i and column j.
Code:
#include <iostream>
using namespace std;
int main() {
// Declare and initialize a pseudo-2D array of size 2x3
int arr[] = {1, 2, 3, 4, 5, 6};
return 0;
}
Output:
123
456
Explanation:
1. Inside the main() function, we declare and initialize a one-dimensional integer array arr with 6
elements: {1, 2, 3, 4, 5, 6}. This array is used to simulate a pseudo multidimensional array with 2
rows and 3 columns.
2. We then use nested for loops to print the elements of this pseudo multidimensional array.
3. The outer loop iterates through the rows (i ranging from 0 to 1), and the inner loop iterates
through the columns (j ranging from 0 to 2).
4. Inside the inner loop, we calculate the index for accessing elements in the one-dimensional array.
The index is computed as i * 3 + j, where i represents the current row and j represents the current
column. This formula maps the 2D indices to the correct index in the 1D array.
5. We finally print each element followed by a space to format the output as rows and columns, and
then print a newline after each row using cout << endl.
Pointers And Array In C++
Pointers are important variables that point to the location/ memory address of an object. They are especially important when storing an
object in any data structure. Hence, the relationship between pointers and arrays is noteworthy.
For instance, say the initial element in an array has a pointer that bears similar characteristics as the name of that array. We can then use
pointers to gain access to specific items in an array or to keep track of their element addresses. We will explore the relationship between
these, but first, lets take a look at the basic syntax for declaring a pointer to an array:
type (*ptr)[size];
Here, type is the data type of array elements, *ptr is the pointer to an array, and size is the number of elements in the array.
Some important points showing how pointers are related to the array are:
Array items can be accessed via pointers.
The addresses of the array's elements can be stored as pointers.
Pointers to arrays can be supplied as parameters to the functions.
The array's first element is the same as the pointer to the array's name.
Now let's take a look at an example that represents the relationship between pointers and arrays.
Code:
#include <iostream>
int main() {
int numbers[5] = {10, 20, 30, 40, 50};
return 0;
}
Output:
First element: 10
Second element: 20
Element 1: 10
Element 2: 20
Element 3: 35
Element 4: 40
Element 5: 50
Explanation:
1. We declare an integer array numbers with 5 elements and initialize it with values 10, 20, 30, 40, and 50.
2. Next, we declare a pointer to an integer ptrToArray and assign it the memory address of the first element of the numbers array.
Since numbers is the name of the array, it also represents the address of its first element (i.e., numbers[0]).
3. We can access the elements of the array using the pointer ptrToArray.
4. In C++, to access the value at the memory address pointed to by a pointer, we use the dereference operator
(*). So, *ptrToArray gives us the value of the first element (number[0]), and *ptrToArray + 1 gives us the value of the second
element, i.e., number[1], and so on.
5. We can also modify elements of the array using pointers. For example, we set the value of the third element number[2] to 35
using *ptrToArray + 2.
6. Finally, we loop through the array using a for loop and a pointer. We also use the std::cout command inside the loop to print the
values of all elements.
Example:
void function() {
int stackArray[5] = {1, 2, 3, 4, 5}; // Stack allocation
// Use the array...
} // Automatically deallocated when the function exits
In the above code snippet, an array stackArray is declared within a function, automatically allocated on the stack, and automatically
deallocated when the function exits.
Heap memory in C++ allows dynamic memory allocation at runtime. It provides more flexibility, especially for larger arrays or when the
size is not known at compile time. To allocate an array on the heap, we use the new operator, and to deallocate it, we use delete[]. This
approach is crucial when dealing with large data sets, as the heap provides a much larger memory pool than the stack.
Additionally, implementing data structures like binary heaps using arrays involves storing elements in a way that maintains heap properties
(e.g., min-heap or max-heap). A binary heap is a binary tree with the following properties:
1. It is a complete binary tree, meaning all levels are filled except possibly the last level, which is
filled from left to right.
2. For a min-heap, the value of each node is smaller than or equal to the values of its children.
3. For a max-heap, the value of each node is greater than or equal to the values of its children.
Example:
Conclusion
An array in C++ programming is an essential data structure, offering a convenient way to store and manipulate collections of data. By
understanding how to use, declare, initialize, and access array elements, you can effectively work with arrays in your programs. However,
it's essential to handle arrays with care, ensuring proper bounds checking to prevent errors and crashes in your programs. With arrays, you
have a powerful tool at your disposal for organizing and processing data efficiently in C++. As you become more proficient with C++, you'll
find that arrays are the building blocks for more advanced data structures and algorithms, making them a fundamental concept to master on
your journey as a C++ developer.
28 mins read
Data structures, a fundamental concept in computer science, are essential for organizing and managing data efficiently. They help optimise
data access and manipulation, making them crucial for effective algorithm implementation. In this article, we will explore what is linear data
structure, its types, some fundamental operations, and more.
Linear data structures are essential in as they provide a simple and efficient way to store and
access data.
They are used in various algorithms and applications, ranging from simple data storage to complex
problem-solving techniques.
Examples of linear data structures include arrays, linked lists, stacks, and queues. Each of these
structures has its unique properties and use cases, but they all share the common trait of linear
organization.
In contrast, nonlinear data structures are those that do not arrange data sequentially but rather in a
hierarchical or interconnected manner. Some examples are trees (binary trees, etc.) and graphs.
All in all, linear data structures help arrange data in a sequential manner, making it easier to traverse and manage. Understanding linear data
structures is fundamental as they form the building blocks for more complex structures and algorithms.
Sequential Arrangement: Elements are arranged in a linear sequence, with each element
connected to its previous and next element.
Single-Level Data Storage: The data elements are stored at a single level, unlike hierarchical
structures such as trees.
Easy & Efficient Traversal: Traversing through elements is straightforward, typically requiring a
single loop or iteration.
Efficient Memory Usage: Linear data structures are memory-efficient for storage, often requiring
contiguous memory locations.
Insertion and Deletion Operations: Operations such as insertion and deletion can be performed
with relative ease, especially in structures like linked lists.
Fixed Size or Dynamic: Some linear structures, like arrays, have a fixed size, while others, like
linked lists, can dynamically grow or shrink.
These characteristics make linear data structures fundamental for various applications, providing a simple yet powerful means to manage
and manipulate data.
Arrays: A collection of elements stored in contiguous/ adjacent memory locations, where elements
are identified by index or key. Arrays have a fixed size and allow for efficient random access.
Linked Lists: A sequence of elements called nodes, i.e., a collection of nodes. Each current node
contains a data element and a reference (i.e., a link or connection between nodes) to the next node
in the sequence. Linked lists can grow or shrink dynamically.
Stacks: A linear data structure that follows the Last In, First Out (LIFO) principle, where the most
recently added element is the first to be removed. Stacks are commonly used in recursion and
backtracking algorithms.
Queues: A linear data structure that follows the First In, First Out (FIFO) principle, where the
earliest added element is the first to be removed. Queues are useful for managing tasks in
sequential order.
These linear data structures are fundamental to many programming algorithms and applications. Each type of structure serves specific
purposes in data management and processing. We will discuss each of these in detail in the sections ahead.
They are created using the square bracket notation in most languages, where the brackets contain
the dimensions or the elements.
Each element in the array can be accessed using an index, making it easy to retrieve and
manipulate data.
Arrays are widely used due to their simplicity and efficiency in accessing elements.
1. One-Dimensional Array: A single row of elements, also known as a linear array. For example-
int[][][] threeD = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}};
Operations On Array Linear Data Structures
The most common operation performed on an array is the simple traversal of an entire array to access, update or modify its elements. Other
operations include:
1. Accessing an Element: Accessing an element in an array retrieves the value stored at a specific index. Loops allow for efficient traversal
and efficient indexing of elements, which is why the complexity for element access is usually O(1), i.e., constant time complexity.
2. Inserting an Element: This involves adding an element/ a new value at a specified index or at the end of the array. The complexity for
element insertion varies as follows-
At the End (Appending): O(1) on average, O(n) in the worst case. Appending to the end requires
checking and potentially resizing the array.
At a Specific Index (Insertion): O(n). Insertion at arbitrary positions requires shifting subsequent
elements.
3. Deleting/ Removal Of Element: Deleting an element removes a value from a specified index or from the end of the array.
The complexity of this operation depends on where the element is being removed from. That is-
From the End: O(1) on average, O(n) in worst case. Deleting the last element is straightforward.
From a Specific Index: O(n). Deleting an element involves shifting subsequent elements to close
the gap.
4. Searching for an Element (Linear Search): Searching finds the index of a specified value in the array, if present. The complexity for
the linear search operation generally remains O(n) - Linear time. In the worst case, the search may need to examine every element in the
array.
5. Sorting the Array (Using Efficient Sorting Algorithms): Sorting arranges the elements in either ascending or descending order. The
best and worst case complexity for this operation is-
Best Sorting Algorithms (e.g., QuickSort, MergeSort): O(n log n). Efficient sorting algorithms
minimize comparisons and swaps.
Worst Case (e.g., BubbleSort, SelectionSort): O(n^2). Less efficient algorithms perform poorly
on large datasets.
6. Updating an Element: Updating modifies the value of an existing element at a specified index. The complexity for this operation is O(1)
- Constant time. Updating an element by index is a direct operation.
Advantages Disadvantages
Unlike arrays, linked lists do not store elements in contiguous memory locations. Instead, each
element points to the next element, forming a chain-like structure of connected memory locations.
This dynamic arrangement allows for efficient insertion and deletion operations.
Linked lists are particularly useful when the number of elements is unknown or when frequent insertions and deletions are required.
Dynamic Size: Linked lists can grow or shrink in size dynamically, making them more flexible
than arrays.
Non-Contiguous Memory Allocation: Nodes are not stored in contiguous memory locations,
allowing for efficient usage and allocation of memory.
Node-Based Structure: Each node contains a data element and a reference (or pointer) to the next
node.
Ease of Insertion/Deletion: Adding or removing nodes is relatively simple and efficient,
especially compared to arrays.
Types of Linked List Linear Data Structure
Singly Linked List: Each node contains a single reference to the next node in the sequence.
Example:
Node1 -> Node2 -> Node3 -> NULL
Doubly Linked List: Each node contains two references: one to the next node and one to the
previous node, allowing for bidirectional traversal. Example:
NULL <- Node1 <-> Node2 <-> Node3 -> NULL
Circular Linked List: The last node of the list points back to the first node, forming a circular
structure. Example:
Node1 -> Node2 -> Node3 -> Node1 (and so on)
Common Operations On Linked List Linear Data Structure
Some common operations on the linked list linear data structure types include:
1. Accessing an Element: Accessing an element in a linked list involves traversing from the head (or tail in doubly linked lists) to the
desired position. The complexity for common traversal methods on linked lists is O(n) - Linear time. Accessing an element requires
traversing through the list until reaching the desired node.
2. Inserting an Element: Inserting an element in a linked list typically involves creating a new node and adjusting pointers to link it
correctly within the list. The complexity for element insertion in linked list linear data structure varies as follows-
At the Beginning: O(1) - Constant time complexity. Insertion at the head of the linked list
involves updating the head pointer.
At the End: O(n) in the worst case. Insertion at the end requires traversing the entire list to reach
the last node.
At a Specific Position: O(n). Insertion at an arbitrary position involves traversing to the insertion
point and adjusting pointers.
3. Deleting an Element: Deleting an element from a linked list involves adjusting pointers to bypass the node to be deleted and freeing its
memory. The complexity for element deletion is-
From the Beginning: O(1) - Constant time. Deleting the head node involves updating the head
pointer.
From the End: O(n) in the worst case. Deleting the last node requires traversing the entire list to
reach the second last node.
From a Specific Position: O(n). Deleting a node at an arbitrary position involves traversing to the
deletion point and adjusting pointers.
4. Searching for an Element: Searching for an element in a linked list involves traversing the list from the head (or tail) and comparing
each node's value until finding the desired element. O(n) - Linear time. In the worst case, searching may require traversing through all nodes
in the list.
5. Sorting the Linked List: Sorting a linked list rearranges its elements in ascending or descending order based on their values.
Efficient Sorting Algorithms (e.g., MergeSort): O(n log n). Efficient algorithms minimize
comparisons and swaps.
Less Efficient Algorithms (e.g., BubbleSort): O(n^2). Less efficient algorithms may perform
poorly on large lists.
6. Updating an Element: Updating an element in a linked list involves traversing to the node to be updated and modifying its value. The
complexity for element updations is O(n), i.e., linear time, as updating requires traversing through the list until reaching the desired node.
7. Merging Linked Lists: Merging two linked lists combines their elements into a single linked list while preserving their order. Since
merging requires iterating through both lists to link them into a single list, the complexity is- O(n), i.e., linear time.
8. Reversing the Linked List: Reversing a linked list changes the order of its elements by reversing the pointers between nodes. Reversing
involves iterating through the list once and adjusting pointers to reverse the order of nodes, so the complexity is linear time, i.e., O(n).
Advantages Disadvantages
This means that the last element added to the stack is the first one to be removed.
Stacks are used in a variety of applications, including function call management in programming,
expression evaluation, and syntax parsing.
They can be implemented using arrays or linked lists, providing a simple yet powerful way to manage data.
LIFO Principle: The Last In, First Out principle ensures that the most recently added element is
the first to be removed.
Dynamic Size: Stacks can dynamically grow and shrink as elements are added or removed.
Single Entry/Exit Point: All operations (push, pop, peek) are performed from one end of the
stack, known as the top.
Efficient Operations: Insertion (push) and deletion (pop) operations are performed in constant
time, O(1).
Types Of Stack Linear Data Structures
Static Stack: Implemented using arrays with a fixed size. The size of the stack is determined at
compile time. For example:
int stack[10];
Dynamic Stack: Implemented using linked lists, allowing the stack to grow and shrink
dynamically without a predefined limit. For example, a linked list can be used to manage stack
nodes.
Double-Ended Stack (Deque): Allows insertion and deletion of elements from both ends, although
this is less common for typical stack use cases. For example (in C++):
std::deque
Common Operations On Stack Linear Data Structures
Here are some of the most popular data structure operations for stacks:
Push(): This operation refers to pushing (or adding) an element onto the stack, specifically pushing a new/ additional element on
top of the stack. This operation usually has a constant time complexity of O(1), as pushing an element onto a stack is a direct
operation. Example:
stack.push(10);
Pop(): The pop operation refers to popping (or removing) an element from the stack, specifically removing the topmost element.
It also has a constant time complexity, i.e., O(1), as popping an element from a stack is also a direct operation. Example:
Searching for an Element: Searching for an element in a stack involves iterating through the
stack elements to find a specific value. This operation has a linear time complexity, i.e., O(n), as
in the worst case, searching may require traversing through all elements in the stack.
Sorting the Stack: Sorting a stack arranges its elements in either ascending or descending order.
The complexity of this operation is-
o Efficient Sorting Algorithms (e.g., MergeSort for Stacks): O(n log n). Efficient
algorithms minimize comparisons and swaps.
o Less Efficient Algorithms (e.g., BubbleSort for Stacks): O(n^2). Less efficient
algorithms may perform poorly on large stacks.
Reversing the Stack: Reversing a stack changes the order of its elements, typically achieved using an auxiliary stack. This
operation has linear time complexity O(n), as reversing involves pushing elements into a new stack and popping them back to
reverse the order.
These operations exemplify the stack's Last In, First Out (LIFO) principle, where elements are added and removed from the same end.
Understanding their complexities is crucial for designing efficient algorithms that utilize stack data structures effectively in various
applications.
Advantages Disadvantages
Simplicity: Stacks are easy to understand and Limited Access: Only the top element can be accessed
implement. directly, making it inefficient to access elements deep
Efficient Memory Use: Only the necessary amount of within the stack.
memory is used, as stacks can dynamically grow and Memory Overhead: In linked list-based implementations,
shrink. additional memory is required for pointers.
Controlled Access: Access to elements is controlled, Fixed Size in Array Implementation: In array-based
reducing the chances of data corruption. stacks, the size is fixed, limiting flexibility.
FIFO Principle: The First In, First Out (FIFO) principle ensures that the first element added is the
first to be removed.
Two Ends: Operations are performed at two different ends; insertion (enqueue) at the rear and
deletion (dequeue) at the front.
Dynamic Size: Queues can dynamically grow and shrink as elements are added or removed.
Ordered Structure: Elements maintain the order in which they are added, ensuring predictable
access patterns.
Types Of Queue Linear Data Structure
Simple Queue: Also known as a linear queue, where elements are added at the rear and removed
from the front. For example:
int queue[SIZE];
Circular Queue: A queue that treats the array as circular, connecting the end back to the front and
making efficient use of space. Example:
if (rear == SIZE - 1) rear = 0; else rear++;
Priority Queue: Elements are added with priority, and the element with the highest priority is
removed first, regardless of the order of insertion. Example:
enqueue(element, priority);
Deque (Double-Ended Queue): A queue where elements can be added or removed from both the
front and the rear. Example:
deque.push_front(element); deque.push_back(element);
Operations On Queue Linear Data Structure
Here is a list of the operations that are most commonly performed on the queue linear data structure type:
Enqueue: It entails adding a new element to the rear of the queue. The complexity for element insertion in the queue is O(1), i.e.,
constant time as an enqueue operation directly adds an element to the rear of the queue. Example:
queue.enqueue(element);
Dequeue: In keeping with the FIFO principle, this operation entails removing the front element from the queue. It has constant
time complexity, i.e., O(1), since the dequeue operation directly removes the front element from the queue. Example:
queue.dequeue();
Front: This operation returns the front element without removing it from the queue. It has a complexity of O(1), i.e., constant
time, since the operation retrieves the front element without altering the queue. Example:
element = queue.front();
Rear: Returns the rear element without removing it from the queue and has a constant time complexity, i.e., O(1). Example:
element = queue.rear();
IsEmpty: Checks if the queue is empty using the isEmpty() method. It returns a boolean value and has a constant time
complexity of O(1). Example:
Advantages Disadvantages
1. Traversal:
This is the process of accessing and processing each element of the data structure sequentially. This operation is essential for performing
tasks like searching, updating, or displaying the elements.
2. Insertion:
This process refers to adding a new element to the linear data structure at a specified position. This involves shifting existing elements to
make space for the new element. The syntax for insertion varies depending on the data structure and language, typically using an indexing
operator [] for arrays.
3. Deletion of Elements:
Deletion involves removing an element from the data structure at a specified position. This requires shifting the subsequent elements to fill
the gap left by the removed element.
4. Searching:
Searching involves finding the position of an element within the data structure. This can be done using linear or binary search algorithms,
depending on whether the data is sorted.
5. Sorting:
Sorting refers to the process of arranging the data structure elements in a specific order, either ascending or descending. Various algorithms
like bubble sort, quicksort, mergesort, insertion sort, etc., can be used, each with different syntax and performance characteristics.
6. Merging:
Merging combines two data structures into one, preserving the order of elements. This operation is often used in conjunction with sorting to
merge sorted lists or arrays efficiently.
7. Reversing:
Reversing changes the order of elements in the data structure, making the last element the first and vice versa. This operation is useful in
various algorithms and applications where the order of data needs to be inverted.
These operations are fundamental for manipulating linear data structures efficiently, providing a solid foundation for more complex
algorithms and data handling techniques.
1. Simplicity
The linear arrangement in linear data structures is straightforward, easy to implement, understand and use. It is also easier to manipulate or
modify them (in comparison to non-linear structures).
2. Efficient Memory Utilization
Linear data structures efficiently utilize memory space, as elements are stored sequentially. For example, in arrays, memory allocation is
contiguous, which minimizes the overhead of memory management. Also in linked lists allow dynamic memory allocation and can grow or
shrink as needed, thus optimizing memory usage.
3. Ease of Traversal
Linear data structures facilitate easy and straightforward traversal of elements, making operations like searching, sorting, and updating more
manageable. Since elements are arranged in a sequence, moving from one element to the next is simple and direct. This ordered nature
supports the efficient execution of various algorithms that require element access and manipulation.
4. Sequential Access
Linear data structures provide efficient sequential access to elements, ideal for algorithms requiring ordered data processing. Operations that
require accessing elements in a specific order benefit from the sequential nature of linear data structures. This is particularly useful in
applications like streaming data processing, where data is processed in the order it is received.
5. Predictable Performance
The time complexity of basic operations (insertion, deletion, access) in linear data structures is predictable and often constant or linear,
making performance analysis straightforward. This further simplifies the analysis and optimization of algorithms. For example, accessing an
element in an array has a constant time complexity (O(1)), while traversing a linked list has a linear time complexity (O(n)).
6. Versatility
Linear data structures can implement more complex data structures and algorithms, serving as building blocks for stacks, queues, and hash
tables. Arrays and linked lists can be adapted and extended to create more sophisticated data structures. For example, a stack can be
implemented using an array or a linked list, and queues can be efficiently managed using linked lists.
9. Ease of Implementation
Many programming languages offer built-in support for linear data structures, simplifying their implementation and use. Languages like C,
C++, Java, and Python provide built-in array and linked list structures, along with libraries and functions to manipulate them. This built-in
support makes it easier for developers to use these data structures without implementing them from scratch.
11. Flexibility
Linear data structures can be adapted to various use cases, providing flexibility in application design. For instance, arrays can be used for
static data storage with fixed size, while linked lists offer dynamic size adjustments. This flexibility allows developers to choose the most
appropriate linear data structure based on the specific needs of their application.
These advantages highlight why linear data structures are widely used and remain fundamental in both educational contexts and real-world
applications.
Non-linear data structures enable more complex data relationships and structures, which are
essential for representing hierarchical and networked data.
These structures include trees and graphs, commonly used in various applications like database
management, networking, and artificial intelligence.
Non-linear data structures are advantageous when dealing with data that requires flexible and
intricate connections.
They help in optimizing various operations such as searching, inserting, and deleting data, making
them crucial for complex problem-solving scenarios.
Difference Between Linear & Non-Linear Data Structures
Understanding the differences between linear and non-linear data structures is crucial for selecting the appropriate structure for your
application. The table below highlights the key differences between them:
Memory Generally uses contiguous memory Can use both contiguous and non-contiguous
Utilization locations memory
Typically, faster access times due to the Access time can vary depending on structure
Access Time
sequential nature and hierarchy
Suitable for simple, linear data Ideal for complex data relationships and
Use Cases
processing hierarchical data
Less flexible, as structure size may be More flexible, can grow or shrink
Flexibility
fixed (like arrays) dynamically (like trees)
For more information, read- What is The Difference Between Linear And Non Linear Data Structure?
Who Uses Linear Data Structures?
Linear data structures are utilized across various fields and applications due to their simplicity, efficiency, and versatility. Here are some key
users and applications of linear data structures:
1. Software Development: Linear data structures are fundamental in software development for
organizing and managing data efficiently. They are used in applications ranging from simple data
storage to complex algorithms and data processing tasks.
2. Database Management Systems: Linear data structures play a key role in managing and
organizing large data sets. For example, arrays and linked lists are employed in database systems
to store and retrieve records sequentially.
3. Operating Systems: Linear data structures are essential components of operating systems for
managing memory allocation, process scheduling, and file systems. For example, arrays and linked
lists are often used to store and manage system data structures.
4. Web Development: Linear data structures are used in web development frameworks and
applications for managing user sessions, storing user data, and handling server-side operations
efficiently.
5. Networking: Linear data structures like queues are employed in networking protocols for
managing packet transmission and ensuring data flow control.
6. Artificial Intelligence and Machine Learning : Linear data structures play a significant role in AI
and ML algorithms for storing and processing large datasets. They are used in data preprocessing,
feature extraction, and model training phases.
7. Education and Academics: Linear data structures are extensively taught and used in educational
settings to teach fundamental programming concepts, algorithms, and data handling techniques.
8. Financial Applications: Linear data structures are used in financial applications for managing
transactions, storing account information, and performing calculations efficiently.
9. Embedded Systems: Linear data structures are employed in embedded systems programming for
efficient memory management, data handling, and real-time processing tasks.
10. Gaming and Graphics: Linear data structures are used in game development and graphics
programming to manage game states, render objects, and handle user inputs.
As is evident, linear data structures are widely used across various domains and industries, given their versatility and crucial role in modern
computing.
What is The Difference Between Linear And Non Linear Data Structure?
Linear and non-linear data structures are two main categories of data structures. Let's understand these in
detail with examples.
8 mins read
A data structure refers to organizing and storing data in a computer's memory in a way that enables efficient access, manipulation, and
retrieval of the data. Data structures are fundamental concepts in computer science and are extensively used in programming and software
development to solve various computational problems.
There are two categories of data structure - linear data structure and non-linear data structure. In real life, linear data structure is used to
develop software, and non-linear data structure is used in image processing and artificial intelligence.
In this article, we will understand the difference between linear and non-linear data structures.
Looking for software engineering jobs? Here are some that you shouldn't miss out on!
What is a linear data structure?
A linear data structure is a type of data structure that stores the data linearly or sequentially. In the linear data structure, data is arranged in
such a way that one element is adjacent to its previous and the next element. It includes the data at a single level such that we can traverse all
data into a single run.
The implementation of the linear data structure is always easy as it stores the data linearly. The common examples of linear data types
are Stack, Queue, Array, LinkedList, and Hashmap, etc.
Brush up your knowledge! Browse through the important Data Structures interview questions
Here, we have briefly explained every linear data type below:
1. Stack
Users can push/pop data from a single end of the stack. Users can insert the data into the stack via push operation and remove data from the
stack via pop operation. The stack follows the rule of LIFO (last in first out). Users can access all the stack data from the top of the stack in
a linear manner.
In real-life problems, the stack data structure is used in many applications. For example, the All web browser uses the stack to store the
backward/forward operations.
2. Queue
Queue data structure stores the data in a linear sequence. Queue data structure follows the FIFO rule, which means first-in-first-out. It is
similar to the stack data structure, but it has two ends. In the queue, we can perform insertion operations from the rear using the Enqueue
method and deletion operations from the front using the deque method.
3. Array
The array is the most used Linear data type. The array stores the objects of the same data type in a linear fashion. Users can use an array to
construct all linear or non-linear data structures. For example, Inside the car management software to store the car names array of the strings
is useful.
We can access the element of the array by the index of elements. In an array, the index always starts at 0. To prevent memory wastage, users
should create an array of dynamic sizes.
4. LinkedList
LinkedList data structure stores the data in the form of a node. Every linked list node contains the element value and address pointer. The
address pointer of the LinkedList consists of the address of the next node. It can store unique or duplicate elements.
It is one type of Non-primitive data structure. In non-linear data structures, data is not stored linear manner. There are multiple levels of
nonlinear data structures. It is also called a multilevel data structure. It is important to note here that we can't implement non-linear data
structures as easily as linear data structures.
1. Tree
A tree data structure is an example of a nonlinear data structure. It is a collection of nodes where each node consists of the data element.
Every tree structure contains a single root node.
In the tree, every child node is connected to the parent node. Every parent and child node has a parent-child node relationship. In the tree,
Nodes remaining at the last level are called leaf nodes, and other nodes are called internal nodes.
Types of trees:
1. Binary tree
2. Binary search tree
3. AVL tree
4. Red-black tree
The Heap data structure is also a non-linear tree-based data structure, and it uses the complete binary tree to store the data.
2. Graph
The graph contains vertices and edges. Every vertex of the graph stores the data element. Edges are used to build a vertices relationship.
Social networks are real-life examples of graph data structure.
Here, we can consider the user as a vertex, and the relation between him/her with other users is called an edge. There is no limitation for
building the relationship between any two vertexes of the graph like a tree. We can implement the graph data structure using the array or
LinkedList.
Simple Graph
Un-directed Graph
Directed Graph
Weighted Graph
3. Hashmap
Hashmaps store data as key-value pairs. It can be either linear or non-linear data structure. We can use the hashmap to map the value with its
keys and search value efficiently from it.
Time complexity increases if we need Time complexity doesn't change much if we need to traverse
to traverse through a large dataset. through the large input size.
It is used to build an operating system All Social networks, telephone networks, Artificial intelligence,
and compiler design. and image processing are using non-linear data structures.
21 mins read
Sorting is a fundamental concept in data structures and algorithms, where the elements of a collection are arranged in a specific order, such
as ascending or descending. This process improves data organization, making it easier to search, analyze, and process. From simple methods
like Bubble Sort to more advanced techniques like Quick Sort and Merge Sort, sorting algorithms play a vital role in optimizing
performance in various applications. In this article, we’ll explore key sorting techniques, their working mechanisms, and use cases to help
you understand their importance and implementation.
A divide-and-conquer algorithm that splits the array into halves, O(n log n) / O(n log n) / O(n
Merge Sort O(n)
recursively sorts each half, and merges the sorted halves into one. log n)
Picks a pivot, partitions the array around it, and recursively sorts O(n log n) / O(n log n) / O(log n) (due to
Quick Sort
the partitions. O(n²) recursion)
Based on a binary heap structure; repeatedly extracts the largest O(n log n) / O(n log n) / O(n
Heap Sort O(1)
(or smallest) element to build the sorted array. log n)
Bucket Divides the array into buckets, sorts each bucket, and combines
O(n + k) / O(n + k) / O(n²) O(n + k)
Sort them.
An optimized version of insertion sort that sorts elements far O(n log n) / O(n log² n) /
Shell Sort O(1)
apart to reduce comparisons, progressively narrowing the gap. O(n²)
How It Works:
#include <iostream>
using namespace std;
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr) / sizeof(arr[0]);
bubbleSort(arr, n);
return 0;
}
Output:
Unsorted array: 64 34 25 12 22 11 90
Sorted array: 11 12 22 25 34 64 90
Explanation:
1. bubbleSort Function: This function sorts the array using Bubble Sort. It has two nested loops:
1. The outer loop runs n-1 times (where n is the number of elements).
2. T h e i n n e r l o o p c o m p a r e s a d j a c e n t e l e m e n t s a n d s w a p s t h e m i f n e c e s s a r y .
printArray Function: This function prints the elements of the array.
main() Function: The array is defined, and then both the unsorted and sorted arrays are printed
before and after calling the bubbleSort function.
What Is Selection Sort?
Selection Sort is a simple sorting algorithm that divides the list into two parts: the sorted part and the unsorted part. It repeatedly selects the
smallest (or largest) element from the unsorted part and moves it to the sorted part.
How It Works:
1. Find the smallest (or largest) element in the unsorted part of the array.
2. Swap it with the first element of the unsorted part.
3. Repeat the process for the remaining unsorted part until the entire array is sorted.
Selection Sort Syntax (In C++)
void selectionSort(int arr[], int n) {
for (int i = 0; i < n-1; i++) {
int minIdx = i; // Index of the minimum element
for (int j = i+1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j; // Update index of the smallest element
}
}
// Swap the found minimum element with the first element of the unsorted part
int temp = arr[minIdx];
arr[minIdx] = arr[i];
arr[i] = temp;
}
}
Code Example:
#include <iostream>
using namespace std;
// Swap the found minimum element with the first element of the unsorted part
int temp = arr[minIdx];
arr[minIdx] = arr[i];
arr[i] = temp;
}
}
int main() {
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr) / sizeof(arr[0]);
selectionSort(arr, n);
return 0;
}
Output:
Unsorted array: 64 25 12 22 11
Sorted array: 11 12 22 25 64
Explanation:
1. The outer loop runs n-1 times (where n is the number of elements).
2. For each iteration, the smallest element in the unsorted part is found using the inner
loop.
3. T h e s m a l l e s t e l e m e n t i s t h e n s w a p p e d w i t h t h e f i r s t e l e m e n t o f t h e u n s o r t e d p a r t .
printArray Function: This function prints the elements of the array.
main() Function: The array is defined, and both the unsorted and sorted arrays are printed before
and after calling the selectionSort function.
What Is Insertion Sort?
Insertion Sort is a simple and intuitive sorting algorithm that builds the final sorted array one element at a time. It works similarly to how
we sort playing cards in our hands:
1. Start with the second element, compare it with elements before it, and insert it in the correct
position.
2. Repeat for all elements, ensuring that the array is sorted after each insertion.
How It Works:
#include <iostream>
using namespace std;
// Shift elements of the sorted part to make space for the key
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
int main() {
int arr[] = {12, 11, 13, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
insertionSort(arr, n);
return 0;
}
Output:
Unsorted array: 12 11 13 5 6
Sorted array: 5 6 11 12 13
Explanation:
1. The loop starts with the second element (i = 1) because a single element is already sorted.
2. For each element, it finds its correct position in the sorted part of the array.
3. Elements larger than the current key are shifted to the right to make space.
4. T h e k e y i s t h e n i n s e r t e d i n t o i t s c o r r e c t p o s i t i o n .
printArray Function: This function prints the elements of the array.
main() Function: The array is defined, and the unsorted and sorted arrays are printed before and
after calling the insertionSort function.
What Is Merge Sort?
Merge Sort is a divide-and-conquer sorting algorithm that recursively divides the array into two halves until each subarray contains only
one element, then merges those subarrays to produce a sorted array. It is one of the most efficient sorting algorithms for large datasets.
How It Works:
1. Divide: Split the array into two halves recursively until each subarray has only one element.
2. Conquer: Merge the subarrays back together in sorted order.
3. Combine: Repeat merging until the entire array is sorted.
Merge Sort involves two main functions:
#include <iostream>
using namespace std;
int L[n1], R[n2]; // Temporary arrays for left and right subarrays
int i = 0, j = 0, k = left;
int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int n = sizeof(arr) / sizeof(arr[0]);
mergeSort(arr, 0, n - 1);
return 0;
}
Output:
Unsorted array: 12 11 13 5 6 7
Sorted array: 5 6 7 11 12 13
Explanation:
1. merge Function:
How It Works:
1. Choose a Pivot: Select an element as the pivot. Common choices include the first element, the last
element, the middle element, or a random element.
2. Partitioning: Reorder the array so that:
#include <iostream>
using namespace std;
int main() {
int arr[] = {10, 7, 8, 9, 1, 5};
int n = sizeof(arr) / sizeof(arr[0]);
quickSort(arr, 0, n - 1);
return 0;
}
Output:
Unsorted array: 10 7 8 9 1 5
Sorted array: 1 5 7 8 9 10
Explanation:
1. partition Function:
1. Build a Max Heap: Construct a binary heap from the input array, ensuring the largest element is at
the root.
2. Extract Elements: Repeatedly remove the root (maximum element) of the heap, place it at the end
of the array, and reduce the heap size.
3. Heapify: Restore the heap property after each extraction to maintain the max heap structure.
Heap Sort involves two key operations:
#include <iostream>
using namespace std;
int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int n = sizeof(arr) / sizeof(arr[0]);
heapSort(arr, n);
return 0;
}
Output:
Unsorted array: 12 11 13 5 6 7
Sorted array: 5 6 7 11 12 13
Explanation:
1. heapify Function:
1. Ensures the max heap property (parent is larger than its children).
2. R e c u r s i v e l y a d j u s t s t h e h e a p i f t h e l a r g e s t e l e m e n t i s n o t a t t h e r o o t .
heapSort Function:
How It Works:
1. Identify the Maximum Element: Determine the maximum number in the array to find the number
of digits.
2. Sort by Each Digit: Starting from the least significant digit (unit place), sort the numbers using
counting sort.
3.
Repeat for Each Place Value: Move to the next significant digit and repeat until all place values
are processed.
Syntax For Radix Sort (In C++)
Radix Sort relies on counting sort to sort numbers at each digit position.
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int arr[] = {170, 45, 75, 90, 802, 24, 2, 66};
int n = sizeof(arr) / sizeof(arr[0]);
radixSort(arr, n);
return 0;
}
Output:
1. countingSort Function:
1. Sorts the array based on the current digit (unit, tens, hundreds, etc.).
2. U s e s a c o u n t i n g a r r a y t o t r a c k o c c u r r e n c e s o f d i g i t s .
radixSort Function:
How It Works:
1. Divide the Input into Buckets: The input array is divided into several buckets based on a pre-
defined range.
2. Sort Each Bucket: Each bucket is sorted individually using another sorting algorithm
(commonly Insertion Sort).
3.
Concatenate the Sorted Buckets: After sorting each bucket, the contents of all buckets are
combined to form the final sorted array.
Code Example:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int arr[] = {0.42, 0.32, 0.23, 0.52, 0.67, 0.31};
int n = sizeof(arr) / sizeof(arr[0]);
bucketSort(arr, n);
return 0;
}
Output:
1. insertionSort Function: Sorts each bucket using Insertion Sort, which is efficient for small
datasets.
2. bucketSort Function:
1. Divides the array into buckets and places elements into the appropriate bucket based on
their value.
2. S o r t s e a c h b u c k e t a n d c o m b i n e s t h e m b a c k i n t o t h e o r i g i n a l a r r a y .
printArray Function: Prints the elements of the array before and after sorting.
What Is Counting Sort?
Counting Sort is a non-comparative sorting algorithm that counts the number of occurrences of each element in the array and uses that
information to place elements in their correct sorted position. It is best suited for sorting integers or objects that can be mapped to non-
negative integer values.
How It Works:
1. Count Occurrences: Count the frequency of each distinct element in the input array.
2. Compute Cumulative Count: Use the frequency counts to compute the cumulative sum, which will
indicate the position of each element in the sorted output.
3. Place Elements: Place each element from the input array into the correct position in the output
array, using the cumulative count.
4. Rebuild Sorted Array: After placing all elements in their correct position, the output array is the
sorted array.
Counting Sort requires:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int arr[] = {4, 2, 2, 8, 3, 3, 1};
int n = sizeof(arr) / sizeof(arr[0]);
return 0;
}
Output:
Unsorted array: 4 2 2 8 3 3 1
Sorted array: 1 2 2 3 3 4 8
Explanation:
1. countingSort Function:
1. First, it determines the maximum element in the array to define the size of the counting
array.
2. It counts the occurrences of each element in the arr[].
3. The count[] array is then updated by adding the previous counts to make it cumulative.
4. The elements are placed into their correct position in the output[] array based on the
cumulative count.
5. F i n a l l y , t h e s o r t e d e l e m e n t s a r e c o p i e d b a c k i n t o t h e o r i g i n a l a r r a y .
printArray Function: Prints the elements of the array before and after sorting.
What Is Shell Sort?
Shell Sort is an optimization of Insertion Sort that allows the exchange of items that are far apart. The basic idea is to arrange the list of
elements so that, starting anywhere, taking every kthk^{th}kth element produces a sorted list. By progressively reducing the gap between
elements, Shell Sort efficiently reduces the number of inversions (out-of-order pairs), which leads to improved sorting performance
compared to regular Insertion Sort.
How It Works:
1. Gap Sequence: Instead of comparing adjacent elements (like Insertion Sort), Shell Sort compares
elements that are far apart. It starts by sorting pairs of elements far apart, then progressively
reduces the gap between elements being compared.
2. Insertion Sort: After comparing and potentially swapping far-apart elements, it sorts the sublists
using Insertion Sort.
3.Reducing the Gap: The gap between elements is reduced at each step, and this process is repeated
until the gap is 1, at which point the algorithm behaves like Insertion Sort.
Code Example:
#include <iostream>
#include <vector>
using namespace std;
int main() {
int arr[] = {5, 2, 9, 1, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
shellSort(arr, n);
return 0;
}
Output:
Unsorted array: 5 2 9 1 5 6
Sorted array: 1 2 5 5 6 9
Explanation:
1. shellSort Function:
1. Starts with a gap of n/2, where n is the number of elements in the array.
2. In each iteration, the gap is halved until it becomes 1.
3.For each gap, the array is sorted using a variation of Insertion Sort, where instead of
comparing adjacent elements, it compares elements that are gap positions apart.
printArray Function: Prints the elements of the array before and after sorting.
Choosing The Right Sorting Algorithm
When selecting a sorting algorithm for a particular problem, several factors should be taken into account to ensure the best performance. The
decision depends on the input data characteristics (like size, order, and range) and the specific requirements such as time and space
efficiency. Here's a guide to help you choose the right sorting algorithm based on these factors.
1. Small Data: For small arrays (under 1000 elements), simpler algorithms like Insertion
Sort or Selection Sort may perform well despite their higher time complexities, as they
have low overhead.
2.
Large Data: For larger datasets, algorithms with better time complexities, such as Merge
Sort, Quick Sort, or Heap Sort, are preferred because of their faster performance for
large inputs.
Time Complexity:
1. Best Time Complexity: Algorithms with O(n logn) average or worst-case time
complexity, like Quick Sort, Merge Sort, and Heap Sort, are efficient for large datasets.
2.
Worst Time Complexity: For sorting where performance guarantees are critical, Merge
Sort is a better choice, as its worst-case time complexity is consistently O(n log n),
unlike Quick Sort which can degrade to O(n^2) in the worst case (with bad pivot
choices).
Space Complexity:
1. In-place Sorting: If memory usage is a concern (e.g., for large datasets), in-place sorting
algorithms like Quick Sort, Heap Sort, and Insertion Sort are preferred, as they use
O(1) extra space.
2. Extra Space Required: Algorithms like Merge Sort require O(n) extra space, which can
be a disadvantage when dealing with large datasets in memory-constrained environments.
Stability:
1. Stable Sorting Algorithms maintain the relative order of records with equal keys. Merge
Sort and Bubble Sort are stable, whereas Quick Sort and Heap Sort are not.
2.
If stability is important (e.g., sorting objects based on one attribute while maintaining the
relative order of objects with the same attribute), then Bubble Sort, Merge Sort,
or Insertion Sort are good choices.
Range of Input Elements:
1. Counting Sort, Radix Sort, and Bucket Sort can perform much faster than O(n log n)
algorithms when the range of input elements is small and can be mapped to integers.
2.
If the input range is large and non-integer, you may need to use comparison-based
algorithms like Quick Sort or Merge Sort.
Input Order:
1. Partially Sorted Data: Algorithms like Insertion Sort are particularly efficient when the
data is already partially sorted. In fact, Insertion Sort can run in O(n) time if the input is
already sorted or nearly sorted.
2. Randomized or Unsorted Data: Quick Sort, Merge Sort, and Heap Sort generally
perform well on random or unsorted data.
Applications Of Sorting
Sorting plays a crucial role in various fields of computer science and real-world applications. It is a fundamental operation that optimizes the
performance of other algorithms and systems. Here are some key applications of sorting:
1. Searching Algorithms
Binary Search: Sorting is essential for efficient searching algorithms like Binary Search. Binary
Search can only work on sorted arrays or lists, allowing it to find elements in O(log n) time, much
faster than linear search, which has a time complexity of O(n).
2. Data Visualization
Graphing and Charts: Sorting helps in organizing data for easier visualization, such as arranging
data points in ascending or descending order before plotting graphs or creating charts. This makes
patterns, trends, and outliers more noticeable.
3. Database Management
Indexing: Many database management systems use sorting to maintain indexes. Sorted data enables
faster retrieval, as the search algorithms can take advantage of the sorted structure to locate
records quickly.
Query Optimization: Sorting improves query performance by optimizing the execution of range
queries and join operations.
4. File Systems
Efficient Storage: In file systems, sorting is used to organize files in directories, ensuring that
file listings (e.g., names or sizes) are displayed in a sorted order, allowing for quick access.
Data Compression: Sorting is used in compression algorithms like Huffman coding, where
symbols are arranged in a frequency order to minimize the storage space required.
5. Social Media and Online Platforms
Ranking Algorithms: Sorting plays a key role in ranking systems on social media platforms, e-
commerce websites, and news feeds. It helps in organizing content based on relevance, popularity,
or user preferences, ensuring that users see the most important or trending content first.
6. Scheduling Algorithms
Task Scheduling: In operating systems, sorting helps in scheduling tasks. For example, Shortest
Job Next (SJN) scheduling algorithm sorts processes based on their burst time to optimize CPU
utilization.
Deadline-based Scheduling: Sorting helps to order tasks by their deadlines, allowing the system
to prioritize tasks efficiently.
7. Data Mining
Clustering and Classification: Sorting can help with data clustering algorithms by grouping
similar data points together. It is also used in decision tree algorithms and other classification
techniques to organize and rank data.
8. Merchandise Inventory Management
Stock Sorting: Sorting algorithms are used in inventory management systems to sort stock items
based on factors such as price, quantity, or item number, allowing efficient tracking and reporting.
9. Networking Protocols
Packet Routing: In networking, sorting helps in routing packets based on priority. For
example, Quality of Service (QoS) algorithms prioritize network traffic by sorting packets,
ensuring that high-priority packets are transmitted first.
10. E-commerce Systems
Product Listings: E-commerce platforms like Amazon, eBay, and others use sorting to arrange
products based on user preferences, such as sorting by price, rating, or newest arrival, to enhance
the shopping experience.
11. Genomic Data Analysis
Sorting DNA Sequences: Sorting plays a crucial role in bioinformatics, especially in sorting DNA
sequences or genomic data. It is used in algorithms for sequence alignment, where sequences need
to be sorted based on certain criteria, such as length or nucleotide composition.
10 mins read
Sorting algorithms play a crucial role in organizing data efficiently, and Bubble Sort is one of the simplest techniques to achieve this. It
repeatedly compares adjacent elements and swaps them if they are in the wrong order, making it a straightforward yet fundamental sorting
method. While not the most efficient for large datasets, Bubble Sort is widely used for teaching purposes due to its easy-to-understand
nature.
In this article, we will explore the working principle of Bubble Sort, analyze its time complexity, and discuss its advantages and limitations
with an implementation in C++ and Python.
Understanding Bubble Sort Algorithm
Bubble Sort is a simple sorting algorithm that repeatedly steps through the list, compares adjacent elements, and swaps them if they are in
the wrong order. This process continues until the list is completely sorted. It is called Bubble Sort because the larger elements "bubble up" to
the end of the list with each pass.
The librarian picks the first two books and compares their heights.
If the first book is taller than the second, they swap them.
Then, they move to the next pair and continue swapping until the tallest book reaches the end.
They repeat the process for the remaining books until all are sorted.
This mimics how Bubble Sort moves larger numbers to the right while sorting the rest of the list.
1. Start
2. Repeat the following steps for (n-1) passes:
Pass 2
Code Example:
#include <iostream>
using namespace std;
// Driver code
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr) / sizeof(arr[0]);
bubbleSort(arr, n);
return 0;
}
Output:
Original array: 64 34 25 12 22 11 90
Sorted array: 11 12 22 25 34 64 90
Explanation:
1. The outer loop iterates through the array, controlling the number of passes.
2. T h e i n n e r l o o p c o m p a r e s a d j a c e n t e l e m e n t s a n d s w a p s t h e m i f t h e y a r e i n t h e w r o n g o r d e r .
We introduce a swapped flag to optimize the sorting process:
1.If no swaps occur in a pass, it means the array is already sorted, so we break out of the
loop early.
The printArray() function iterates through the array and prints its elements.
In main(), we define an integer array with some unsorted values.
We calculate the number of elements in the array using sizeof(arr) / sizeof(arr[0]).
We print the original array using the printArray() function.
We call the bubbleSort() function to sort the array.
We print the sorted array to verify the output.
The program returns 0, indicating successful execution.
Time And Space Complexity Analysis Of Bubble Sort Algorithm
Bubble Sort is a simple sorting algorithm, but its efficiency depends on the arrangement of elements in the input array.
Slow for Large Data Sets (O(n²) Time Complexity) → Inefficient compared to other sorting
algorithms.
Too Many Swaps → Unnecessary swaps increase execution time.
Not Used in Real-World Applications → Better alternatives exist (QuickSort, MergeSort).
Inefficient for Nearly Sorted Arrays (Without Optimization) → Performs redundant passes if
not optimized.
Applications of Bubble Sort Algorithms
While Bubble Sort is not the most efficient sorting algorithm for large datasets, it still has some practical applications in specific scenarios
due to its simplicity and ease of understanding. Here are some of the key applications of Bubble Sort:
11 mins read
Sorting is a fundamental operation in computer science, essential for organizing data efficiently. Among various sorting algorithms, Merge
Sort stands out due to its divide-and-conquer approach, ensuring stable and efficient sorting. Developed by John von Neumann in 1945,
Merge Sort recursively divides an array into smaller subarrays, sorts them individually, and then merges them back together in a sorted
order.
In this article, we will explore the working of Merge Sort, its implementation, time complexity, and why it is preferred over other sorting
techniques in certain scenarios.
1. Divide: The array is recursively divided into two halves until each subarray contains only one
element (which is naturally sorted).
2. Conquer: Each pair of small sorted arrays is merged together in sorted order.
3. Combine: The merging continues until we get a fully sorted array.
For example, let’s sort the array [8, 3, 5, 1, 7, 2, 6, 4] using Merge Sort:
1. Divide:
[8, 3, 5, 1] [7, 2, 6, 4]
[8, 3] [5, 1] [7, 2] [6, 4]
[8] [3] [5] [1] [7] [2] [6] [4]
(Each number is now a single-element array.)
Instead of comparing every element with every other element ( O(n²) time in Bubble Sort or
Insertion Sort), Merge Sort cleverly splits and merges efficiently in O(n log n) time.
The systematic merging ensures stability, meaning equal elements retain their relative order ,
which is crucial in certain applications like database sorting.
Thus, Merge Sort leverages Divide and Conquer to sort data efficiently, just like organizing a messy bookshelf in small, manageable steps!
We keep recursively dividing the array into two halves until each subarray contains a single
element (which is inherently sorted).
2. Sorting and Merging the Subarrays (Conquer & Combine Phase)
Once we reach the smallest subarrays, we start merging them back in a sorted order.
This merging process compares elements from two sorted subarrays and arranges them in the
correct order.
Pseudocode Representation:
MERGE_SORT(arr, left, right)
1. If left < right:
2. Find the middle index: mid = (left + right) / 2
3. Recursively sort the first half: MERGE_SORT(arr, left, mid)
4. Recursively sort the second half: MERGE_SORT(arr, mid + 1, right)
5. Merge the sorted halves: MERGE(arr, left, mid, right)
Code Example:
#include <iostream>
using namespace std;
return 0;
}
Output:
Original array: 8 3 5 1 7 2 6 4
Sorted array: 1 2 3 4 5 6 7 8
Explanation:
1. We begin by including the <iostream> header file, which allows us to use standard input and
output operations.
2. Then we use namespace std; to avoid prefixing std:: before standard library elements.
3. The merge() function is responsible for merging two sorted subarrays into a single sorted
subarray.
4. We calculate the sizes of the two subarrays using mid - left + 1 for the left subarray and right -
mid for the right subarray.
5. Temporary arrays leftArr and rightArr store elements of the left and right subarrays, respectively.
6. We copy elements from the original array into these temporary arrays.
7. A while loop merges the two temporary arrays back into the original array while maintaining the
sorted order.
8. If elements remain in either subarray, we copy them back into the original array.
9. The mergeSort function recursively divides the array into halves until we reach single-element
subarrays.
10. Once divided, the function merges the sorted subarrays back using the merge function.
11. The printArray function prints the elements of an array, separating them with spaces.
12. In main() function, we define an integer array {8, 3, 5, 1, 7, 2, 6, 4} and calculate its size.
13. We print the original array using printArray.
14. We call mergeSort, passing the array and its boundaries (0 to size - 1).
15. After sorting, we print the sorted array using printArray.
16. The program follows the divide-and-conquer approach, making recursive calls and merging sorted
subarrays efficiently.
Time And Space Complexity Analysis Of Merge Sort
Merge Sort follows the Divide and Conquer approach, recursively breaking down the array and merging sorted subarrays. Below is a
detailed analysis of its time and space complexity:
Time
Case Explanation
Complexity
Occurs when the array is already sorted. The algorithm still divides and merges, leading
Best Case O(n log n)
to O(n log n).
The array is randomly ordered. The division process takes O(log n), and merging takes
Average Case O(n log n)
O(n), resulting in O(n log n).
Even in the worst case (reverse-sorted array), the divide and merge steps remain O(n
Worst Case O(n log n)
log n).
Space
O(n) Extra space is required to store temporary subarrays during merging.
Complexity
Key Takeaways
Merge Sort maintains consistent O(n log n) performance across all cases.
It is not in-place due to its O(n) space requirement, making it less memory-efficient than Quick
Sort.
Suitable for sorting large datasets and linked lists due to its stability and efficiency.
Advantages And Disadvantages Of Merge Sort
Merge Sort is a widely used sorting algorithm known for its efficiency and stability. However, it also has some drawbacks.
Advantage Explanation
Consistent O(n log n) Time Unlike Quick Sort, which has a worst case of O(n²), Merge Sort always runs in O(n log n),
Complexity making it predictable.
Maintains the relative order of equal elements, which is useful in applications like database
Stable Sorting Algorithm
sorting.
Works well with large arrays and linked lists since it divides the problem into smaller parts and
Efficient for Large Datasets
merges them efficiently.
Works Well with External Since it processes data in chunks, it is useful for sorting very large files stored in external
Sorting memory (e.g., disk-based sorting).
Disadvantage Explanation
Higher Space Requires extra O(n) space for temporary subarrays, making it not memory-efficient for large in-
Complexity (O(n)) memory sorting.
Slower for Small Arrays Other algorithms like Insertion Sort are faster for small datasets due to lower overhead.
Not an In-Place Unlike Quick Sort, which sorts within the original array, Merge Sort requires additional space, making it
Algorithm less suitable for memory-constrained environments.
1. Sorting Large Datasets – Efficiently sorts massive datasets, especially when dealing
with millions of elements.
2. External Sorting (Disk-Based Sorting) – Used when the data does not fit into memory, such as
sorting large log files or databases stored on hard drives or SSDs.
3. Linked List Sorting – Performs better than Quick Sort for linked lists since it doesn’t
require random access and works efficiently in O(n log n) time.
4. Stable Sorting in Databases – Ensures that records with the same key retain their original order,
making it ideal for database management systems (DBMS).
5. Multi-Threaded Sorting – Works well in parallel computing environments since it can divide the
problem into smaller parts and sort them concurrently.
6. Genomic and Bioinformatics Applications – Used in DNA sequencing and other computational
biology tasks where sorting huge amounts of genetic data is required.
7. Sorting in Graphics and Computer Vision – Helps in image processing tasks, such as
sorting pixels by intensity levels for efficient rendering and filtering.
What Is Selection Sort Algorithm? Explained With Code Examples
Selection Sort repeatedly finds the smallest element and swaps it with the first unsorted one. It's simple but
inefficient for large datasets. It’s in-place but not stable, with minimal swaps.
11 mins read
Sorting is a fundamental operation in computer science, and one of the simplest sorting techniques is the Selection Sort algorithm. It follows
a straightforward approach: repeatedly finding the smallest (or largest) element from an unsorted section and swapping it with the first
unsorted element. While not the most efficient sorting method for large datasets, its simplicity makes it a great choice for learning how
sorting works at a basic level.
In this article, we will explore the working of the Selection Sort algorithm, its time complexity, implementation in different programming
languages, and key advantages and limitations.
The loop runs from the first element to the second-last element since the last element will already
be sorted after all iterations.
We assume the first element of the unsorted section (arr[i]) is the minimum.
2. Find the Minimum Element in the Remaining Array
A second loop (j from i+1 to n-1) scans the unsorted part of the array.
If an element smaller than arr[min_index] is found, update min_index to store its index.
3. Swap the Found Minimum Element with the First Unsorted Element
After finding the smallest element in the unsorted part, swap it with arr[i] to place it in its correct
position.
4. Repeat the Process Until the Entire Array is Sorted
The outer loop progresses, shrinking the unsorted part, until the whole array is sorted.
Implementation Of Selection Sort Algorithm
Below is the C++ implementation of the Selection Sort algorithm.
Code Example:
#include <iostream>
using namespace std;
// Swap the found minimum element with the first element of the unsorted part
swap(arr[i], arr[min_index]);
}
}
int main() {
int arr[] = {29, 10, 14, 37, 13};
int n = sizeof(arr) / sizeof(arr[0]);
selectionSort(arr, n);
return 0;
}
Output:
Original array: 29 10 14 37 13
Sorted array: 10 13 14 29 37
Explanation:
1. We start by including the necessary header file <iostream> to use input-output operations.
The using namespace std; statement allows us to use standard library functions without prefixing
them with std::.
2. The selectionSort() function sorts an array using the Selection Sort algorithm. It takes an array
and its size as parameters.
3. In each iteration of the outer loop, we assume the first unsorted element is the smallest. We store
its index in min_index.
4. The inner loop iterates through the remaining unsorted elements to find the actual smallest
element in that part of the array. If a smaller element is found, we update min_index to its
position.
5. After finding the minimum element in the unsorted part, we swap it with the first unsorted
element, ensuring that the sorted portion of the array expands with each iteration.
6. The printArray() function is a helper function that prints all elements of the array separated by
spaces, followed by a newline.
7. In main(), we define an array {29, 10, 14, 37, 13} and calculate its size using sizeof(arr) /
sizeof(arr[0]).
8. We print the original array before sorting using the printArray() function.
9. We call selectionSort() to sort the array in ascending order.
10. After sorting, we print the updated array to show the sorted result.
11. The program returns 0, indicating successful execution.
Complexity Analysis Of Selection Sort
The table below summarizes the time and space complexity of the Selection Sort algorithm in different scenarios.
Time
Case Explanation
Complexity
Even if the array is already sorted, Selection Sort still compares every element to find
Best Case (Sorted Array) O(n²)
the minimum. No swaps occur, but comparisons remain O(n²).
Average Case (Random Regardless of input, Selection Sort always performs (n-1) + (n-2) + ... + 1
O(n²)
Order) = O(n²) comparisons.
Best/Worst/Average
O(n) At most, there are (n-1) swaps, as only one swap per iteration occurs.
Swaps
Selection Sort is not stable, as swapping non-adjacent elements may disrupt the
Stable? No
relative order of equal elements.
Adaptive? No Selection Sort does not take advantage of partially sorted arrays—it always performs
O(n²) comparisons.
Sorting Algorithm Best Case Average Case Worst Case Space Complexity Stable? Adaptive?
Merge Sort O(n log n) O(n log n) O(n log n) O(n) (Extra Space) Yes No
Quick Sort (Avg.) O(n log n) O(n log n) O(n²) (Worst) O(log n) (In-place) No Yes
Heap Sort O(n log n) O(n log n) O(n log n) O(1) (In-place) No No
Key Takeaways:
Selection Sort is inefficient for large datasets due to its O(n²) time complexity.
Merge Sort, Quick Sort, and Heap Sort are more efficient with O(n log n) time complexity.
Bubble Sort and Insertion Sort are adaptive, meaning they perform better on nearly sorted data.
Stable Sorting Algorithms (like Merge Sort, Bubble Sort, and Counting Sort) maintain the
order of equal elements.
Advantages And Disadvantages Of Selection Sort
Let's explore the strengths and weaknesses of Selection Sort.
1. Simple and Easy to Implement: The algorithm is straightforward and does not require complex
logic.
2. In-Place Sorting Algorithm: It requires only O(1) extra space, meaning it does not need
additional memory beyond the input array.
3. Performs Well on Small Datasets: For small arrays, Selection Sort can be a viable option due to
its simplicity.
4.Works Well When Memory Writes Are Costly: Since it performs at most (n-1) swaps, it
minimizes writes compared to Bubble Sort or Insertion Sort, making it useful in scenarios like
Flash memory where write operations are expensive.
Disadvantages Of Selection Sort:
1. Time Complexity is Always O(n²): Regardless of whether the input is sorted or not, Selection
Sort always performs O(n²) comparisons, making it inefficient for large datasets.
2. Not a Stable Sort: If there are equal elements, their relative order might change due to swapping,
which can cause issues in some applications.
3. Not Adaptive: Selection Sort does not take advantage of already sorted or partially sorted data,
unlike Insertion Sort, which performs O(n) in the best case.
4. Slower Compared to Other Sorting Algorithms: Sorting algorithms like Merge Sort, Quick Sort,
and Heap Sort perform much better on large datasets with an average time complexity of O(n log
n).
Application Of Selection Sort
Some of the common applications of the selection sort algorithm are:
1. Small datasets – Since Selection Sort has a time complexity of O(n²), it is best suited for small
datasets where its simplicity outweighs efficiency concerns.
2. Embedded systems – Due to its minimal memory requirements and in-place sorting nature,
Selection Sort is useful in memory-constrained environments like microcontrollers.
3. Teaching sorting concepts – Selection Sort is often used in educational settings to introduce
sorting algorithms due to its simple logic and step-by-step element swapping.
4. Sorting linked lists – Although Selection Sort is not the best choice for arrays, it works well with
linked lists since swapping nodes requires only pointer adjustments, avoiding unnecessary data
movement.
5. Selecting top k elements – When we only need the smallest or largest k elements, Selection Sort
can be useful since it finds the smallest elements in each iteration.
6. Stable environments – In cases where data movement cost is minimal and simplicity is preferred
over speed, Selection Sort is a viable option.
Conclusion
Selection Sort is a simple yet inefficient sorting algorithm that works by repeatedly selecting the smallest element and placing it in its
correct position. While it is easy to understand and implement, its O(n²) time complexity makes it impractical for large datasets. It is not
adaptive, meaning it does not benefit from partially sorted data, and it is also not stable, which can impact sorting when dealing with
duplicate values.
Despite its limitations, Selection Sort is useful in scenarios where memory writes are expensive, as it performs fewer swaps compared to
other quadratic-time algorithms like Bubble Sort. However, for larger datasets, more efficient sorting algorithms like Merge Sort, Quick
Sort, or Heap Sort should be preferred.
For example, consider the following array of tuples where the second value represents an index:
When memory writes are expensive: Since it performs at most n-1 swaps, it is useful for cases
where minimizing write operations is crucial (e.g., Flash memory, EEPROM).
For small datasets: Due to its simple implementation, it can be used for sorting small
arrays where efficiency is not a major concern.
When a stable sort is not required : If the relative order of duplicate elements does not matter,
Selection Sort can be considered.
However, for large datasets, Merge Sort, Quick Sort, or Heap Sort are better choices due to their O(n log n) time complexity.
Q. How does Selection Sort compare to Bubble Sort and Insertion Sort?
Insertion Sort is adaptive, meaning it performs well on nearly sorted arrays (O(n) in best case).
Bubble Sort is stable, meaning it preserves the order of equal elements, while Selection Sort is
not.
Selection Sort is better than Bubble Sort in terms of fewer swaps, but it still has O(n²)
comparisons.
15 mins read
Sorting algorithms play a crucial role in computer science, enabling efficient organization and retrieval of data. One such fundamental
algorithm is Insertion Sort, which works similarly to how we arrange playing cards in our hands. It builds the sorted list one element at a
time by comparing and placing each new element in its correct position.
In this article, we will explore the Insertion Sort algorithm, its working mechanism, time complexity, and implementation in different
programming languages. Whether you're a beginner or an experienced programmer, understanding this simple yet effective sorting
technique will enhance your grasp of algorithmic problem-solving.
Real-Life Analogy
Imagine you are sorting a deck of playing cards in your hand:
InsertionSort(arr, n):
for i from 1 to n-1:
key = arr[i]
j=i-1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j] // Shift elements to the right
j=j-1
Code Example:
#include <iostream>
// Main function
int main() {
int arr[] = {6, 3, 8, 5, 2};
int n = sizeof(arr) / sizeof(arr[0]);
insertionSort(arr, n);
return 0;
}
Output:
Original array: 6 3 8 5 2
Sorted array: 2 3 5 6 8
Explanation:
1. We start by including the <iostream> header, which allows us to use input and output operations.
2. The using namespace std; directive enables us to use standard library elements without prefixing
them with std::.
3. We define the insertionSort() function, which takes an array and its size as arguments.
4. The sorting process begins with the second element (index 1) since a single-element array is
already sorted.
5. For each element, we store its value in key and compare it with the elements before it.
6. If an element is greater than key, we shift it one position to the right to make space for insertion.
7. Once we find the correct position, we insert the key value.
8. The process repeats for all elements, gradually building a sorted section of the array.
9. The printArray() function helps us display the array elements, separated by spaces.
10. In main(), we define an integer array {6, 3, 8, 5, 2} and determine its size using sizeof(arr) /
sizeof(arr[0]).
11. We print the original array before sorting.
12. The insertionSort() function is called to sort the array.
13. After sorting, we print the updated array to show the sorted order.
14. The program returns 0, indicating successful execution.
Time And Space Complexity Of Insertion Sort
Let’s now have a look at these aspects of the insertion sort algorithm:
1. Time Complexity
Best Case O(n) The array is already sorted. Only one comparison per element, no shifting.
Worst Case O(n²) The array is in reverse order. Each element needs to be compared and shifted.
Average Case O(n²) On average, elements are randomly placed, leading to about n²/4 shifts.
2. Space Complexity
Space Complexity: O(1) (In-place sorting, no extra memory used except a few variables).
Stable Sort? Yes (Preserves the relative order of equal elements).
Advantages Of Insertion Sort Algorithm
Some common advantages of insertion sort are:
1. Simple and Easy to Implement: The algorithm is straightforward and easy to code.
2. Efficient for Small or Nearly Sorted Data: Performs well when the dataset is already partially
sorted.
3. Stable Sorting Algorithm: Maintains the relative order of equal elements.
4. In-Place Sorting: Requires only O(1) extra space (no additional arrays needed).
5.Adaptive Nature: Runs in O(n) time if the array is already sorted, making it faster than other
O(n²) algorithms in such cases.
Disadvantages Of Insertion Sort Algorithm
Some common disadvantages of insertion sort are:
1. Inefficient for Large Datasets: Has a worst-case time complexity of O(n²), making it slow for
large inputs.
2. Not Suitable for Highly Unsorted Data: Requires many comparisons and shifts in the worst case
(reverse-sorted data).
3. Slower Compared to Advanced Sorting Algorithms: Merge Sort (O(n log n)) and Quick Sort
(O(n log n)) perform better for large datasets.
Applications Of Insertion Sort Algorithm
Some of the most common applications of insertion sort are as follows:
1. Small Data Sets: Insertion Sort performs well on small datasets due to its simple implementation
and low overhead. For small lists, its simplicity makes it faster than more complex algorithms like
Merge Sort or Quick Sort.
2. Nearly Sorted Data: If the input list is already partially sorted or nearly sorted, Insertion Sort
performs very efficiently. In such cases, it can be more efficient than algorithms like Merge Sort,
which may still take time for splitting and merging.
3. Online Sorting: Insertion Sort is useful in applications where data is received in a continuous
stream, and elements need to be inserted in sorted order as they arrive. This is often referred to
as online sorting, and Insertion Sort handles such cases very well because it can insert new
elements into an already sorted list incrementally.
4. Adaptive Sorting: In scenarios where data is frequently updated (e.g., inserting new records into a
sorted list), Insertion Sort can adapt well by adding new elements in their correct positions
without re-sorting the entire list.
5. Low Memory Usage: Since Insertion Sort works in-place and requires no extra memory other than
a few variables for tracking elements, it is well-suited for environments where memory is limited,
such as embedded systems or devices with restricted resources.
6. Educational and Demonstration Purposes: Insertion Sort is often used in educational settings to
teach algorithm design and sorting techniques due to its simplicity and intuitive approach. It helps
beginners understand how algorithms work by focusing on fundamental concepts like comparisons
and element swapping.
7. Data Stream Processing: Insertion Sort can be applied in situations where we need to sort
incoming data (e.g., streaming data from sensors or real-time systems) incrementally as each data
point arrives, especially when it’s necessary to maintain an always-sorted list.
8. Implementing Other Algorithms: Insertion Sort can be used as a part of more complex
algorithms, such as Shell Sort, where the initial sorting steps are performed with a smaller gap
and then refined using Insertion Sort for the final ordering.
Comparison With Other Sorting Algorithms
Here's a comparison between Insertion Sort and other popular sorting algorithms like Bubble Sort, Selection Sort, and Merge Sort in terms
of efficiency, complexity, and use cases.
Builds a sorted array one element at a time by Repeatedly swaps adjacent elements if they are in the wrong
Working Principle
inserting each element in its correct position. order, "bubbling" the largest element to the end.
Worst Case
O(n²) (when the list is reversed) O(n²) (same as Insertion Sort in worst case)
Complexity
Average Case
O(n²) O(n²)
Complexity
Use Cases Small datasets, partially sorted data, online sorting Small datasets, educational purposes, small-scale sorting
Key Difference: Insertion Sort is generally faster than Bubble Sort due to fewer comparisons and fewer swaps. Insertion Sort often
outperforms Bubble Sort, especially with partially sorted data.
Builds a sorted array one element at a time by Finds the smallest (or largest) element in the unsorted part of the
Working Principle
inserting each element in its correct position. array and swaps it with the first unsorted element.
Worst Case
O(n²) (when the list is reversed) O(n²) (same as Insertion Sort in worst case)
Complexity
Average Case
O(n²) O(n²)
Complexity
Use Cases Small datasets, partially sorted data, online sorting Simple to implement, small datasets, educational purposes
Key Difference: Insertion Sort is generally more efficient than Selection Sort in terms of the number of swaps required. Selection Sort
makes a fixed number of swaps (n-1 swaps for n elements), while Insertion Sort makes fewer swaps when the data is nearly sorted.
Working Principle Builds a sorted array one element at a time by inserting Divides the array into halves, recursively sorts them, and
each element in its correct position. merges them to form the sorted array.
Best Case
O(n) (when the list is already sorted or nearly sorted) O(n log n) (always, as it splits and merges the array)
Complexity
Worst Case
O(n²) (when the list is reversed) O(n log n) (same for all cases)
Complexity
Average Case
O(n²) O(n log n)
Complexity
Space Complexity O(1) (in-place sorting) O(n) (requires additional space for merging)
15 mins read
Sorting is a fundamental operation in computer science, and Quick Sort stands out as one of the most efficient and widely used sorting
algorithms. It follows the divide-and-conquer approach, breaking down a problem into smaller subproblems and solving them recursively.
The algorithm works by selecting a pivot element, partitioning the array into two halves—one with elements smaller than the pivot and
another with elements greater than the pivot—and then sorting them independently.
In this article, we will explore the working principle of Quick Sort, its time complexity, advantages, and implementation in different
programming languages to understand why it remains a go-to choice for sorting large datasets.
Step-by-Step Breakdown
1. Choose a Pivot – Select an element as the pivot (this could be the first, last, middle, or a
randomly chosen element).
2. Partition the Array – Rearrange elements such that all elements smaller than the pivot are on one
side, and all elements larger than the pivot are on the other side.
3. Recursive Sorting – Apply the same logic to the left and right partitions until the array is
completely sorted.
Real-Life Analogy: Arranging Books On A Shelf
Imagine you have a messy bookshelf and you want to organize the books based on their height. Instead of sorting all the books at once, you
use a smart strategy:
1. Pick a Reference Book (Pivot) – Choose a book at random from the shelf.
2. Partition the Books – Move all books shorter than the chosen book to the left and all taller books
to the right. The reference book is now in its correct position.
3. Repeat the Process – Now, take the left section and apply the same rule: pick another reference
book, sort around it, and continue. Do the same for the right section.
4. Books Get Sorted Naturally – After repeating this process a few times, all the books will be
neatly arranged from shortest to tallest.
Just like Quick Sort, this approach divides the problem into smaller parts and recursively organizes them, leading to an efficient sorting
process.
Select an element from the array as the pivot (commonly, the first, last, or middle element).
Step 2: Partition The Array
arr = [8, 4, 7, 3, 5, 2, 6]
Step 1: Choose a Pivot
Move elements:
1. Pivot = 3
2. A f t e r p a r t i t i o n i n g : [ 2 ] 3 [ 4 ]
Sorting Right Subarray [8, 7, 6]
1. Pivot = 7
2. After partitioning: [6] 7 [8]
Final Sorted Array
[2, 3, 4, 5, 6, 7, 8]
Implementation Of Quick Sort Algorithm
Here's a complete C++ implementation of the Quick Sort algorithm-
Code Example:
#include <iostream>
using namespace std;
// Driver code
int main() {
int arr[] = {8, 4, 7, 3, 5, 2, 6};
int n = sizeof(arr) / sizeof(arr[0]);
quickSort(arr, 0, n - 1);
return 0;
}
Output:
Original Array: 8 4 7 3 5 2 6
Sorted Array: 2 3 4 5 6 7 8
Explanation:
1. We start by including the <iostream> library to enable input and output operations.
2. The partition function is responsible for rearranging the array around a pivot element, which is
chosen as the last element of the array.
3. We initialize i to low - 1 to track the position where smaller elements should be placed.
4. We iterate through the array from low to high - 1, swapping elements that are smaller than the
pivot with the element at i + 1.
5. After the loop, we swap the pivot element with arr[i + 1], ensuring that elements to the left are
smaller and elements to the right are greater.
6. The quickSort function recursively sorts the left and right subarrays by choosing a partition index
using partition.
7. The base condition if (low < high) ensures that recursion stops when the subarray has one or zero
elements.
8. The printArray function iterates through the array and prints each element, followed by a newline
for better readability.
9. In the main function, we define an array {8, 4, 7, 3, 5, 2, 6} and determine its size
using sizeof(arr) / sizeof(arr[0]).
10. We print the original unsorted array using printArray.
11. We call quickSort on the entire array, passing the first and last index as arguments.
12. Finally, we print the sorted array to verify the result.
Time Complexity Analysis Of Quick Sort
Quick Sort is a highly efficient divide-and-conquer sorting algorithm. Its performance largely depends on how well the pivot divides the
array. Let's analyze its time complexity in different scenarios.
Time
Case Explanation
Complexity
Best Case O(n log n) The pivot divides the array into two equal halves, leading to optimal recursive depth.
Average The pivot divides the array into reasonably balanced partitions, ensuring efficient
O(n log n)
Case sorting.
The pivot is always the smallest or largest element, leading to highly unbalanced
Worst Case O(n²)
partitions.
1. The pivot splits the array evenly, reducing the recursion depth.
2. Since each partition is about half the size of the previous, the number of recursive calls
is log(n).
3. T h e o v e r a l l c o m p l e x i t y r e m a i n s O ( n l o g n ) .
Bad Pivot (Smallest or Largest Element) → O(n²)
1. The pivot causes one partition to have n-1 elements and the other to have 0 .
2. This results in n recursive calls, with each call taking O(n) operations.
3. This leads to the worst-case complexity of O(n²).
Advantages Of Quick Sort Algorithm
Quick Sort is one of the most efficient sorting algorithms due to its unique approach. Here are its key advantages:
1. Faster in Practice
Quick Sort has an average-case time complexity of O(n log n) , making it significantly faster than
other sorting algorithms like Bubble Sort (O(n²)) and Insertion Sort (O(n²)) for large datasets.
It outperforms Merge Sort in many real-world applications due to better cache efficiency.
2. In-Place Sorting (Low Memory Usage)
Quick Sort does not require extra memory like Merge Sort (which needs O(n) additional space for
merging).
It sorts the array by swapping elements within the same array, requiring only O(log n) auxiliary
space for recursion.
3. Efficient for Large Datasets
Quick Sort is widely used in large-scale applications such as databases, search engines, and
sorting libraries because of its efficiency.
It is a preferred choice for sorting large datasets compared to Heap Sort and Merge Sort.
4. Suitable for Parallel Processing
Quick Sort can be efficiently implemented using multi-threading by sorting left and right
subarrays in parallel, improving performance.
5. Better Cache Performance
Quick Sort works efficiently with modern CPU architectures due to better locality of reference,
reducing cache misses.
This makes it faster than Merge Sort in practical scenarios, even though both have an O(n log
n) time complexity.
6. Foundation for Library Implementations
Many standard sorting functions in programming languages use Quick Sort or a variant of it.
C++ STL (std::sort) uses a hybrid of Quick Sort and Heap Sort.
Java’s Arrays.sort() for primitives is based on a tuned Quick Sort.
Disadvantages Of Quick Sort Algorithm
Despite its efficiency, Quick Sort has some drawbacks that can impact performance in certain scenarios. Here are its key disadvantages:
If the pivot is poorly chosen, such as always picking the smallest or largest element in an already
sorted array, Quick Sort can degrade to O(n²), making it much slower than Merge Sort (O(n log
n)).
This happens when the partitioning is highly unbalanced, leading to inefficient recursion.
Solution: Use a random pivot or the median-of-three method to improve performance.
2. Not Stable
Quick Sort is not a stable sorting algorithm, meaning it does not preserve the relative order of
equal elements.
For example, if sorting [(A, 5), (B, 5), (C, 3)], Quick Sort might swap (A,5) and (B,5), losing their
original order.
Solution: If stability is required, consider Merge Sort or Insertion Sort.
3. Recursive Overhead
Quick Sort is a recursive algorithm, and deep recursion can lead to stack overflow for large
datasets, especially in languages with limited stack size.
In the worst case (e.g., already sorted or reverse-sorted input with a poor pivot choice), the
recursion depth can reach O(n).
Solution: Use tail recursion optimization or switch to an iterative approach with a manual
stack.
Quick Sort performs poorly on small datasets (typically n < 10), as the recursive overhead
outweighs its efficiency.
In such cases, Insertion Sort is often faster because it has a lower constant factor.
Solution: Many implementations switch to Insertion Sort when subarrays are below a threshold
(e.g., 10 elements).
While Quick Sort is in-place, its recursive function calls require O(log n) additional stack space.
This is still better than Merge Sort (which needs O(n) extra space), but it can still be a limitation
for extremely large arrays.
Solution: Use iterative Quick Sort to avoid recursion overhead.
Quick Sort vs. Other Sorting Algorithms
In this section we will compare quick sort algorithm with different sorting algorithms:
1. Quick Sort Vs. Merge Sort
Time Complexity
O(n log n) / O(n log n) / O(n²) O(n log n) / O(n log n) / O(n log n)
(Best/Average/Worst)
In-Place Sorting Yes (modifies array in-place) No (requires O(n) extra space)
Performance on Large Datasets Very Fast (due to cache efficiency) Fast but needs extra memory
When memory is limited and a fast in-place sort is When stability is required or working with
Best Use Case
needed. linked lists.
Key Takeaway:
Time Complexity
O(n log n) / O(n log n) / O(n²) O(n) / O(n²) / O(n²)
(Best/Average/Worst)
Efficiency Fast for large datasets Very slow for large datasets
Time Complexity
O(n log n) / O(n log n) / O(n²) O(n log n) / O(n log n) / O(n log n)
(Best/Average/Worst)
Quick Sort is used in databases and search engines to handle massive datasets due to its O(n log
n) average-case time complexity.
Example: Sorting millions of records in MySQL, PostgreSQL, and MongoDB.
2. Competitive Programming & Libraries
Many programming languages use Quick Sort or its optimized versions in their standard sorting
functions:
Quick Sort is used in file system operations, such as sorting files by name, size, or date.
Memory management in operating systems utilizes Quick Sort for paging algorithms.
4. Graph Algorithms & Computational Geometry
Quick Sort helps in sorting edges in Kruskal’s algorithm (used in finding Minimum Spanning
Trees).
Used in Convex Hull algorithms (like Graham’s scan) for sorting points based on coordinates.
5. Artificial Intelligence & Machine Learning
In AI, Quick Sort is used for feature selection, where data needs to be sorted based on importance
scores.
Helps in clustering algorithms and sorting training datasets efficiently.
6. Sorting in E-Commerce & Finance
E-commerce platforms like Amazon, Flipkart, and eBay use Quick Sort for sorting products by
price, popularity, or ratings.
Financial systems use Quick Sort in order matching engines to process buy/sell orders in stock
markets.
7. DNA Sequencing & Bioinformatics
In genomic research, Quick Sort is used to arrange DNA sequences based on length, genetic
markers, or frequency.
8. Image Processing & Computer Vision
Used in median filtering, where pixel intensities need to be sorted to remove noise.
Helps in object detection and pattern recognition by sorting feature vectors efficiently.
9. Cybersecurity & Data Encryption
Quick Sort helps in sorting cryptographic keys for secure data transmission.
Used in sorting logs in intrusion detection systems (IDS) to detect unusual activity.
Heap Sort Algorithm - Working And Applications (+ Code Examples)
Heap Sort is a comparison-based sorting algorithm that uses a binary heap to organize elements. It first
builds a heap (O(n)) and then repeatedly extracts the root, heapifying (O(log n)), ensuring O(n log n)
sorting.
11 mins read
Heap Sort is a popular comparison-based sorting algorithm that leverages the properties of a binary heap data structure to efficiently
organize elements in ascending or descending order. It follows a divide-and-conquer approach and operates in two main phases: heap
construction and element extraction. Unlike other sorting techniques, Heap Sort offers a consistent O(n log n) time complexity, making it
suitable for handling large datasets.
In this article, we will explore the working of the Heap Sort algorithm, its implementation, time complexity analysis, and its advantages over
other sorting techniques.
In a max heap, every parent node is greater than or equal to its children.
In a min heap, every parent node is smaller than or equal to its children.
This structure ensures that the maximum (or minimum) element is always at the root, making heaps useful for priority-based operations.
The max heap works like a priority queue, where patients with higher severity (larger values) are
treated first.
As new patients arrive or are treated, the heap dynamically adjusts to ensure the most critical
patient is always at the top.
This concept is widely used in scheduling tasks, Dijkstra’s algorithm for shortest paths, and memory management.
1. Swap the root (largest element) with the last element of the heap.
2. Reduce the heap size (ignore the last element since it's now sorted).
3. Heapify the root to restore the max heap property.
4. Repeat steps 5-7 until only one element remains in the heap.
Step 3: Final Sorted Array
1. BUILD_MAX_HEAP(A)
2.
for i = length(A) downto 2:
swap A[1] with A[i] // Move max element to its correct position
HEAPIFY(A, 1, i-1) // Restore max heap property
BUILD_MAX_HEAP(A):
1.
for i = floor(length(A)/2) downto 1:
HEAPIFY(A, i, length(A))
HEAPIFY(A, i, heap_size):
1. left = 2 * i
2. right = 2 * i + 1
3. largest = i
4. if left ≤ heap_size and A[left] > A[largest]:
largest = left
6. if largest ≠ i:
swap A[i] with A[largest]
HEAPIFY(A, largest, heap_size)
Implementation Of Heap Sort Algorithm
Here’s a C++ implementation of the Heap Sort algorithm:
Code Example:
#include <iostream>
using namespace std;
heapSort(arr, n);
return 0;
}
Output:
Original array: 4 10 3 5 1
Sorted array: 1 3 4 5 10
Explanation:
1. We start by including the <iostream> header file to handle input and output operations.
2. We then begin using namespace std; to avoid prefixing std:: before standard functions like cout.
3. The heapify() function ensures that a given subtree follows the heap property.
Consistent Time Complexity: Heap Sort guarantees a worst-case time complexity of O(n log n),
making it reliable even in the worst-case scenarios.
In-Place Sorting: It sorts the data within the array with only a constant amount of additional
space, which is beneficial for memory-constrained environments.
No Worst-Case Degradation: Unlike some other algorithms (like Quick Sort), Heap Sort's
performance does not degrade in the worst-case scenario, ensuring predictable run times.
Versatility: It can be applied to various types of data and is particularly useful when a guaranteed
time complexity is required.
Simple Data Structure: The use of a binary heap, a well-understood and straightforward data
structure, makes the algorithm conceptually clear.
Disadvantages Of Heap Sort
Some common disadvantages of the heap sort algorithm are:
Not Stable: Heap Sort does not maintain the relative order of equal elements, which can be a
significant drawback when stability is required.
Cache Inefficiency: Its memory access patterns are not as localized as those in algorithms like
Quick Sort, which can lead to suboptimal performance on modern processors with complex caching
mechanisms.
Complex Implementation: While the heap structure is simple conceptually, implementing Heap
Sort correctly (especially in-place heap construction) can be more challenging compared to some
other sorting methods.
Poorer Constant Factors: In practical scenarios, the constant factors hidden in the O(n log n)
complexity can result in slower performance compared to Quick Sort for many datasets.
Not Adaptive: Heap Sort does not take advantage of any existing order in the input data, unlike
some algorithms that can perform better on nearly-sorted arrays.
Real-World Applications Of Heap Sort
Heap Sort is widely used in various domains due to its efficiency and ability to handle large datasets. Below are some real-world
applications where Heap Sort plays a crucial role:
Heap Sort is the foundation for priority queues, which are extensively used in operating systems,
network scheduling, and data structures like heaps (min-heap and max-heap).
Example: Dijkstra’s algorithm for finding the shortest path in graphs uses a priority queue, often
implemented using a heap.
2. Operating Systems (Process Scheduling)
CPU scheduling and disk scheduling use priority queues to determine the order of execution for
processes based on their priority.
Heap Sort ensures that processes are handled efficiently without worst-case performance
degradation.
3. Graph Algorithms
Heap Sort plays a key role in algorithms like Dijkstra’s shortest path algorithm and Prim’s
Minimum Spanning Tree (MST) algorithm , where heaps are used for selecting the next minimum-
cost edge or shortest path.
4. Heap-Based Data Structures
Search engines use heaps to rank web pages based on their relevance.
Large-scale data processing applications, like log file analysis, often use Heap Sort for efficiently
managing and sorting large datasets.
6. Memory Management
Heap Sort is used in garbage collection algorithms in programming languages like Java, where
memory allocation and deallocation must be handled efficiently.
7. Embedded Systems and Real-Time Applications
In embedded systems where deterministic execution time is required, Heap Sort is preferred due to
its consistent O(n log n) complexity.
Real-time applications like traffic management systems use Heap Sort for ranking and scheduling
tasks.
8. Load Balancing and Task Scheduling
Cloud computing and distributed computing frameworks use heaps for efficiently managing
workloads, ensuring that tasks are assigned optimally to servers based on priority.
Counting Sort Algorithm In Data Structures (Working & Example)
Counting Sort is a non-comparison sorting algorithm that counts element occurrences, stores cumulative
sums, and places elements in order. It works in O(n + max) time, making it efficient for small-range
integers.
14 mins read
Sorting is a fundamental operation in computer science used to organize data efficiently for searching, processing, and analysis. Counting
Sort is a non-comparative sorting algorithm that sorts integers by counting the occurrences of each element and using this information to
determine their positions in the sorted array. Unlike comparison-based algorithms such as QuickSort or MergeSort, Counting Sort works by
leveraging the range of input values, making it highly efficient for datasets with a limited range of integers.
In this article, we will explore how the Counting Sort algorithm works, its time complexity, advantages, limitations, and practical
applications.
1. Non-Comparative Sorting – It does not compare elements against each other; instead, it relies on
counting occurrences.
2. Stable Sorting Algorithm – If two elements have the same value, their relative order remains
unchanged in the sorted array.
3. Works Best for a Limited Range – It is efficient when sorting numbers within a known, small
range (e.g., sorting student marks from 0 to 100).
4.Requires Extra Space – It uses an auxiliary array to store counts, making it less efficient when
dealing with large number ranges.
Working Of Counting Sort Algorithm
The algorithm follows a simple approach:
Counting Sort is best suited when the maximum value (max) in the input array is not
significantly larger than the number of elements (n).
If max is too large relative to n, the auxiliary counting array becomes excessively large, leading
to high space complexity.
Example (Good Use Case): Sorting an array of ages [21, 25, 30, 25, 21, 27] (small range: 21-30).
Example (Bad Use Case): Sorting an array containing values between 1 and 1,000,000 when there
are only 10 elements—this would waste a lot of space.
2. Only Works For Non-Negative Integer Values
Counting Sort does not work directly with negative numbers or floating-point values.
The algorithm relies on array indexing, which is non-applicable for negative indices.
Possible Workarounds for Negative Numbers: Shift all numbers by adding the absolute value of
the smallest negative number, then shift back after sorting.
3. Efficient When max Is Close To n
Unlike Quick Sort or Merge Sort, Counting Sort does not compare elements.
It uses counting and indexing, making it useful for special cases but unsuitable for generic
sorting needs.
5. Stable Sorting Algorithm
Counting Sort preserves the relative order of duplicate elements, making it a stable sort.
This is particularly important when sorting records based on multiple attributes (e.g., sorting
students by age while maintaining alphabetical order).
6. Requires Extra Space (O(max))
Index: 0 1 2 3 4 5 6 7 8
Count: 0 0 0 0 0 0 0 0 0
Step 3: Count Occurrences Of Each Element And Store Them In the Counting Array
Now, we traverse the input array and update the counting array by incrementing the index corresponding to each element.
Index: 0 1 2 3 4 5 6 7 8
Count: 0 1 2 2 1 0 0 0 1
Step 4: Modify The Counting Array To Store Cumulative Sums For Positions
We now modify the counting array so that each index stores the cumulative sum of previous values.
This tells us the position where each number should be placed in the final sorted array.
We compute cumulative sums as follows:
Index: 0 1 2 3 4 5 6 7 8
Count: 0 1 3 5 6 6 6 6 7
Step 5: Construct The Sorted Output Array Using The Cumulative Positions
We now create the final sorted array by placing each element in its correct position.
We traverse the input array from right to left to ensure stability (relative order of equal
elements is preserved).
We use the cumulative counting array to determine each element’s position.
Processing from the end of the input array:
Sorted: [1, 2, 2, 3, 3, 4, 8]
Implementation Of Counting Sort Algorithm
Here’s a step-by-step C++ implementation of Counting Sort, following the logic we discussed earlier.
Code Example:
#include <iostream>
#include <vector>
#include <algorithm> // Include this for max_element
// Driver function
int main() {
vector<int> arr = {4, 2, 2, 8, 3, 3, 1}; // Example array
countingSort(arr);
return 0;
}
Output:
Original Array: 4 2 2 8 3 3 1
Sorted Array: 1 2 2 3 3 4 8
Explanation:
1. We begin by including necessary headers: <iostream> for input/output and <vector> for using
dynamic arrays.
2. The countingSort() function sorts an array using the Counting Sort algorithm. It takes a
vector<int>& arr as input, meaning the original array is modified directly.
3. First, we find the maximum element in the array using max_element(), as this determines the size
of our count array.
4. We create a count array of size max + 1, initializing all elements to zero. This array will store the
frequency of each number in arr.
5. We iterate through arr and update count[num] to track how many times each number appears.
6. Next, we modify the count array to store cumulative counts, which help in placing elements at the
correct position in the sorted array.
7. We create an output array of the same size as arr. Starting from the last element in arr, we place
each element in its correct position using the count array, ensuring stability.
8. After placing each element, we decrement its count to handle duplicates properly.
9. Finally, we copy the sorted output array back into arr.
10. The main() function initializes an example array, prints it, calls countingSort(), and prints the
sorted result.
Time And Space Complexity Analysis Of Counting Sort
Here’s a detailed breakdown of Counting Sort’s complexity in different scenarios:
Complexity
Notation Explanation
Type
O(n + When the array is already sorted, Counting Sort still processes all elements and fills the
Best Case (Ω)
max) counting array.
O(n +
Worst Case (O) When max is very large, creating and processing the counting array takes significant time.
max)
Space O(n +
Additional space is required for both the counting array (O(max)) and output array (O(n)).
Complexity max)
Key Observations:
Linear Time Complexity: Counting Sort runs in O(n + max), making it faster than comparison-
based algorithms (like Quick Sort O(n log n)) for small-range values.
High Space Requirement: If max is too large, the algorithm wastes memory, making it impractical
for sorting large-range numbers.
Stable Sorting: The sorting preserves the order of duplicate elements, making it useful for
applications requiring stability.
Comparison Of Counting Sort With Other Sorting Algorithms
Here’s a detailed comparison of Counting Sort against other common sorting algorithms:
Time
Sorting Time Complexity Time Complexity Space Comparison-
Complexity Stable? Best Use Case
Algorithm (Average) (Worst) Complexity Based?
(Best)
Counting
O(n + max) O(n + max) O(n + max) O(n + max) Yes No Small range integers
Sort
Small datasets,
Bubble Sort O(n) O(n²) O(n²) O(1) Yes Yes
nearly sorted lists
Priority queues,
Heap Sort O(n log n) O(n log n) O(n log n) O(1) No Yes
heaps
Large integers,
Radix Sort O(nk) O(nk) O(nk) O(n + k) Yes No
fixed-length strings
Key Takeaways:
Counting Sort is very fast for small-range integer values but has a high space cost.
Quick Sort is best for general-purpose sorting with good average-case performance.
Merge Sort is useful when stability is required, despite using extra space.
Bubble, Selection, and Insertion Sort are not suitable for large datasets.
Heap Sort is efficient but not stable.
Radix Sort is useful for sorting large numbers or strings but needs extra space.
Advantages Of Counting Sort Algorithm
Some of the advantages of counting sort algorithms are:
1. Linear Time Complexity (O(n + max)) – Faster than comparison-based sorting algorithms for
small-range numbers.
2. Stable Sorting Algorithm – Maintains the relative order of duplicate elements, which is useful for
sorting records.
3. Non-Comparison Sorting – Does not rely on element comparisons like Quick Sort or Merge Sort,
making it efficient for integers.
4. Efficient for Small Range – Performs well when sorting numbers within a limited range, such as 0
to 1000.
5. Suitable for Large Input Size – When max is small, Counting Sort is very fast, even for large n.
Disadvantages Of Counting Sort Algorithm
Some of the disadvantages of counting sort algorithms are:
1. High Space Complexity (O(n + max)) – Requires additional space for the counting array, making
it inefficient for large max values.
2. Not Suitable for Large Ranges – If the maximum element (max) is significantly larger
than n, memory usage becomes impractical.
3. Limited to Integers – Cannot be used for sorting floating-point numbers or complex data
types directly.
4. Inefficient for Unbounded Data – If numbers have a very large range, Counting Sort
becomes space-inefficient compared to Quick Sort (O(n log n)).
5. Not In-Place Sorting – Requires extra memory to store the counting array and output array,
unlike Quick Sort, which sorts in-place.
Applications Of Counting Sort Algorithm
Counting Sort is useful in scenarios where the range of input values is small and well-defined. Here are some key applications:
Used when sorting integers within a known, small range (e.g., student roll numbers, exam scores,
or age data).
Example: Sorting exam scores between 0 to 100 for thousands of students efficiently.
2. Data Analysis And Histogram Counting
Used in digital circuits, where sorting small fixed-size integer values is needed for performance
optimization.
Example: Sorting pixel intensity values in image processing.
4. DNA Sequencing And Bioinformatics
Used in electronic voting machines (EVMs) to count and sort votes based on candidate IDs.
Example: Sorting vote counts when candidate IDs are within a small range.
6. Radar And Sensor Data Processing
Applied in military and weather systems where sensor data values fall within a limited numerical
range.
Example: Sorting radar frequency signals in air traffic control.
7. Network Packet Sorting
10 mins read
Sorting is a fundamental operation in computer science, used to arrange data in a specific order for efficient searching, retrieval, and
processing. One such sorting algorithm is Shell Sort, an optimization of Insertion Sort that significantly improves performance for larger
datasets. Developed by Donald Shell in 1959, this algorithm introduces the concept of gap-based sorting, which allows elements to move
over larger distances in the initial stages, reducing the total number of swaps required.
In this article, we will explore the working mechanism of Shell Sort, its time complexity, advantages, and implementation in code.
Compares and inserts elements one by one in a Uses a gap sequence to compare and swap distant
Sorting Approach
sorted portion of the array. elements, reducing large shifts.
Inefficient for large datasets due to many shifts of More efficient as distant swaps reduce overall shifts
Efficiency on Large Data
elements. needed.
Adaptability Performs well on nearly sorted data. Works well even for moderately unsorted data.
Number of Comparisons Lower than Insertion Sort because elements move across
High, as elements are moved one step at a time.
& Swaps larger gaps first.
Best for Small Data? Yes, simple and effective for small arrays. Can be overkill for very small datasets.
1. Initially, you pick books that are far apart (let’s say every 5th book) and arrange them in
order.
2. This ensures that roughly sorted sections appear quickly, reducing the need for excessive
small adjustments later.
Smaller Gaps Next:
1. Now, you refine the order by arranging books every 2nd position.
2. This further organizes the shelf with fewer movements compared to placing each book one
by one from the beginning.
Final Fine-Tuning:
1. Finally, when books are almost in place, you switch to sorting adjacent books, making
small adjustments to achieve the perfectly ordered shelf.
This method is much faster than sorting one book at a time because you reduce unnecessary small movements in the early stages, just
like Shell Sort optimizes sorting using gap-based swaps before fine-tuning with Insertion Sort.
Pass 1 (gap = 2)
Compare elements that are 2 positions apart:
Index 0 1 2 3 4
Array 12 34 54 2 3
Now, the gap is reduced to 1, which means we perform Insertion Sort on the nearly sorted array.
Code Example:
#include <iostream>
using namespace std;
int main() {
int arr[] = {12, 34, 54, 2, 3};
int n = sizeof(arr) / sizeof(arr[0]);
shellSort(arr, n);
return 0;
}
Output:
Original array: 12 34 54 2 3
Sorted array: 2 3 12 34 54
Explanation:
1. We start by including the <iostream> header to handle input and output operations.
2. The using namespace std; directive allows us to use standard library functions without prefixing
them with std::.
3. The shellSort() function sorts an array using the Shell Sort algorithm, which is an optimized
version of insertion sort that works with gaps.
4. We begin with a large gap (n / 2) and keep reducing it by half in each iteration until it becomes
0.
5. For each gap value, we apply insertion sort but only on elements that are gap positions apart.
6. We pick an element and compare it with previous elements at a distance of gap, shifting elements
if needed to maintain order.
7. Once the correct position is found, we insert the element there. This improves efficiency compared
to normal insertion sort.
8. The printArray() function prints the elements of the array in a single line, separated by spaces.
9. In main(), we define an array {12, 34, 54, 2, 3} and determine its size using sizeof(arr) /
sizeof(arr[0]).
10. We print the original array before sorting.
11. We call shellSort() to sort the array in ascending order.
12. After sorting, we print the modified array to display the sorted elements.
Time Complexity Analysis Of Shell Sort Algorithm
Shell Sort’s time complexity depends on the gap sequence used, as it determines how elements move across the array. Below is the
complexity analysis for different cases:
The best case occurs when the array is already nearly sorted and requires minimal swaps.
If the gap sequence is chosen well (e.g., Hibbard's sequence), the best-case complexity is Ω(n log
n).
2. Average-Case Complexity (Θ(n log² n))
The worst case occurs when a bad gap sequence is chosen, causing inefficient sorting (similar to
Insertion Sort).
With gaps reducing inefficiently (like simply dividing by 2), the worst-case complexity
becomes O(n²).
Impact Of Different Gap Sequences On Performance
Shell’s Original n/2, n/4... O(n²) O(n²) Slower, inefficient for large inputs.
Sedgewick’s Sequence Hybrid approach O(n^(4/3)) O(n^(4/3)) More efficient than Hibbard’s.
Pratt’s Sequence 2^p * 3^q O(n log² n) O(n log² n) One of the best choices for Shell Sort.
Key Takeaway: The choice of gap sequence greatly impacts performance, with Sedgewick’s and Pratt’s sequences offering the best
practical results, making Shell Sort approach O(n log n) efficiency.
1. Improves Over Insertion Sort: By allowing far-apart elements to move earlier, Shell Sort reduces
the number of shifts required in the final pass.
2. Efficient for Moderate-Sized Data: Performs significantly better than Bubble Sort and Insertion
Sort for medium-sized datasets.
3. In-Place Sorting Algorithm: Requires no extra space (O(1) space complexity), making it memory-
efficient.
4. Adaptive for Nearly Sorted Data: If the array is already partially sorted, Shell Sort performs
very efficiently.
5. Flexible Choice of Gap Sequence: Different gap sequences can optimize performance for specific
datasets.
Disadvantages Of Shell Sort Algorithm
Some common disadvantages of shell sort algorithms are:
1. Not Stable: Shell Sort does not maintain the relative order of equal elements, making it unstable.
2. Performance Depends on Gap Sequence: A poorly chosen gap sequence (e.g., simple division by
2) can lead to O(n²) worst-case complexity.
3. Not Optimal for Very Large Datasets: While better than Insertion Sort, it is still outperformed
by Quick Sort, Merge Sort, and Heap Sort for large datasets.
4. Complex to Implement Efficiently: Finding the best gap sequence requires additional research
and tuning for different datasets.
Applications Of Shell Sort Algorithm
Shell Sort is widely used in scenarios where insertion-based sorting is preferable but needs optimization for larger datasets. Here are some
practical applications of Shell Sort:
Use Case: When sorting a moderate number of elements where Merge Sort or Quick Sort may be
unnecessary due to their extra space usage or recursive overhead.
Example: Sorting small logs or records in applications with limited memory.
2. Improving Performance in Hybrid Sorting Algorithms
Use Case: Many hybrid sorting algorithms, like Timsort (used in Java’s Arrays.sort() for
smaller arrays), use Insertion Sort when subarrays become small.
Shell Sort can be used as an optimization over Insertion Sort for improved performance.
3. Cache-Friendly Sorting in Low-Level Systems
Use Case: In embedded systems and memory-constrained environments , Shell Sort works well
as it has better cache performance compared to other O(n²) sorting algorithms.
4. Organizing Records in Databases
Use Case: Databases with moderately sized datasets can use Shell Sort for quick data reordering
before applying more complex indexing algorithms.
5. Used in Competitive Programming
Use Case: Due to its in-place sorting and simple implementation , Shell Sort is often used in
competitive programming when sorting small arrays efficiently.
19 mins read
Imagine searching for a word in a dictionary. Do you flip through every page one by one? Of course not! You jump to the middle, check the
word, and then decide whether to move left or right. That’s precisely how Binary Search works—an efficient searching algorithm that
halves the search space with each step.
Used in databases, search engines, and competitive programming, Binary Search is a fundamental algorithm every programmer should
master. In this article, we will discuss the binary search algorithm in detail, including how it works, when to use it, its implementation with
examples, and its real-world applications.
1. Start with a sorted array and two pointers–one at the beginning (low) and one at the end (high).
2. Find the middle element (mid).
3. Compare it with the target:
// Sorted Array
{5, 12, 18, 25, 36, 42, 50}
Here is how binary search will work for this array:
Algorithm Steps:
int main() {
int arr[] = {5, 12, 18, 25, 36, 42, 50};
int n = sizeof(arr) / sizeof(arr[0]);
int target = 25;
if (result != -1){
cout << "Element found at index " << result << endl;
}else{
cout << "Element not found" << endl;}
return 0;
}
Output:
We begin the code example by including the header file <iostream> for input and output operations. The using namespace std; statement is
added to avoid using std:: before standard library functions.
Function Definition: We define a C++ function binarySearch() that implements the binary search
algorithm iteratively.
o The function takes three arguments: arr[] (sorted array), n (array size), and target
(element to be searched).
o Inside, we initialize two pointers: low to 0 (starting index) and high = n - 1 (last index).
Binary Search Implementation: Then, we use a while loop to repeatedly divide the array into
halves as long as low <= high.
After that, we compare the element at the mid position with the target using an if-else-if
statement:
Inside the loop, we first calculate the middle index. This avoids potential integer overflow that
could occur if we used (low + high) / 2.
o If arr[mid] == target, we return mid (index of the found element).
o If arr[mid] < target, the target must be in the right half → update low = mid + 1.
o If arr[mid] > target, the target must be in the left half → update high = mid - 1.
o If the loop exits without finding the element, -1 is returned, indicating the target was not
found.
Main Execution: In the main() function, we define an integer array arr containing sorted elements
{5, 12, 18, 25, 36, 42, 50}.
Then, we declare and initialize a variable target with the integer data type value 25, which we
want to search for.
We then use the sizeof() operator to calculate the size of the array.
Next, we call the function binarySearch() function with arr, n, and target as arguments. We store
the outcome in the variable result.
Result Handling: We use an if-else statement to check if the result is not equal to -1, to see
whether the element is found:
o If the result is not -1, the program prints the index of the found element.
o Otherwise, it prints "Element not found".
Iterative Binary Search Program In Python Example
def binary_search(arr, target):
low, high = 0, len(arr) - 1
# Example usage
arr = [5, 12, 18, 25, 36, 42, 50]
target = 25
if result != -1:
print(f"Element found at index {result}")
else:
print("Element not found")
Output:
Function Definition: We define a function binary_search() that implements the binary search
algorithm iteratively.
o The function takes two arguments: arr (a sorted list) and target (the element to be
searched).
o Inside the function, we initialize two pointers: low with the value 0 (starting index) and
high assigned the value given by len(arr) - 1 (last index).
Binary Search Implementation: Then, using a while loop, we repeatedly divide the array into
halves as long as low <= high.
Inside the loop, we calculate the middle index (mid = (low + high) // 2) to ensure that integer
division avoids errors caused by floating-point values.
We then compare the element at mid position with the target using an if-elif statement:
o If arr[mid] == target, we return mid (index of the found element).
o If arr[mid] < target, the target must be in the right half, so we update low = mid + 1.
o If arr[mid] > target, the target must be in the left half, so we update high = mid - 1.
oIf the loop exits without finding the element, -1 is returned, indicating the target was not
found.
Main Execution: We define a sorted list arr containing elements [5, 12, 18, 25, 36, 42, 50].
We also define and initialize a variable target with the valur 25, which we want to search for.
Then, we call the function binary_search() with arr and target as arguments and store the returned
result in result.
Result Handling: Then, with an if-else statement, we check whether the element is found by
comparing the result with -1 (using not equal to relational operator).
o If the result is not -1, the program prints the index of the found element.
o Otherw ise, it prints "Element not found".
Algorithm Steps:
int main() {
int arr[] = {5, 12, 18, 25, 36, 42, 50};
int n = sizeof(arr) / sizeof(arr[0]);
int target = 25;
if (result != -1) {
cout << "Element found at index " << result << endl;
} else {
cout << "Element not found" << endl;
}
return 0;
}
Output:
Function Definition: We define a function binarySearch() that implements the recursive version of
the binary search algorithm. The function takes four arguments:
o arr[]: The sorted array in which we search for the target.
o low: The starting index of the array (or subarray).
o high: The last index of the array (or subarray).
o target: The element to be searched.
Recursive Binary Search Implementation: We define the base case, where the function first
checks if low > high. This means that the element is not present in the array or subarray. In this
case, -1 is returned.
The middle index is calculated in the same way as in the iterative version:
o If arr[mid] == target, the function returns mid, indicating that the element has been found.
o If arr[mid] < target, we know the target lies in the right half, so the function is called
recursively with updated low = mid + 1 and the same high.
o If arr[mid] > target, the target lies in the left half, so the function is called recursively
with updated high = mid - 1 and the same low.
o If the target element is not found during the recursion, the function eventually returns -1.
Main Execution: We define the sorted array arr containing elements {5, 12, 18, 25, 36, 42, 50}.
The target is set to 25.
We call the function binarySearch() with arr, 0, n - 1, and target. The returned result is stored in
the result variable.
Result Handling: We check if the result is not equal to -1 using an if-else statement.
o If the result is not -1, the program prints the index of the found element.
o Otherwise, it prints "Element not found".
Recursive Binary Search Program In Python
def binary_search(arr, low, high, target):
if low > high:
return -1 # Element not found   Â
# Example usage
arr = [5, 12, 18, 25, 36, 42, 50]
target = 25
if result != -1:
print(f"Element found at index {result}")
else:
print("Element not found")
Output:
Function Definition: We define a function binary_search() that implements the recursive binary
search algorithm. The function takes four arguments:
o arr: The sorted list in which we search for the target.
o low: The starting index of the list (or sublist).
o high: The last index of the list (or sublist).
o target: The element to be searched.
Recursive Binary Search Implementation: We define the base case where the function checks if
low > high. If true, it returns -1, indicating the element is not present.
The middle index is calculated in the same way as the iterative approach:
o If arr[mid] == target, the function returns mid, indicating the element has been found.
o If arr[mid] < target, we recursively search the right half by calling the function with
updated low = mid + 1 and the same high.
o If arr[mid] > target, we recursively search the left half by calling the function with
updated high = mid - 1 and the same low.
o If the element is not found, the function will eventually return -1.
Main Execution: We define the sorted list arr containing elements [5, 12, 18, 25, 36, 42, 50]. The
target is set to 25.
We call the function binary_search() with arr, 0, len(arr) - 1, and target. The result is stored in the
result.
Result Handling: We check if the result is not -1 using an if-else statement.
o If the result is not -1, it prints the index of the found element.
o Otherwise, it prints "Element not found".
Best Case: In the best-case scenario, the element is found at the middle index on the first
comparison, so the algorithm terminates immediately. This occurs in constant time.
Time Complexity (Best Case): O(1)
Average and Worst Case: In both the average and worst cases, we halve the search space at each
step. This is because, with each iteration (or recursion), we discard half of the remaining elements.
In the worst case, we may have to repeatedly halve the list until we narrow down to the target or
determine the target is absent.
Time Complexity (Average and Worst Case): O(log n)
Binary search is highly efficient with a time complexity of O(log n), making it much faster than linear search for large datasets.
Iterative Approach: The iterative approach of binary search uses a fixed amount of space. It only
requires a few variables to track the indices (low, high, mid), irrespective of the size of the input.
Space Complexity (Iterative): O(1)
Recursive Approach: The recursive approach involves function calls, and each recursive call adds
a new frame to the call stack. The maximum depth of recursion is proportional to the logarithm of
the array size, which happens when the array is repeatedly halved.
Space Complexity (Recursive): O(log n)
Summary Of Complexity Analysis For Binary Search Algorithm
In this approach, the binary search works using a while loop that repeatedly divides the array in
half. This method is preferred when minimizing memory usage is a priority, as it doesn't involve
extra function calls.
The space complexity for the iterative approach is O(1), since it only uses a few variables to track
the indices (low, high, mid).
Recursive Approach:
In this approach, binary search is implemented using function calls that call themselves with a
narrowed search space. This method is usually easier to understand and implement as it follows a
more natural divide-and-conquer pattern.
The space complexity for the recursive approach is O(log n), due to the function calls stacked on
the call stack.
Key Differences Iterative Vs. Recursive Binary Search
Often faster (due to lack of overhead from Slightly slower (due to function call
Performance
function calls) overhead)
Advantages Disadvantages
Less Comparisons: Compared to linear search, Not Suitable for Linked Lists: Binary search is
binary search requires fewer comparisons to find inefficient for linked lists since accessing the
an element, as it eliminates half of the remaining middle element requires O(n) time, making it
search space with each step. unsuitable for linked structures.
Stable Search in Sorted Arrays: It guarantees Overhead in Small Datasets: For small datasets,
finding an element quickly in sorted arrays the overhead of performing binary search may
without the need for additional sorting. outweigh the benefits, as simpler algorithms like
linear search could be faster.
Searching in Sorted Arrays: Binary search is commonly used to search for an element in sorted
arrays. The array could be part of any system that requires fast access to sorted data, like a
dictionary, inventory, or ordered list. Example Use Case: Finding the position of a word in a
sorted dictionary or looking up a specific product in a sorted inventory list.
Finding Elements in Large Data Sets: When working with massive datasets (like logs, databases,
or large collections of files), binary search helps locate the desired element in logarithmic time
rather than linearly scanning the entire set. This makes it extremely efficient in big data
scenarios. Example Use Case: Searching for a customer’s order in a massive e-commerce database
or locating a document in a large index of files.
Applications in Databases: In databases, especially when performing search queries on sorted
tables, binary search is essential for improving query performance. Many database systems rely on
indexing techniques that use binary search to quickly find records without scanning the entire
dataset. Example Use Case: When running an SQL query with a WHERE clause on a sorted
column, the database may internally use binary search on an index to find the rows more
efficiently.
Real-World Examples Of Binary Search
1. Version Control Systems: Version control systems like Git use binary search to pinpoint which
commit introduced a bug. By searching through the commit history in a binary fashion, developers
can quickly isolate the problematic commit.
2. Pricing Algorithms: Binary search is used in competitive pricing algorithms where a company
adjusts its prices based on competitor data. By maintaining a sorted list of competitor prices,
binary search can quickly identify the optimal pricing point.
3. Stock Market Algorithms: In high-frequency trading, sorted price lists are used for real-time
decision-making. Binary search enables traders to quickly retrieve the best bid/ask price,
maximizing trading efficiency.
15 mins read
In data structures, a linked list is a dynamic linear data structure where elements, called nodes, are connected using pointers. Unlike arrays,
linked lists do not require contiguous memory allocation, making them efficient for insertions and deletions. Each node typically consists
of data and a pointer to the next node, forming a chain-like structure.
In this article, we will explore the types of linked lists, their implementation, advantages, and real-world applications to understand why they
are a fundamental concept in data structures.
A Linked List is a data structure used to store a collection of elements, where each element (called a node) contains two parts:
Each coach represents a node (it has data: passengers, and a link to the next coach).
The engine (head of the linked list) points to the first coach.
If we want to add or remove a coach, we don’t need to shift all other coaches (unlike an array
where shifting might be required).
The last coach (node) has no further link, just like the tail of a linked list points to NULL.
Types Of Linked Lists In Data Structures
A linked list is a dynamic data structure consisting of nodes connected through pointers. Depending on how nodes are linked, linked lists
can be classified into different types.
Structure:
Advantages:
Data
A pointer to the next node
A pointer to the previous node
This structure allows both forward and backward traversal.
Structure:
Advantages:
Advantages:
Singly Linked List is suitable when memory optimization is a priority and one-way traversal is
sufficient.
Doubly Linked List is useful when frequent insertions and deletions are required, and backward
traversal is needed.
Circular Linked List is ideal when a continuous looping structure is required, such as
in operating systems and scheduling algorithms.
Linked List Operations In Data Structures
A linked list is a dynamic data structure that allows insertion and deletion of elements efficiently. The fundamental operations on a linked
list include:
1. Insertion
Adding a new node at different positions in the linked list.
Types of Insertion:
At the Beginning: The new node becomes the head, and its pointer is set to the previous head.
At a Specific Position: The new node is inserted between two existing nodes, adjusting their
pointers.
At the End: The new node is added at the tail, and the last node’s pointer is updated.
Real-Life Analogy:
Adding a new coach to a train at the front, middle, or end of a railway track.
Complexity:
#include <iostream>
using namespace std;
class Node {
public:
int data;
Node* next;
Node(int value) {
data = value;
next = NULL;
}
};
if (temp == NULL) {
cout << "Position out of bounds!" << endl;
return;
}
newNode->next = temp->next;
temp->next = newNode;
}
// Function to print the list
void printList(Node* head) {
while (head != NULL) {
cout << head->data << " -> ";
head = head->next;
}
cout << "NULL" << endl;
}
int main() {
Node* head = NULL;
insertAtBeginning(head, 10);
insertAtEnd(head, 30);
insertAtPosition(head, 20, 1);
return 0;
}
Output:
1. Node Class:
1. We define a Node class with data (to store values) and next (to point to the next
node).
2. T h e c o n s t r u c t o r i n i t i a l i z e s t h e d a t a a n d s e t s n e x t t o N U L L .
Insert at Beginning:
1. We create a new node and point its next to the current head.
2. The head is updated to the new node.
Insert at End:
Types of Deletion:
From the Beginning: The head node is removed, and the next node becomes the new head.
From a Specific Position: The node is removed, and the previous node’s pointer is updated to skip
it.
From the End: The last node is removed, and the second-last node’s pointer is set to NULL.
Real-Life Analogy:
Removing a specific coach from a train without affecting the remaining coaches.
Complexity:
int main() {
Node* head = NULL;
insertAtEnd(head, 10);
insertAtEnd(head, 20);
insertAtEnd(head, 30);
deleteNode(head, 1);
return 0;
}
Output:
Approach:
Complexity:
int main() {
Node* head = NULL;
insertAtEnd(head, 10);
insertAtEnd(head, 20);
insertAtEnd(head, 30);
return 0;
}
Output:
Linked List: 10 20 30
Explanation:
Complexity:
int main() {
Node* head = NULL;
insertAtEnd(head, 10);
insertAtEnd(head, 20);
insertAtEnd(head, 30);
return 0;
}
Output:
1. Search Function:
Approach:
int main() {
Node* head = NULL;
insertAtEnd(head, 10);
insertAtEnd(head, 20);
insertAtEnd(head, 30);
return 0;
}
Output:
Unlike arrays, linked lists do not require a fixed size during declaration.
Memory is allocated as needed, avoiding wastage.
Inserting or deleting elements in a linked list is efficient (O(1) for beginning and O(n) for middle
or end).
No need for shifting elements as in arrays.
Since linked lists grow dynamically, there is no need to reserve extra memory in advance, reducing
wastage.
Linked lists serve as the foundation for stacks, queues, graphs, and hash tables .
They are essential for creating circular and doubly linked lists.
Unlike arrays, where elements must be of the same size, linked lists can have elements of varying
sizes.
Disadvantages Of Linked Lists
Each node requires additional memory for storing a pointer to the next node.
This increases the overall memory consumption compared to arrays.
Unlike arrays, where elements can be accessed using an index, linked lists require sequential
traversal (O(n)) to access a node.
Random access is not possible.
4. Cache Unfriendliness
Unlike arrays, which store elements in contiguous memory locations, linked lists use scattered
memory.
This results in poor cache locality and slower performance due to frequent memory access.
Comparison Of Linked Lists And Arrays
In data structures, both linked lists and arrays are used to store collections of data, but they differ in memory management, access speed, and
efficiency. Arrays offer fast random access due to their contiguous memory allocation, whereas linked lists allow efficient insertions and
deletions because they use dynamic memory allocation.
The table below provides a detailed comparison between Linked Lists and Arrays, highlighting their key differences in terms of memory,
performance, and operations.
Extra memory required for pointers (next and previous in doubly No extra memory required for pointers; only data
Memory Usage
linked lists). is stored.
Data Storage Stored in non-contiguous memory locations. Stored in contiguous memory locations.
Element Access Sequential access (O(n)), must traverse from the head. Direct access (O(1)), using an index.
Insertion at Beginning O(1) (constant time, just update the head pointer). O(n) (requires shifting all elements).
O(n) for singly linked lists (traverse to the last node), O(1) if tail O(1) if space is available, O(n) if resizing is
Insertion at End
pointer is maintained. needed.
Insertion at Middle O(n) (requires traversal to find the correct position). O(n) (requires shifting elements to create space).
Deletion at Beginning O(1) (just update the head pointer). O(n) (requires shifting elements after removal).
O(n) for singly linked lists (traverse to the last node), O(1) if tail O(1) if no resizing is needed, O(n) if resizing
Deletion at End
pointer is maintained. occurs.
Deletion at Middle O(n) (requires traversal to find the correct position). O(n) (requires shifting elements to fill the gap).
Low (nodes are scattered in memory, affecting CPU cache High (stored in contiguous memory, improves
Cache Friendliness
performance). CPU cache performance).
Implementation
Complex (requires pointer management). Simple (index-based access).
Complexity
When frequent insertions/deletions are needed (e.g., dynamic When fast access is required (e.g., look-up tables,
Best Use Cases
memory allocation, linked stacks, and queues). matrices, and static lists).
When insertion and deletion operations are more frequent than access operations.
When dynamic memory allocation is required without wasting space.
When implementing stack, queue, graph structures, and hash tables.
When To Use An Array?
Used in operating systems to manage memory allocation (e.g., heap memory allocation).
Enables programs to use memory efficiently without knowing the exact size in advance.
2. Implementation of Stacks and Queues
Stacks (Last In, First Out – LIFO) can be implemented using a singly linked list.
Queues (First In, First Out – FIFO) can be implemented using a singly or doubly linked list.
3. Managing Graph Data Structures
Adjacency lists in graphs use linked lists to store neighbors of a node efficiently.
Used in social networks, navigation systems, and recommendation algorithms .
4. Undo/Redo Functionality in Software
Applications like text editors, graphic software, and web browsers use doubly linked lists to
track changes.
Users can navigate back and forth using the previous and next pointers.
5. Hash Tables (Collision Handling with Chaining)
Polynomial operations (addition, multiplication) use linked lists to store terms dynamically.
Large numbers (beyond built-in data types) are represented as linked lists to handle operations
efficiently.
7. Browser Navigation (Forward and Backward)
Doubly linked lists are used to store browsing history in web browsers.
Users can navigate forward and backward easily.
8. Music and Video Playlists
Circular linked lists are used in media players where the last song connects back to the first
song.
Enables seamless looping of playlists.
9. OS Process Management (Job Scheduling)
Operating systems use linked lists to manage processes and tasks in schedulers.
Circular linked lists help in round-robin scheduling.
10. Implementation of LRU (Least Recently Used) Cache
22 mins read
Picture this: you’re organizing a line of tasks to complete one after another. You can only see the current task and the next one ahead, and
each task knows only the one that follows. This concept is the essence of a singly linked list, a fundamental data structure in programming.
In this article, we’ll explore singly linked lists, including what they are, how they work, and why they’re essential for storing and managing
data. Whether you’re coding a to-do list, building a navigation system, or designing a queue for processing, singly linked lists are here to
make your life easier.
Dynamic Size: They grow or shrink as needed, making them memory-efficient compared to arrays.
Sequential Access: Nodes can only be accessed sequentially, starting from the head.
Efficient Insertion/Deletion: Adding or removing elements doesn’t require shifting data, unlike
arrays.
Think of a singly linked list as a treasure hunt. Each clue (node) points to the location of the next clue until you reach the final treasure (end
of the list). Now that you know the basics of single-linked lists in data structures, let’s take a look at the internal structure of these lists and
how they work.
1. Node: The basic building block of a singly linked list is a node, which consists of two main parts:
2. Head: The head is a special pointer that points to the first node in the list. Without the head, the
list would be inaccessible.
3. Tail: The tail is the last node in the list, where the Next pointer is NULL, signaling the end of the
list.
4.
Traversal: Starting from the head, the list is traversed node by node using the Next pointer. This
sequential nature means you can only access elements in order.
Diagram Of Singly Linked List
Here:
#include <iostream>
using namespace std;
class Node {
public:
int data; // To store data
Node* next; // Pointer to the next node
// Constructor
Node(int value) {
data = value;
next = nullptr; // Initialize next as NULL
}
};
int main() {
// Creating nodes
Node* node1 = new Node(10);
Node* node2 = new Node(20);
// Linking nodes
node1->next = node2;
// Accessing data
cout << "Data in Node 1: " << node1->data << endl;
cout << "Data in Node 2: " << node1->next->data << endl;
return 0;
}
Output:
Data in Node 1: 10
Data in Node 2: 20
Code Explanation:
1. We create a class called Node containing two data members: data (an integer data type variable to
store the value of the node.) and next (a pointer variable of type Node* to store the address of the
next node in the list).
1. Both members are defined using the public access specifier, meaning they can be accessed directly
throughout the program.
2. We then define a class constructor (also public) to initialize the data member with the
given value and set next to nullptr.
In the main() function, we create two nodes, node1 and node2. Each node is an instance of the
Node class, initialized with a data value and a next pointer set to nullptr by default.
Next, we update the next pointer of node1 to point to node2, creating a link between the two
nodes.
We can then traverse the list starting from node1 and access the data values stored in each node.
Next, we’ll explore insertion operations in single linked lists in data structures to see how nodes are added at different positions.
#include <iostream>
using namespace std;
class Node {
public:
int data;
Node* next;
Node(int value) {
data = value;
next = nullptr;
}
};
int main() {
Node* head = nullptr; // Initially, the list is empty
insertAtBeginning(head, 30);
insertAtBeginning(head, 20);
insertAtBeginning(head, 10);
Just like in the previous example, we begin by importing the iostream header file for input/output operations. We then define a Node class
with two data members and a constructor.
1. Helper Functions: We define two functions: insertAtBeginning(, which adds a new node at the
start of the list, and printList() to traverse the list and print the content.
2. Here, the printList() function uses a while loop to check if the pointer is not null, in which case it
prints the content.
3. Dynamic Node Creation: Inside insertAtBeginning(), a new node is dynamically created using the
Node constructor and initialized with newData.
4. Pointer Update: Then, the next pointer of the newly created node is assigned the current head
node, effectively linking it to the existing list.
5. Head Update: After that, the head pointer is updated to point to the new node, making it the first
node in the list.
6.As a result, the new node becomes the head of the list, and the previous nodes are shifted down in
sequence.
Insertion At End Of Singly Linked List
Inserting a node at the end of a singly linked list involves adding a new node as the last element. The next pointer of the current last node is
updated to point to the new node, and the new node's next pointer is set to nullptr. Here is how this works:
#include <iostream>
using namespace std;
class Node {
public:
int data;
Node* next;
Node(int value) {
data = value;
next = nullptr;
}
};
void insertAtEnd(Node*& head, int newData) {
// Step 1: Create a new node
Node* newNode = new Node(newData);
// Step 2: Check if the list is empty
if (head == nullptr) {
head = newNode; // Make the new node the head
return;
}
// Step 3: Traverse to the last node
Node* temp = head;
while (temp->next != nullptr) {
temp = temp->next;
}
// Step 4: Update the last node's next pointer
temp->next = newNode;
}
int main() {
Node* head = nullptr; // Initially, the list is empty
insertAtEnd(head, 10);
insertAtEnd(head, 20);
insertAtEnd(head, 30);
Similar to the previous example, we have a Node class with two data members, a constructor and a printList() function to traverse and print
the list contents.
1. Function to Add Node: We also define an insertAtEnd() function to insert a new node at the end of
the list.
2. Node Creation: Inside insertAtEnd(), a new node is dynamically created using the Node
constructor and initialized with newData.
3. Empty List Check: Then, we use an if-statement to check if the list is empty (i.e.,the head is
nullptr). If it is true (the list is empty), the head pointer is set to the new node.
4. Traverse to Last Node: If the list is not empty, a temporary pointer (temp) traverses the list until
it reaches the last node (where temp->next == nullptr).
5. Linking New Node: We then update the next pointer of the last node to point to the new node,
linking it to the end of the list.
6.Pointer Update: The next pointer of the new node is set to nullptr, indicating that it is the last
node.
Insertion At A Specific Position In Singly Linked List
Inserting a node at a specific position in a singly linked list involves placing the new node at a user-defined position. The list can be
traversed until the desired position is reached, and then the new node is inserted between two existing nodes. This operation requires
updating the next pointer of the node just before the insertion point. Here is how it works:
#include <iostream>
using namespace std;
class Node {
public:
int data;
Node* next;
Node(int value) {
data = value;
next = nullptr;
}
};
int main() {
Node* head = new Node(10); // Starting with a linked list containing a single node
head->next = new Node(20); // Adding another node
return 0;
}
Output:
We begin with defining the Node class and then two functions: printList() to traverse and display the list, and insertAtPosition() to add a
node at a specific position.
1. Insertion at Position 0: Inside the insertAtPosition() function, we first check if the specified
position is 0. If it is, we treat that as insertion at the beginning of the list.
2. Insertion at Other Positions: If the specified position is greater than 0, we traverse the list to find
the node just before the specified position.
1. We start from the head and use a while loop to move through the list until we reach the
node at the position position - 1.
2.This is done by incrementing the currentPos and updating temp to point to the next node
in each iteration.
3. Inserting the New Node: Once we find the node just before the desired position, we:
1. Set newNode->next to temp->next, which points to the node that will come after the new
node.
2. Update temp->next to point to the new node, effectively inserting it into the list at the
specified position.
4. Out of Bounds Check: If we reach the end of the list (i.e., temp is nullptr before reaching the
desired position), it indicates that the position is out of bounds. In this case, a message is printed:
"Position is out of bounds!".
5. In the main() function, we create a new node using the constructor of the Node class, which
initializes the new node with the data newData. The next pointer of this new node is set to nullptr
by default.
6. Initial List: We start with a linked list containing two nodes: 10 -> 20 -> NULL.
7. Insert at Position 1: The insertAtPosition() function inserts the new node with value 15 between
10 and 20. It achieves this by traversing the list until it reaches the node just before position 1,
and then adjusting the next pointers accordingly.
This example shows how to start with an existing linked list and insert a new node at a specific position. It gives a clearer picture of how
insertion works in a more realistic scenario.
Boost your learning curve with one-on-one mentorship from industry professionals.
Deletion Operation On Singly Linked List
In a singly linked list, deletion operations involve removing a node from a specific location—either the beginning, end or a specified
position within the list. Deleting a node requires careful handling of pointers to ensure the integrity of the list.
Code Example:
#include <iostream>
using namespace std;
class Node {
public:
int data;
Node* next;
Node(int value) {
data = value;
next = nullptr;
}
};
int main() {
Node* head = new Node(10);
head->next = new Node(20);
head->next->next = new Node(30);
return 0;
}
Output:
1. We check if the list is empty (i.e., head == nullptr). If so, we print an error message and exit the
function.
2. If the list is not empty, we store the current head node in the temp pointer.
3. We update the head pointer to point to the next node in the list (head = head->next),
effectively removing the first node.
4. F i n a l l y , w e d e l e t e t h e o r i g i n a l h e a d n o d e u s i n g d e l e t e t e m p .
This operation is O(1) in terms of time complexity, as it only involves updating the head pointer and deleting the first node.
Code Example:
#include <iostream>
using namespace std
class Node {
public:
int data;
Node* next;
Node(int value) {
data = value;
next = nullptr;}
};
return 0;
}
Output:
We define the Node class and the printList() function just like before. Then, we define the deleteFromEnd() function:
1. Delete from the End Function: The deleteFromEnd() function first checks if the list is empty or
contains only one node.
2. If the list contains only one node, it deletes the head node and sets the head to nullptr.
If the list contains more than one node:
Code Example:
#include <iostream>
using namespace std
class Node {
public:
int data;
Node* next;
Node(int value) {
data = value;
next = nullptr;}
};
int main() {
Node* head = new Node(10);
head->next = new Node(20);
head->next->next = new Node(30);
head->next->next->next = new Node(40);
return 0;
}
Output:
We have a Node class to create nodes, and a printList() function to traverse an print the contents of a linked list.
1. Delete from a Specific Position: We also define a function to delete the node from a specific
position.
2. The deleteAtPosition() function first checks if the list is empty. If the position is 0, it deletes the
head node in the same way as the deletion at the beginning.
3. If the position is greater than 0:
1. We traverse the list to find the node just before the specified position.
2. Once the node before the target is found, we update its next pointer to skip the node at the
target position.
3. W e t h e n d e l e t e t h e n o d e a t t h e t a r g e t p o s i t i o n .
If the position is invalid (greater than the number of nodes), an out-of-bounds message is printed.
In the main() function, we create a linked list with four nodes and then print the same.
Next, we call the deleteAtPosition() function with arguments head and 2, i.e., to delete the node at
position 2.
The function deletes the respective node and shifts the previous one ahead. We display the new list
for comparison.
Want to become an expert? Dive into this curated course on DSA and advance your career.
Searching For Elements In Single Linked List
Searching in a singly linked list involves looking for a specific node that contains a given value. The search operation typically starts from
the head node and traverses the list node by node until it finds the target value or reaches the end of the list.
How It Works:
1. Start from the Head: We begin at the head node and check if it contains the desired value.
2. Traverse the List: If the current node does not contain the desired value, we move to the next node
and repeat the process until we either find the value or reach the end of the list (when the next
pointer is nullptr).
3. Return Result: If the value is found, we return the node or a true indication. If the end of the list
is reached and the value isn't found, we return false or an appropriate message.
Code Example:
#include <iostream>
using namespace std
class Node {
public:
int data;
Node* next;
Node(int value) {
data = value;
next = nullptr;}
};
int main() {
Node* head = new Node(10);
head->next = new Node(20);
head->next->next = new Node(30);
return 0;
}
Output:
Just like before, we define a class Node for node creation and a printList() function to traverse and print the linked list.
1. Search Function: We also define a search() function that takes the head of the list and the target
value as arguments. It iterates over each node in the list.
1. If the value of the current node (temp->data) matches the target, the function returns true,
indicating the element is found.
2. If the loop reaches the end of the list (temp == nullptr) without finding the target, it
returns false.
In the main() function, we create a linked list with values 10, 20, and 30, and then search for the
element 20.
The program will output that the value is found.
This search operation has a time complexity of O(n), where n is the number of nodes in the list.
How It Works:
#include <iostream>
using namespace std
class Node {
public:
int data;
Node* next;
Node(int value) {
data = value;
next = nullptr;}
};
int main() {
Node* head = new Node(10);
head->next = new Node(20);
head->next->next = new Node(30);
return 0;
}
Output:
We have the Node class for node creation and printList() to display the linked list.
1. Calculate Length Function: Then, we define the calculateLength() function, which initializes a
length variable to 0 and starts at the head node.
1. It traverses the list node by node, incrementing the length for each node it encounters.
2. O n c e t h e e n d o f t h e l i s t i s r e a c h e d ( i . e . , t e m p = = n u l l p t r ) , i t r e t u r n s t h e l e n g t h o f t h e l i s t .
In the main() function, we create a linked list with three nodes, and then calculate the length using
the calculateLength() function.
The result is 3, since there are three nodes in the list.
This operation has a time complexity of O(n), where n is the number of nodes in the list.
1. Implementing Stacks and Queues: These data structures can be implemented efficiently using
singly linked lists. The dynamic nature of SLLs allows easy push and pop operations (for stacks)
or enqueue and dequeue operations (for queues) without worrying about resizing, as you would
with arrays.
2. Memory Management: In memory management, linked lists are used to represent free blocks of
memory. When a block of memory is freed, it is added to a free list (a linked list), and when
memory is allocated, it is taken from the free list.
3. Dynamic Data Allocation: In applications where the size of the data cannot be determined in
advance or changes frequently, single linked lists (SLLs) provide an efficient way to handle this.
They allow for adding or removing data dynamically without the need to allocate or resize a
contiguous block of memory.
4. Polynomials: They are often represented using linked lists, where each node contains a coefficient
and an exponent. This allows for efficient addition, subtraction, and multiplication of polynomials,
especially when the number of terms varies.
5. Graph Representation: Singly linked lists are often used to represent adjacency lists in graph
data structures. Each vertex in the graph has a linked list of adjacent vertices, making it easy to
store and traverse a sparse graph.
6. Implementing Hash Tables: In hash table implementation using chaining, each bucket can be
implemented using a linked list. This helps in handling collisions efficiently when multiple keys
hash to the same bucket.
7. Sparse Matrices: Singly linked lists are useful for storing sparse matrices, where most of the
elements are zero. Rather than allocating memory for every element, linked lists allow efficient
storage of only non-zero elements.
8. Undo Mechanism: In software applications like text editors or drawing tools, linked lists can be
used to store a history of operations. By using a linked list to represent actions, the system can
easily undo or redo previous operations.
Common Problems With Singly Linked Lists
While singly linked lists are a powerful data structure, they come with certain challenges and potential pitfalls. Understanding these
common issues helps in building robust solutions and avoiding common mistakes.
1. Memory Leaks: Failing to properly deallocate memory when nodes are removed can lead to
memory leaks.
Solution: Always ensure that you free memory when nodes are deleted or no longer needed. In C+
+, this can be done using delete for each node that is removed.
2. Traversal Time Complexity: Accessing elements in a singly linked list requires traversal from the
head to the desired position, leading to O(n) time complexity for searches, insertions, and
deletions.
Solution: For more efficient access, consider using other data structures like arrays or doubly
linked lists, where backward traversal is also possible.
3. Difficulty with Backtracking: Singly linked lists only allow traversal in one direction. Once you
move past a node, you cannot go back to it without restarting from the head.
Solution: If you need frequent backward traversal, a doubly linked list may be more appropriate,
as it allows traversal in both directions.
4. Insertion at Specific Positions: Inserting an element at a specific position in a singly linked list
requires traversal to that position, which can be inefficient if the position is near the end of the
list.
Solution: If insertion at specific positions is a frequent operation, consider using other data
structures like a doubly linked list or an array-based structure for faster access.
5. Difficulty in Reversing the List: Reversing a singly linked list involves changing the direction of
the pointers for each node, which requires an additional traversal and careful handling of pointers
to avoid breaking the list.
Solution: Use a well-defined algorithm for reversing the list. For instance, iteratively reverse the
list by updating the pointers from the head to the end.
6. Limited Use of Indexing: Unlike arrays, linked lists don’t support indexing, meaning direct
access to a node by position is not possible.
Solution: If you need frequent random access to elements, an array or vector might be more
efficient.
7. Complicated Deletion: Deleting nodes in a singly linked list, especially when dealing with
deletion at the middle or end of the list, can be tricky as it requires keeping track of the previous
node in order to update the next pointer.
Solution: Properly manage pointers during deletion to ensure the list remains intact. When
deleting from the middle or end, handle the previous node’s next pointer carefully.
20 mins read
Ever tried flipping a stack of pancakes without making a mess? Reversing a linked list is kind of like that–except without the risk of syrup
stains.
In this blog, we’ll explore how to reverse a linked list using three different approaches: recursive, iterative, and stack-based. Whether you’re
prepping for coding interviews or just strengthening your DSA fundamentals, this article will give you a clear and structured understanding
of the problem and its solutions.
1. Base Case: If the linked list is empty or has only one node, return the head as it’s already
reversed.
2. Recursive Step: Recursively reverse the rest of the list, starting from the second node.
3. Reverse Pointers: Update the next node’s pointer to point back to the current node.
4. Fix the Last Node: Ensure the original head’s next pointer is set to NULL to mark the new end.
Visual Representation Of Recursive Approach To Reverse A Linked List
Before Recursion:
head → [10 | *] → [20 | *] → [30 | *] → NULL
During Recursion (Breaking Down)
The function keeps calling itself until it reaches the base case:
struct Node {
int data;
Node* next;
Node(int val) : data(val), next(NULL) {}
};
return newHead;
}
// Driver code
int main() {
Node* head = new Node(10);
head->next = new Node(20);
head->next->next = new Node(30);
cout << "Original List: ";
printList(head);
head = reverseRecursive(head);
cout << "Reversed List: ";
printList(head);
return 0;
}
Output:
In the example above, we first include the header file <iostream> for input/output operations and use the std namespace to use standard
library functions directly.
Structure For Linked List: We define a structure Node containing data (and integer data type
variable member to store the node's value), next (a pointer variable that points to the next node),
and constructor Node(int val) to initialize the node with a given value and set next to NULL.
Recursive Function: Then, we define the C++ function reverseRecursive(Node* head). Inside the
function, we define the base case and recursive call as follows:
Base Case: We use an if-statement to check if the head node is NULL.
o If head == NULL (empty list) or head->next == NULL (only one node exists), return head
as the list is already reversed.
Recursive Call: The function recursively moves to the last node by calling reverseRecursive(head-
>next).
o Once it reaches the last node, the recursion starts unwinding, and nodes begin re-linking
in reverse order.
Rewiring the Pointers: After every recursive call, we modify the next pointer of the next node to
point back to the current node:
o head->next->next = head;– This makes the next node’s next pointer (which was
previously NULL) point back to head.
o We then set head->next = NULL; to ensure that the old head (which becomes the last node
in the reversed list) no longer points to the next node.
Returning the New Head: The function returns newHead, which is the last node of the original
list but becomes the new head of the reversed list.
Main Execution: We create a linked list in the main() function by first initializing the head node
using new Node(10).
Then we add subsequent nodes by setting head->next = new Node(20); and head->next->next = new
Node(30);.
Next, we print the original linked list using the printList(head) function, which traverses the list
and prints its elements in order.
We then call the function reverseRecursive(head), which reverses the linked list recursively and
updates head with the new head of the reversed list.
Finally, we print the reversed list using printList(head) again to display the modified order of
elements.
This ensures that the linked list is reversed in place with a time complexity of O(n) and space complexity of O(n) due to the recursive call
stack.
def reverse_recursive(head):
if not head or not head.next:
return head
new_head = reverse_recursive(head.next)
head.next.next = head
head.next = None
return new_head
def print_list(head):
while head:
print(head.data, end=" -> ")
head = head.next
print("NULL")
# Driver code
head = Node(10)
head.next = Node(20)
head.next.next = Node(30)
head = reverse_recursive(head)
print("Reversed List:", end=" ")
print_list(head)
Code Explanation:
In the Python program example, we define a class to create the linked list, a recursive function to reverse it, and a print function (print_list())
to display the linked lists.
Class Definition: We define a Node class with attributes data (to store the value) and next (to
point to the next node). This will be used to create the Python linked list.
Recursive Function (reverse_recursive): We define a Python function to reverse the linked list.
Inside, we have:
o Base Case: Using an if-statement, we check if the head node is None or only one node
exists. In either situation, the function returns the head.
o
Recursive Call: If the base is unmet, the function recursively moves to the last node
using reverse_recursive(head.next).
Rewiring Pointers: After each recursive call, the function makes the next node point back to head
(head.next.next = head) and breaks the old link (head.next = None).
Main Execution: We create a linked list containing three nodes, 10, 20, and 30, and link them.
We then print a string message to the console, and then the linked list is printed without the
newline (using the end parameter).
After that, we reverse the linked list using reverse_recursive(head) and then print the reversed list.
Recursive Approach To Reverse A Linked List Java Example
class Node {
int data;
Node next;
Node(int data) {
this.data = data;
this.next = null;
}
}
return newHead;
}
System.out.println("NULL");
}
head = reverseRecursive(head);
System.out.print("Reversed List: ");
printList(head);
}
}
Code Explanation:
Class Definition: We define a Node class with attributes data (to store value) and next (to point to
the next node). This will be used to create the linked list.
Recursive Function (reverseRecursive): Then, we define a recursive function to reverse a linked
list. Inside the function:
Base Case: If the head is null or only one node exists, return the head.
Recursive Call: Call reverseRecursive(head.next) to reach the last node.
Rewiring Pointers: head.next.next = head; makes the next node point back to head, and head.next
= null; removes the old link.
Main Execution: We create a linked list, print it, call reverseRecursive(head) to reverse it, and
then print the reversed list.
Iterative Approach To Reverse A Linked List
Think of reversing a linked list iteratively, like flipping a stack of playing cards one by one—each card (node) needs to be placed in a new
order by adjusting its connections without losing track of the next one. Instead of relying on recursive calls, we systematically move through
the list, reversing the links as we go.
Node(int val) {
data = val;
next = NULL;
}
};
// Main function
int main() {
return 0;
}
Output:
In the C++ code example, we include the essential header and use the namespace.
Structure For Linked List: Then, we define a structure Node containing: data (an integer to store
the node’s value), next (a pointer to the next node in the list), and a constructor Node(int val)
initializes the node with a given value and sets next to NULL.
Iterative Function (reverseIterative): We then define a function to reverse a linked list.
The function takes head as input and reverses the linked list in place using three pointers:
o prev (initially NULL) to keep track of the reversed portion.
o curr (starting at head) to process the current node.
o next to temporarily store the next node before breaking the link.
Pointer Reversal Process: We then use a while loop to reverse the points.
o Store curr->next in next (to avoid losing access to the next node).
o Reverse the link by setting curr->next = prev.
o Move prev and curr forward by one node.
oThe loop continues until curr becomes NULL. At this point, prev holds the new head of
the reversed list, which is returned.
Main Execution: We create a linked list is created in main() by initializing the head node (10),
followed by linking 20 and 30.
Then, we print the original linked list using printList(head).
Next, we call the function reverseIterative(head) to reverse the list, and the new head is assigned
back to head.
The reversed linked list is printed again to display the modified order of elements.
This approach efficiently reverses the linked list in O(n) time and O(1) space, as it modifies pointers directly without extra recursion stack
space.
while curr:
next_node = curr.next # Store the next node
curr.next = prev # Reverse the pointer
prev = curr # Move prev to current
curr = next_node # Move curr to next node   Â
return prev # New head of the reversed list
# Main execution
head = Node(10)
head.next = Node(20)
head.next.next = Node(30)
head = reverse_iterative(head)
Class Definition: We first define a class Node with the constructor __init__(), which initializes
class members: data, and next (default None). We will use this to create the linked list.
Function Definition: Then we define a function reverse_iterative() to reverse a linked list
iteratively. Inside the function, we first initialize three pointers:
Node(int val) {
data = val;
next = null;
}
}
Class Definition: We define a class Node with three members: data (int variable to store the
value), next (a pointer to store the reference to the next node), and a constructor Node(int val) to
initialize the node with a value and set next = null.
Function Definition (reverseIterative): Then we define a function that reverses a linked list
iteratively using three pointers:
o prev (initially null) → Tracks the previous node.
o curr (initially head) → The current node being processed.
o next (stores the next node before breaking the link).
Reversal Logic: Then, we use a while loop to traverse the nodes until curr becomes null (curr !=
null).
In each iteration, the loop saves the next node using next = curr.next before modifying links.
Reverse the Link: Point the current node (curr.next) to the previous node (prev), breaking its
original forward connection. Move Pointers Forward:
o Set prev = curr (move prev to the current node).
o Set curr = next (move curr to the next node).
The loop continues until curr becomes null, meaning all nodes are reversed.
Finally, return prev, which now holds the new head of the reversed linked list.
Main Execution: We first create a linked list with three nodes and link them (10 → 20 → 30).
We then print the original list using printList(head).
Next, we reverse the list calling reverseIterative(head), which finally updates head.
Lastly, we print the reverse list to the console.
Time Complexity: O(n) | Space Complexity: O(1)
Using Stack To Reverse A Linked List
A stack follows the Last In, First Out (LIFO) principle, making it a great tool for reversing a linked list. Instead of modifying pointers
directly, we push all nodes onto a stack and then pop them in reverse order to reconstruct the list.
struct Node {
int data;
Node* next;
Node(int val) {
data = val;
next = NULL;
}
};
stack<Node*> nodeStack;
Node* temp = head;
int main() {
Node* head = new Node(10);
head->next = new Node(20);
head->next->next = new Node(30);
return 0;
}
Output:
Original List: 10 20 30
Reversed List: 30 20 10
Code Explanation:
Node Structure: We define a Node structure with data (stores value) and next (points to the next
node).
o It also contains a constructor that initializes a new node with a given value and sets next
= NULL.
o We will use this to create a linked list that we will then reverse.
Function reverseUsingStack(Node* head): Then, we define a function to reverse a linked list as
follows:
o We first check if the list is empty (head == NULL) or has only one node (head->next ==
NULL). In this case the function returns the head as it's already reversed.
o If the condition for the if-statement is not met, we create a stack of Node pointers
(nodeStack) to store all nodes.
Push All Nodes onto the Stack: Then we create a temporary pointer temp to traverse the list and
assign the head node to it.
Then, as mentioned in code comments, we push each node onto the stack (nodeStack.push(temp)).
Pop Nodes and Reconstruct the Reversed List:
o Set head = nodeStack.top() → The last node pushed becomes the new head.
o Pop nodes from the stack and reassign next pointers to reverse the links.
o Stop when the stack is empty and ensure the last node's next is set to NULL to mark the
end of the reversed list.
Main Execution: We create a linked list with three nodes linked together (10 → 20 → 30) and
print it using printList(head).
Then, we call reverseUsingStack(head) to reverse the list and print that to the console.
This ensures a time complexity of O(n) (traversing and pushing each node once) and a space complexity of O(n) (due to stack storage).
def reverse_using_stack(head):
if not head or not head.next:
return head
stack = []
temp = head
head = reverse_using_stack(head)
Node Class: We define a Node class with attributes data (stores the value) and next (points to the
next node). It also has a constructor that initializes data and sets next to None.
Function reverse_using_stack(head): Then, we have a function to reverse a linked list as follows:
o Using an if-statement, we check if the list is empty or has one node. In either case the
functionr returns head.
o If the condition is not met, we use a list as a stack to store node references.
Push All Nodes onto the Stack: Then, we traverse the list using a while loop, and in every
iteration, we push each node onto the stack using the append() function.
Pop Nodes and Reconstruct the Reversed List:
o Pop the top node and set it as the new head.
o Continue popping and updating next pointers to reconstruct the reversed list.
Ensure the last node's next is set to None to mark the end.
Main Execution: Then, we create a linked list with three nodes (10 → 20 → 30) and print it using
print_list(head).
Next, we call the function reverse_using_stack(head) to reverse the list and also print it to the
console.
Stack-based Approach To Reverse A Linked List Java Example
import java.util.Stack;
class Node {
int data;
Node next;
Node(int val) {
data = val;
next = null;
}
}
return head;
}
head = reverseUsingStack(head);
Code Explanation:
Node Class: Defines a Node class with attributes data (stores the value) and next (points to the
next node).
o It also contains a constructor that initializes data and sets next to null.
Function reverseUsingStack(Node head): We define a function that reverses a linked list as
follows:
o We first check if the list is empty or has one node. If so, it returns head.
o If not, we create a Stack to store node references.
Push All Nodes onto the Stack: Then we traverse the list using while loop and push each node
onto the stack.
Pop Nodes and Reconstruct the Reversed List:
o Pop the top node and set it as the new head.
o Continue popping and updating next pointers to reconstruct the reversed list.
o Ensure the last node's next is set to null to mark the end.
Main Execution: We create a linked list (10 → 20 → 30) and print it to the console
printList(head).
Then, we reverse the linked list using reverseUsingStack(head) and print it.
Complexity Analysis Of Different Approaches To Reverse A Linked List
Each method to reverse a linked list has different time and space complexities. Below is a comparison of the three approaches:
Recursive O(n) O(n) Each recursive call adds a function to the call stack.
Iterative O(n) O(1) Uses only a few pointers without extra memory.
Using Stack O(n) O(n) Requires extra stack memory to store all nodes.
Key Takeaways:
1. Fastest Approach: All three methods run in O(n) time complexity since every node is processed
once.
2. Most Memory-Efficient:
Iterative Approach (O(1) space) is the best as it does not use additional memory beyond a
few pointers.
Recursive Approach (O(n) space) is inefficient for large lists due to function call stack
usage.
Stack-Based Approach (O(n) space) is also memory-heavy since it stores all nodes in an
explicit stack.
12 mins read
A stack is a fundamental data structure that follows the Last In, First Out (LIFO) principle, meaning the last element added is the first to be
removed. Imagine a stack of plates—when you add a new plate, it goes on top, and when you remove one, you take the topmost plate first.
In this article, we'll explore the concept of a stack, its operations (push, pop, peek, and isEmpty), its implementation using arrays and linked
lists, and its real-world applications in areas like recursion, expression evaluation, and undo/redo functionality.
This is exactly how a stack works—the last plate placed (pushed) is the first one taken (popped).
1. Push (Insertion)
The Push operation adds an element to the top of the stack. If the stack is implemented using an array and is full, we call this a stack
overflow.
Example:
Imagine we have an empty stack, and we push elements 10, 20, and 30 onto it.
1 Push(10) 10
2 Push(20) 20 → 10
3 Push(30) 30 → 20 → 10
Example (C++ using an array):
Example:
Continuing from the previous stack (30 → 20 → 10):
1 Pop() 20 → 10
2 Pop() 10
3 Pop() (Empty)
Example (C++ using an array):
void pop() {
if (top == -1) {
cout << "Stack Underflow!\n";
return;
}
cout << arr[top--] << " popped from stack\n"; // Remove top element
}
3. Peek (Top Element)
The Peek (Top) operation returns the top element without removing it.
Example:
If the stack is 30 → 20 → 10, peek() will return 30.
int peek() {
if (top == -1) {
cout << "Stack is empty!\n";
return -1;
}
return arr[top]; // Return top element
}
4. isEmpty (Check if Stack is Empty)
The isEmpty operation checks whether the stack contains any elements.
bool isEmpty() {
return (top == -1); // Returns true if stack is empty
}
Stack Implementation In Data Structures
Stacks can be implemented using two approaches:
#include <iostream>
#define MAX 5 // Maximum size of stack
using namespace std;
class Stack {
int top;
int arr[MAX]; // Stack array
public:
Stack() { top = -1; } // Constructor initializes top
// Push operation
void push(int value) {
if (top == MAX - 1) {
cout << "Stack Overflow!\n";
return;
}
arr[++top] = value; // Increment top and add element
cout << value << " pushed to stack\n";
}
// Pop operation
void pop() {
if (top == -1) {
cout << "Stack Underflow!\n";
return;
}
cout << arr[top--] << " popped from stack\n"; // Remove top element
}
// Peek operation
int peek() {
if (top == -1) {
cout << "Stack is empty!\n";
return -1;
}
return arr[top];
}
int main() {
Stack s;
s.push(10);
s.push(20);
s.push(30);
cout << "Top element: " << s.peek() << endl;
s.pop();
cout << "Top element after pop: " << s.peek() << endl;
return 0;
}
Output:
10 pushed to stack
20 pushed to stack
30 pushed to stack
Top element: 30
30 popped from stack
Top element after pop: 20
Explanation:
In the above code example-
1. We include <iostream> header to enable input and output operations and define MAX as 5, which
represents the maximum size of the stack.
2. The Stack class has two private members: top, which tracks the top index, and arr[MAX], an
array that stores stack elements.
3. The constructor initializes top to -1, indicating that the stack is empty initially.
4. The push function checks if the stack is full (top == MAX - 1). If it is, we print "Stack
Overflow!". Otherwise, we increment top and insert the new value.
5. The pop function checks if the stack is empty (top == -1). If so, we print "Stack Underflow!".
Otherwise, we remove and display the topmost element.
6. The peek function returns the top element if the stack is not empty; otherwise, it prints "Stack is
empty!" and returns -1.
7. The isEmpty function checks whether the stack is empty by returning true if top == -1.
8. In main(), we create a Stack object s, push three values (10, 20, 30), and display the top element
using peek().
9. After popping an element, we check the top element again, demonstrating the Last-In-First-Out
(LIFO) behavior of the stack.
Stack Implementation Using Linked Lists
A linked list-based stack dynamically allocates memory as needed, overcoming the fixed size limitation of arrays.
#include <iostream>
using namespace std;
// Node structure
class Node {
public:
int data;
Node* next;
Node(int value) {
data = value;
next = nullptr;
}
};
class Stack {
Node* top;
public:
Stack() { top = nullptr; } // Constructor initializes top as NULL
// Push operation
void push(int value) {
Node* newNode = new Node(value);
newNode->next = top; // New node points to old top
top = newNode; // Update top to new node
cout << value << " pushed to stack\n";
}
// Pop operation
void pop() {
if (top == nullptr) {
cout << "Stack Underflow!\n";
return;
}
Node* temp = top;
top = top->next; // Move top pointer to next node
cout << temp->data << " popped from stack\n";
delete temp; // Free memory
}
// Peek operation
int peek() {
if (top == nullptr) {
cout << "Stack is empty!\n";
return -1;
}
return top->data;
}
int main() {
Stack s;
s.push(10);
s.push(20);
s.push(30);
cout << "Top element: " << s.peek() << endl;
s.pop();
cout << "Top element after pop: " << s.peek() << endl;
return 0;
}
Output:
10 pushed to stack
20 pushed to stack
30 pushed to stack
Top element: 30
30 popped from stack
Top element after pop: 20
Explanation:
In the above code example-
1. We include <iostream> to enable input and output operations and use the std namespace for
convenience.
2. The Node class represents a stack node, containing an integer data and a pointer next that links to
the next node.
3. The Node constructor initializes data with the given value and sets next to nullptr.
4. The Stack class has a private pointer top, which tracks the top node of the stack.
5. The constructor initializes top as nullptr, indicating an empty stack.
6. The push function creates a new node, sets its next pointer to the current top, and updates top to
point to this new node.
7. The pop function checks if the stack is empty (top == nullptr). If so, we print "Stack
Underflow!". Otherwise, we store top in a temporary pointer, move top to the next node, print the
popped value, and free the memory of the removed node.
8. The peek function returns the value of top if the stack is not empty; otherwise, it prints "Stack is
empty!" and returns -1.
9. The isEmpty function checks whether the stack is empty by returning true if top == nullptr.
10. In main(), we create a Stack object s, push three values (10, 20, 30), and display the top element
using peek().
11. After popping an element, we check the top element again, demonstrating the Last-In-First-Out
(LIFO) behavior of the stack.
Memory Usage Can waste memory if underutilized Efficient, uses only required memory
Insertion/Deletion Speed Fast (direct index access) Slower (requires pointer traversal)
Stack Overflow Risk Yes (if the array is full) No (until system memory is exhausted)
Best Use Case When size is known and fixed When size is unpredictable or varies
Therefore:
Array-based stacks are simpler and faster but suffer from fixed size limitations.
Linked list-based stacks offer dynamic memory allocation and avoid overflow but require extra
memory for pointers and are slightly slower.
For applications with a known, limited size, array-based stacks are efficient. If the stack size is dynamic, linked list-based stacks are
preferred.
When a function is called, its execution details (such as local variables, return address, etc.) are pushed onto the stack. When the function
completes, it is popped from the stack. This mechanism enables recursion to work properly.
2. Expression Evaluation
Use Case: Evaluating infix, prefix, and postfix expressions.
Stacks are used in expression conversion (infix to postfix) and expression evaluation (postfix evaluation).
Example: Postfix Expression Evaluation
A postfix expression like 3 4 + 2 * is evaluated using a stack:
1. Push 3, push 4.
2. Encounter + → Pop 3 and 4, compute 3 + 4 = 7, push 7.
3. Push 2.
4. Encounter * → Pop 7 and 2, compute 7 * 2 = 14.
Final result: 14.
3. Undo/Redo Operations
Use Case: Used in text editors and software to track changes.
Undo: Stores previous states of a document in a stack. Pressing "Undo" pops the last state.
Redo: Uses another stack to store undone operations. Pressing "Redo" pushes back the popped
state.
4. Backtracking (Maze Solving, DFS in Graphs)
Use Case: Used in solving mazes and Depth-First Search (DFS) in graphs.
Backtracking involves exploring a path, and if it fails, reverting to the previous step (LIFO principle).
Back Button: Pops the last visited page from the stack.
Forward Button: Pushes the popped page onto a redo stack.
Advantages And Disadvantages Of Stack Data Structure
Like every data structure, a stack has its own advantages and limitations. Let's explore them in detail.
1. Simple and Efficient – Follows LIFO (Last In, First Out), making operations easy to implement.
2. Fast Operations – Push, pop, and peek take O(1) time complexity (very efficient).
3. Dynamic Memory Allocation – Linked list implementation avoids fixed size limitations of arrays.
4. Function Call Management – Used in recursion to store function execution states.
5. Expression Evaluation – Helps in evaluating infix, prefix, and postfix expressions .
6. Backtracking Support – Used in maze solving, DFS (Depth-First Search), and undo operations .
7. Reversing Data – Helpful in reversing strings, linked lists, and arrays .
Disadvantages Of Stack Data Structure
1. Fixed Size (Array Implementation) – If implemented with an array, size is limited and can
cause stack overflow.
2. No Random Access – Can only access the top element; middle elements are not directly
accessible.
3. Not Suitable for Frequent Modifications – Inefficient when inserting or deleting elements from
the middle.
4. Stack Overflow in Recursion – Too many recursive calls can lead to program crashes.
5. Extra Memory Usage (Linked List Implementation) – Linked list-based stacks need extra
space for pointers
Graph Data Structure | Types, Algorithms & More (+Code Examples)
A graph is a data structure consisting of vertices (nodes) and edges (connections) that represent
relationships. Graphs can be directed or undirected, weighted or unweighted, and are widely used in
networks, maps, and social connections.
17 mins read
In the world of computer science, a graph is more than just a visual representation—it's a fundamental data structure that captures
relationships between entities. Whether you're mapping social networks, optimizing delivery routes, or modeling web pages for search
engines, graphs are essential tools for solving complex, real-world problems.
In this article, we’ll explore the concept of graph data structures, their components, types, and practical uses. Additionally, we’ll understand
the implementation techniques and algorithms that bring graphs to life in programming.
1. Vertices (or nodes): These are the individual entities or points in the graph.
2. Edges (or arcs): These are the connections or relationships between the vertices. An edge can be
directed (one-way) or undirected (two-way).
In a graph, vertices represent objects, while edges represent the relationships between those objects. This structure can model various real-
world systems like social networks, transportation systems, or even computer networks.
Versatility: Graphs can model a wide variety of systems, from social networks to transportation
routes.
Dynamic Representation: They allow flexible representation of objects and their relationships,
adapting to changes in the real world.
Efficient Algorithms: Many algorithms for searching, finding the shortest path, and detecting
cycles in graphs are highly efficient and widely used in real-world applications.
Types Of Graphs In Data Structure
Graphs can be categorized in various ways based on their structure, directionality, and whether they have additional properties like weights.
Below are the most common types of graphs:
Definition: A directed graph is one in which edges have a specific direction. An edge from vertex
A to vertex B is represented as (A → B), meaning the edge points from A to B.
Key Feature: Each edge has a direction, indicating a one-way relationship.
Example: A Twitter network where a user follows another user.
2. Undirected Graph
Definition: In an undirected graph, edges do not have any direction. An edge between vertex A and
vertex B is simply represented as {A, B} or (A, B), meaning the relationship is bidirectional.
Key Feature: Edges represent mutual relationships, where if A is connected to B, B is also
connected to A.
Example: A Facebook friendship network where friendships are mutual.
3. Weighted Graph
Definition: A weighted graph is a graph in which each edge has a weight or cost associated with
it. The weight could represent distance, time, or any other measurable quantity.
Key Feature: Edges have weights that represent the "cost" or "distance" between vertices.
Example: A road network where the weight of an edge represents the distance between two cities.
4. Unweighted Graph
Definition: An unweighted graph is a graph where all edges are treated equally, and no weight is
assigned to any edge.
Key Feature: No weights are associated with edges.
Example: A simple social network where the edges represent connections but no additional cost or
relationship strength is considered.
5. Complete Graph (Kn)
Definition: A complete graph is one where there is a unique edge between every pair of vertices.
In other words, every vertex is connected to every other vertex.
Key Feature: Every pair of distinct vertices is connected by an edge.
Example: A fully connected network of people where everyone is connected to everyone else.
6. Bipartite Graph
Definition: A bipartite graph consists of two sets of vertices, and edges only exist between
vertices from different sets, not within a set.
Key Feature: Vertices can be divided into two disjoint sets, and edges only connect vertices from
different sets.
Example: A job allocation system where one set represents jobs and the other set represents
employees, with edges indicating which employee can do which job.
7. Cyclic Graph
Definition: A cyclic graph is one that contains at least one cycle, which is a path that starts and
ends at the same vertex, with no repeated edges or vertices along the way.
Key Feature: Contains at least one cycle.
Example: A road system where there’s a circular route connecting cities, allowing for infinite
loops.
8. Acyclic Graph
Definition: An acyclic graph is one that does not contain any cycles. There are no paths that begin
and end at the same vertex.
Key Feature: Contains no cycles.
Example: A tree structure, such as a family tree, which has no loops.
9. Tree
Definition: A tree is a special type of acyclic graph that is connected and does not have cycles. It
has a hierarchical structure with a single root node, from which all other nodes are descendants.
Key Feature: Acyclic, connected graph with a hierarchical structure.
Example: A file system directory structure where each folder contains files or other folders.
10. Forest
Definition: A forest is a disjoint collection of trees. It can be seen as a set of disconnected trees,
each being an acyclic graph.
Key Feature: A collection of trees (multiple disconnected components).
Example: A set of independent organizational charts within different departments of a company.
11. Planar Graph
Definition: A planar graph is one that can be drawn on a plane without any of its edges crossing.
Key Feature: Can be drawn on a plane without edge intersections.
Example: A map of cities where each road is represented as an edge, and no two roads cross each
other.
12. Sparse and Dense Graphs
Sparse Graph:
Definition: A sparse graph has relatively few edges compared to the number of vertices.
The number of edges is much smaller than the maximum possible number of edges.
Example: A graph representing a social network with a small number of connections.
Dense Graph:
Definition: A dense graph has a large number of edges, close to the maximum possible
edges.
Example: A fully connected graph, like a social network where most users are friends
with each other.
13. Directed Acyclic Graph (DAG)
Definition: A directed acyclic graph (DAG) is a directed graph that contains no cycles. It is often
used in scenarios where the graph represents a flow or process, such as task scheduling.
Key Feature: Directed edges with no cycles.
Example: A project dependency graph where tasks are connected by dependencies, and tasks must
be completed in a specific order.
14. Multi-Graph
Definition: A multi-graph allows multiple edges between two vertices. These multiple edges are
often called parallel edges.
Key Feature: Multiple edges between the same pair of vertices are allowed.
Example: A transportation network where multiple routes connect two cities.
Types Of Graph Algorithms
Graph algorithms are used to solve various problems related to graphs, such as finding the shortest path between vertices, finding minimum
spanning trees, and detecting negative weight cycles. Below are some of the most common graph algorithms:
1. Dijkstra’s Algorithm (Shortest Path)
Dijkstra's algorithm is used to find the shortest path from a source vertex to all other vertices in a weighted graph with non-negative edge
weights.
Algorithm Steps:
Initialize the distance to the source vertex as 0 and all other vertices as infinity.
Visit all vertices, and for each vertex, update the shortest distance using the current vertex’s
distance and edge weights.
Repeat until all vertices are visited.
Code Example:
#include <iostream>
#include <vector>
#include <set>
#include <climits>
while (!s.empty()) {
int u = s.begin()->second;
s.erase(s.begin());
int main() {
int V = 5; // Number of vertices
vector<vector<pair<int, int>>> adj(V);
adj[0].push_back({1, 10});
adj[0].push_back({4, 5});
adj[1].push_back({2, 1});
adj[2].push_back({3, 4});
adj[3].push_back({0, 7});
adj[4].push_back({1, 3});
adj[4].push_back({2, 9});
return 0;
}
Output:
Distance from 0 to 0 is 0
Distance from 0 to 1 is 8
Distance from 0 to 2 is 9
Distance from 0 to 3 is 13
Distance from 0 to 4 is 5
Explanation:
In this code, we implement Dijkstra's algorithm to find the shortest paths from a source vertex to all other vertices in a weighted graph.
1. Initialization:
1. We create a dist vector initialized with INT_MAX to represent infinite distances. The
distance from the source to itself is set to 0.
2.
A set is used to store vertices, prioritizing those with the smallest known distance (pair of
distance and vertex).
Main Algorithm:
1. We repeatedly pick the vertex with the smallest distance from the set (u), then remove it.
2. We visit each neighbor of u. If the distance through u to a neighbor v is shorter than the
current distance to v, we update it and insert the new distance into the set.
Output:
1. After the algorithm completes, we print the shortest distance from the source to each
vertex.
In the main() function, we define a graph with 5 vertices and their respective weighted edges. We then call the dijkstra() function to find
and print the shortest distances from vertex 0.
2. Bellman-Ford Algorithm
The Bellman-Ford algorithm can be used to find the shortest path from a source vertex to all other vertices in a weighted graph, including
graphs with negative edge weights. It can also detect negative weight cycles.
Algorithm Steps:
Initialize the distance to the source vertex as 0 and all others as infinity.
Relax all edges V-1 times, where V is the number of vertices.
Check for negative weight cycles by relaxing all edges one more time.
Code Example:
#include <iostream>
#include <vector>
#include <climits>
int main() {
int V = 5; // Number of vertices
vector<vector<int>> edges = {
{0, 1, -1}, {0, 2, 4}, {1, 2, 3}, {1, 3, 2}, {1, 4, 2},
{3, 2, 5}, {3, 1, 1}, {4, 3, -3}
};
Distance from 0 to 0 is 0
Distance from 0 to 1 is -1
Distance from 0 to 2 is 2
Distance from 0 to 3 is -2
Distance from 0 to 4 is 1
Explanation:
In this code, we implement the Bellman-Ford algorithm to find the shortest paths from a source vertex to all other vertices in a graph, while
also detecting negative weight cycles.
1. Initialization:
1.
We create a dist vector, initialized to INT_MAX, to represent infinite distances. The
distance from the source to itself is set to 0.
Relaxation:
1.
We relax all edges V-1 times (where V is the number of vertices). For each edge (u, v)
with weight w, if the distance to v through u is shorter than the current distance to v, we
update the distance to v.
Negative Weight Cycle Detection:
1. After relaxing all edges, we check each edge again to see if we can further reduce the
distance to any vertex. If so, it indicates the presence of a negative weight cycle in the
graph.
Output:
1. Finally, we print the shortest distances from the source vertex to all other vertices. If a
negative weight cycle is detected, a message is printed indicating its presence.
In the main() function, we define a graph with 5 vertices and the respective weighted edges. We then call the bellmanFord() function to
find and print the shortest distances from vertex 0, or detect a negative weight cycle if one exists.
3. Floyd-Warshall Algorithm
The Floyd-Warshall algorithm computes the shortest paths between all pairs of vertices in a weighted graph. It can handle negative weights
but cannot detect negative weight cycles.
Algorithm Steps:
#include <iostream>
#include <vector>
#include <climits>
// Main algorithm
for (int k = 0; k < V; ++k) {
for (int i = 0; i < V; ++i) {
for (int j = 0; j < V; ++j) {
if (dist[i][k] != INT_MAX && dist[k][j] != INT_MAX &&
dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
int main() {
int V = 4;
vector<vector<int>> graph = {
{0, 3, INT_MAX, INT_MAX},
{2, 0, INT_MAX, 4},
{INT_MAX, 7, 0, 1},
{INT_MAX, INT_MAX, 2, 0}
};
floydWarshall(V, graph);
return 0;
}
Output:
0397
2064
9701
11 9 2 0
Explanation:
In this code, we implement the Floyd-Warshall algorithm to find the shortest paths between all pairs of vertices in a weighted graph.
1. Initialization:
1.We create a dist matrix that initially holds the input graph, where dist[i][j] represents
the distance from vertex i to vertex j. If no direct edge exists between two vertices,
the distance is set to INT_MAX (infinity).
Main Algorithm:
1. The algorithm iterates through each possible intermediate vertex k and tries to improve
the distance between every pair of vertices (i, j) by checking if the path from i to j
through k is shorter than the current known distance. If it is, the distance is updated.
Output:
1.
After completing the algorithm, we print the shortest distances between every pair of
vertices. If the distance is INT_MAX, it is printed as "INF" to indicate no path exists
between the vertices.
In the main() function, we define a graph with 4 vertices and the respective edge weights. We then call the floydWarshall() function to
compute and print the shortest distances between all pairs of vertices.
Algorithm Steps:
struct Edge {
int u, v, weight;
bool operator<(const Edge& e) const {
return weight < e.weight;
}
};
if (rootX != rootY) {
if (rank[rootX] > rank[rootY])
parent[rootY] = rootX;
else if (rank[rootX] < rank[rootY])
parent[rootX] = rootY;
else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
vector<Edge> mst;
for (Edge e : edges) {
int u = e.u, v = e.v;
if (find(parent, u) != find(parent, v)) {
mst.push_back(e);
unionSet(parent, rank, u, v);
}
}
int main() {
int V = 4;
vector<Edge> edges = {{0, 1, 10}, {0, 2, 6}, {0, 3, 5}, {1, 3, 15}, {2, 3, 4}};
kruskal(V, edges);
return 0;
}
Output:
2-3:4
0-3:5
0 - 1 : 10
Explanation:
In this code, we implement Kruskal's algorithm to find the Minimum Spanning Tree (MST) of a graph.
1. Edge Structure:
1.We define an Edge struct to store the vertices (u, v) and the weight of the edge between
them. We also define an overload for the < operator to sort edges by their weights.
Find and Union Operations:
1. The find() function uses path compression to find the root of a given vertex, ensuring
efficient operations for disjoint sets.
2.
The unionSet() function unites two sets containing vertices x and y based on their ranks.
The rank is used to keep the tree flat during the union process, optimizing the find
operation.
Main Algorithm:
1. In the kruskal() function, we first sort the edges in increasing order of weight.
2. We initialize two arrays: parent[] to store the representative of each vertex and r ank[] to
store the rank of each vertex for union operations.
3. We then iterate over the edges, adding an edge to the MST if it connects two different
sets (i.e., no cycle is formed). We use the find() and unionSet() functions to manage the
disjoint sets.
Output:
1. F i n a l l y , w e p r i n t t h e e d g e s o f t h e M S T a l o n g w i t h t h e i r w e i g h t s .
In the main() function, we define a graph with 4 vertices and the respective edges with weights. We then call the kruskal() function to find
and print the edges of the MST.
1. Social Networks: In platforms like Facebook or Twitter, users are represented as vertices, and
their connections (friends, followers) are represented as edges.
2. Navigation Systems: Graphs represent cities or locations as vertices and the roads between them
as edges. This structure is essential for GPS and map applications, especially when finding the
shortest path between two locations.
3. Computer Networks: In networking, computers or devices are vertices, and communication
channels (like routers and switches) are edges. This helps in routing data efficiently.
4. Recommendation Systems: Websites like Amazon or Netflix use graph structures to recommend
products or movies by analyzing the relationships between products, users, and preferences.
5. Web Crawling: The web itself can be viewed as a graph, where web pages are nodes and
hyperlinks between them are edges. Graph algorithms help web crawlers index pages efficiently.
Challenges And Complexities In Graphs
Graphs, despite their versatility and wide applications, present several challenges in both theoretical and practical contexts. Here are some
key challenges and complexities associated with graphs:
1. Scalability:
o As the number of vertices and edges in a graph increases, the space and time complexity
of operations can become very large.
o Algorithms like Dijkstra’s, Bellman-Ford, or Floyd-Warshall require efficient handling of
large graphs to avoid performance degradation.
o
Sparse graphs (with fewer edges) may require different approaches compared to dense
graphs (with many edges), as dense graphs tend to have higher time complexities due to
the large number of edges.
2. Memory Consumption:
o Storing large graphs can be memory-intensive, especially when using adjacency matrices
for dense graphs. Adjacency lists are more memory-efficient for sparse graphs but may
still require significant space for very large graphs.
o
For instance, an adjacency matrix requires O(V^2) space (where V is the number of
vertices), whereas an adjacency list uses O(E) space (where E is the number of edges).
3. Cycle Detection:
o Detecting cycles in graphs is crucial for ensuring data consistency, especially in directed
graphs or when dealing with feedback loops.
o Algorithms for cycle detection (like DFS-based approaches) may face challenges with
graphs that have many complex interactions, leading to high computational costs.
4. Negative Weight Cycles:
o In the presence of negative weight edges, algorithms like Dijkstra’s algorithm fail to give
correct results, requiring us to use alternatives like Bellman-Ford or modifications for
accurate handling.
o Additionally, detecting negative weight cycles can complicate algorithms and extend their
running time, as we need to repeatedly check for updates after each iteration.
5. Graph Traversal:
o In real-world applications, graphs are often dynamic (edges and vertices can change over
time). Updating shortest paths when the graph is modified can be a difficult challenge.
o Algorithms such as Dijkstra’s or Bellman-Ford need to be adapted for dynamic graphs to
handle frequent updates efficiently.
7. Graph Coloring and NP-Completeness :
o Problems like graph coloring, where vertices are assigned colors such that no two adjacent
vertices share the same color, are NP-complete in the general case.
o These types of problems require heuristic or approximation algorithms to find acceptable
solutions in practical scenarios.
8. Edge Weights and Heuristics:
o Graph algorithms that involve weighted edges (like the shortest path algorithms) may be
sensitive to the way edge weights are assigned. Poorly chosen weights or heuristics can
lead to suboptimal or incorrect results, especially in algorithms like A* that rely on
heuristic functions.
9. Parallelism and Distributed Graph Processing :
o In certain applications (e.g., navigation, AI), we may encounter graphs that are
theoretically infinite or unbounded. Handling such graphs, where all possible paths are
not known upfront, requires specialized techniques like approximations or online
algorithms.
Complexity Of Graph Algorithms:
Time Complexity: The time complexity of graph algorithms can vary significantly. For example:
15 mins read
A tree data structure is a fundamental concept in computer science, designed to represent hierarchical relationships between elements. Much
like an actual tree, this structure consists of nodes connected by edges, with one node serving as the root and others branching out in a
parent-child hierarchy. Trees are widely used in various applications, including databases, file systems, and artificial intelligence, due to
their ability to organize and manage data efficiently.
In this article, we’ll explore the core concepts of tree data structures, their types, operations, and real-world applications, providing a
comprehensive understanding of why they are indispensable in the realm of programming and problem-solving.
Key Characteristics:
1. Node
The topmost node in the tree, which serves as the starting point of the structure.
Example: In a family tree, the founding ancestor is the root.
Note: A tree has only one root node.
3. Parent Node
Nodes that do not have any children; they are at the bottom level of the tree.
Example: Files in a file system directory tree are leaf nodes.
6. Edge
The number of edges on the longest path from the node to a leaf.
Example: In a tree with a root, a branch, and a leaf, the height of the root is 2.
9. Depth of a Node
The number of edges on the path from the root to the node.
Example: If a node is three levels below the root, its depth is 3.
10. Level
Ancestor: A node higher in the tree that leads to the current node.
Descendant: Any node that is part of the subtree of the current node.
14. Path
The number of edges in the longest path from the root to a leaf.
16. Balanced Tree
A tree where the height difference between the left and right subtrees of any node is minimal.
Real-World Analogy:
Imagine a family tree:
1. General Tree
Description: A tree where each node has at most two children, often referred to as the left and
right child.
Use Case: Representing expressions, decision trees, or searching and sorting algorithms.
3. Binary Search Tree (BST)
The left child contains nodes with values less than the parent.
The right child contains nodes with values greater than the parent.
Use Case: Efficient searching, insertion, and deletion operations.
4. Balanced Binary Tree
Description: A binary tree where the height difference between the left and right subtrees of any
node is minimal.
Use Case: Maintaining balance for faster operations (e.g., AVL tree, Red-Black tree).
5. Complete Binary Tree
Description: A binary tree where all levels, except possibly the last, are completely filled, and all
nodes are as left as possible.
Use Case: Implementing heaps.
6. Full Binary Tree (Strictly Binary Tree)
Description: A binary tree where all internal nodes have two children, and all leaves are at the
same level.
Use Case: Representing complete datasets with no irregularities.
8. AVL Tree
Description: A self-balancing binary search tree where the height difference between the left and
right subtrees of any node is at most 1.
Use Case: Maintaining efficient performance for dynamic datasets.
9. Red-Black Tree
Description: A self-balancing binary search tree where each node has a color (red or black) and
follows specific balancing rules.
Use Case: Efficient searching in databases and associative containers (e.g., std::map in C++).
10. B-Tree
Description: A self-balancing search tree designed for efficient data storage and retrieval on disk.
Use Case: Databases and file systems.
11. Heap
Description: A tree used to store strings, where each path represents a prefix of a word.
Use Case: Auto-completion, dictionary implementation, IP routing.
13. N-ary Tree
Description: A generalization of a binary tree where each node can have up to N children.
Use Case: Representing file systems or game trees.
14. Segment Tree
Description: A tree used to store intervals or segments, allowing fast queries and updates.
Use Case: Range queries in numerical datasets (e.g., sum, max, min).
15. Fenwick Tree (Binary Indexed Tree)
Description: A data structure for efficiently updating and querying prefix sums.
Use Case: Cumulative frequency tables.
16. Suffix Tree
Description: A subgraph of a connected graph that includes all the vertices with minimal edges.
Use Case: Network routing and optimization (e.g., Minimum Spanning Tree using Kruskal's or
Prim's algorithm).
18. Expression Tree
Description: A binary tree where leaves represent operands, and internal nodes represent
operators.
Use Case: Parsing and evaluating mathematical expressions.
Basic Operations On Tree Data Structure
1. Insertion: Adds a node to the tree, maintaining its structural or property constraints.
2. Deletion: Removes a node, rebalancing the tree if necessary.
3. Traversal:
class Node:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
class BinaryTree:
def __init__(self):
self.root = None
# Insertion
def insert(self, key):
if not self.root:
self.root = Node(key)
else:
self._insert(self.root, key)
# Traversal
def inorder(self, node):
if node:
self.inorder(node.left)
print(node.key, end=" ")
self.inorder(node.right)
# Height
def height(self, node):
if not node:
return 0
left_height = self.height(node.left)
right_height = self.height(node.right)
return 1 + max(left_height, right_height)
# Size
def size(self, node):
if not node:
return 0
return 1 + self.size(node.left) + self.size(node.right)
# Searching
def search(self, node, key):
if not node or node.key == key:
return node
if key < node.key:
return self.search(node.left, key)
return self.search(node.right, key)
# Mirror Image
def mirror(self, node):
if node:
node.left, node.right = node.right, node.left
self.mirror(node.left)
self.mirror(node.right)
# Example Usage
tree = BinaryTree()
values = [10, 5, 20, 3, 7, 15, 25]
print("In-order Traversal:")
tree.inorder(tree.root)
print("\nPre-order Traversal:")
tree.preorder(tree.root)
print("\nPost-order Traversal:")
tree.postorder(tree.root)
key = 15
found = tree.search(tree.root, key)
print(f"Node {key} {'found' if found else 'not found'} in the tree.")
In-order Traversal:
3 5 7 10 15 20 25
Pre-order Traversal:
10 5 3 7 20 15 25
Post-order Traversal:
3 7 5 15 25 20 10
Tree Height: 3
Tree Size: 7
Node 15 found in the tree.
1. Node Class: We define a Node class to represent each node in the tree. Each node stores a key
(data) and pointers to its left and right children.
2. BinaryTree Class: This is where we define the tree structure and implement its operations. The
root is initialized as None.
3. Insertion:
Use Case: Binary Search Trees (BST), B-trees, and B+ trees in databases.
Example: B-trees are used in database indexing for efficient query execution.
3. Routing and Network Design
Use Case: Segment Trees and Binary Indexed Trees (Fenwick Trees).
Example: Efficient range queries and updates in dynamic programming problems.
14. Genealogy and Biology
Each node (except the root) has Each element connects only to the
Connections Nodes can have multiple connections (edges).
exactly one parent. next (or previous) in the sequence.
Cyclic Nature Always acyclic (no loops). Can be cyclic or acyclic. Not applicable (sequence-based).
DFS (in-order, pre-order, post-order), DFS, BFS, and more complex algorithms
Traversal Sequential (forward or backward).
BFS. (e.g., Dijkstra).
Ease of Moderate complexity due to recursive Complex, especially with algorithms like Easy to implement and use for basic
Implementation nature. shortest path or spanning trees. tasks.
Efficient for hierarchical data or Versatile for relational data and network Suitable for simple sequential access
Efficiency
searching with a BST. traversal. and storage.
1. Efficient Searching: Trees, especially Binary Search Trees (BSTs), allow for faster searching
compared to linear data structures like arrays or linked lists, with a time complexity of O(log n) in
balanced trees.
2. Hierarchical Representation: Trees are ideal for representing hierarchical relationships, such as
file systems, organizational structures, and decision-making processes.
3. Fast Insertion and Deletion: In balanced trees, insertion and deletion operations can be
performed efficiently, typically in O(log n) time, which is much faster than array-based or list-
based operations.
4. Optimal Memory Utilization: Trees use pointers to dynamically allocate memory, making them
more memory-efficient for managing dynamic datasets compared to static data structures like
arrays.
5. Supports Multiple Operations: Trees support a wide range of operations, including searching,
sorting, traversal, and balancing, which makes them versatile for many applications such as
databases, compilers, and network routing.
6. Improved Performance in Sorted Data: With trees like AVL and Red-Black trees, maintaining
sorted data can be done efficiently, which speeds up operations such as range queries, data
retrieval, and updates.
Disadvantages Of Tree Data Structure
Here are the disadvantages of tree data structures:
12 mins read
Dynamic Programming (DP) is a technique for solving problems by breaking them into smaller, overlapping subproblems and reusing
solutions to save time. It is ideal for optimization and recursive problems, ensuring efficiency by avoiding redundant computations.
In this article, we will explore the core concepts of dynamic programming and its key principles and provide step-by-step guides with real-
world examples.
What Is Dynamic Programming?
Dynamic programming (DP) is a problem-solving approach used in computer science to solve problems by breaking them into smaller
overlapping subproblems. It is particularly effective for optimization problems and those with a recursive structure.
1. Optimal Substructure: A problem exhibits optimal substructure if its solution can be constructed
efficiently from the solutions of its smaller subproblems.
1.Example: Finding the shortest path in a graph involves combining the shortest paths of
smaller subgraphs.
Overlapping Subproblems: Many problems involve solving the same subproblem multiple times.
DP solves each subproblem once and stores the results to avoid redundant computations.
1. E x a m p l e : R e c u r s i v e F i b o n a c c i w i t h m e m o i z a t i o n .
Tabulation (Bottom-Up Approach): In this approach, subproblems are solved iteratively, and the
results are stored in a table (usually an array) to build up the solution for the larger problem.
1. Break It Down:
You group the puzzle pieces by color or pattern (e.g., edges, sky, trees, etc.), making it easier to
focus on smaller parts of the puzzle.
2. Reuse Your Work:
Once you solve a smaller section (e.g., the sky), you don’t revisit it or redo it. Instead, you place
it aside, knowing it’s solved. This is like memoization in dynamic programming.
3. Build Step by Step:
After solving smaller sections, you combine them to form larger sections (e.g., attaching the sky to
the trees). This is similar to tabulation, where you solve subproblems iteratively and build up to
the final solution.
4.Avoid Redundancy:
If you’ve already figured out where a piece fits, you don’t test it again in other places. This
prevents repetitive work and saves time.
How This Relates To Dynamic Programming:
1. Optimal Substructure: Can the problem be broken into smaller, simpler subproblems?
2.Overlapping Subproblems: Are some subproblems solved multiple times in a naive
solution?
Define the State
1. Write a mathematical relation that connects the solution of the current subproblem to
smaller subproblems.
2. E x a m p l e f o r F i b o n a c c i : F ( n ) = F ( n − 1 ) + F ( n − 2 )
Choose an Approach
1. Memoization (Top-Down): Solve the problem recursively and store results to avoid
recomputation.
2.Tabulation (Bottom-Up): Solve subproblems iteratively, starting from the smallest and
building up to the final solution.
Define Base Cases
1. Establish the solutions for the simplest subproblems, which will act as starting points.
2. E x a m p l e f o r F i b o n a c c i : F ( 0 ) = 0 , F ( 1 ) = 1
Implement the Solution
1. If only a few previous states are required, reduce space usage by storing only those.
2. E x a m p l e : I n F i b o n a c c i , r e d u c e s p a c e c o m p l e x i t y f r o m O ( n ) t o O ( 1 ) .
Test the Solution Thoroughly
1. Validate the implementation using various test cases, including edge cases.
2. Example: For Fibonacci, test with n=0,1,10,50, etc.
Dynamic Programming Algorithm Techniques
Dynamic programming (DP) involves solving complex problems by breaking them into overlapping subproblems. The two main techniques
are:
Concept:
Recursive in nature.
Saves computation time by avoiding redundant calculations.
Can be slower for problems with deep recursion due to function call overhead.
Code Example: Fibonacci Using Memoization
# Example usage
n = 10
print("Fibonacci number at position", n, ":", fibonacci_memoization(n))
Output:
Concept:
Solve the problem iteratively from the smallest subproblems up to the main problem.
Use a table (e.g., array) to store results for subproblems.
Each subproblem builds on the results of previously solved subproblems.
Key Characteristics:
Iterative in nature.
Avoids recursion and function call overhead.
Often uses less space compared to memoization (no recursion stack).
Code Example: Fibonacci Using Tabulation
def fibonacci_tabulation(n):
# Handle base cases
if n == 0:
return 0
if n == 1:
return 1
# Example usage
n = 10
print("Fibonacci number at position", n, ":", fibonacci_tabulation(n))
Output:
Space Complexity O(n) for cache and recursion O(n) for table
Ease of Implementation Easier for complex problems Slightly harder for some problems
1. Optimal Substructure: DP ensures that a problem can be broken down into simpler subproblems,
and solutions to those subproblems are combined to solve the overall problem optimally.
2. Avoids Redundant Calculations: By storing the results of previously solved subproblems (either
in a table or cache), DP significantly reduces the computation time, especially in problems with
overlapping subproblems.
3. Improves Efficiency: With DP, problems that would otherwise take exponential time (like the
Fibonacci sequence or the knapsack problem) can be solved in polynomial time.
4. Provides Clear Structure: The recursive structure of the problem and the defined relationship
between subproblems make DP easy to model and understand once the recurrence relation is
identified.
Disadvantages Of Dynamic Programming
Some of the common disadvantages of dynamic programming are:
1. Space Complexity: DP requires additional memory to store the intermediate results, which can
sometimes be prohibitive, especially in problems with a large number of subproblems.
2. Overhead: The need to store and retrieve intermediate results (in memoization) or iterate over
large tables (in tabulation) can introduce overhead, especially in problems where the number of
subproblems is very large.
3. Complex Implementation: Identifying the optimal substructure, formulating the recurrence
relation, and setting up the state and transition can sometimes be difficult, especially for complex
problems.
4. Not Always Applicable: Not all problems exhibit optimal substructure and overlapping
subproblems, making DP unsuitable for many types of problems.
Applications Of Dynamic Programming
Some of the important applications of dynamic programming are:
1. Fibonacci Numbers: Classic example where DP reduces the exponential time complexity of the
recursive approach to linear time.
2. Knapsack Problem: DP is widely used to solve optimization problems, like the knapsack problem,
where we need to maximize or minimize a certain quantity (e.g., profit or cost) while adhering to
constraints.
3. Longest Common Subsequence (LCS): In string matching problems, DP helps in finding the
longest common subsequence between two strings, commonly used in bioinformatics and text
comparison.
4. Matrix Chain Multiplication: DP is used in problems where the goal is to optimize the order of
matrix multiplication to minimize computational cost.
5. Shortest Path Problems (e.g., Floyd-Warshall, Bellman-Ford) : DP is used to find the shortest
paths between vertices in a graph, which is useful in routing algorithms, network optimization, and
GPS navigation.
6. Edit Distance (Levenshtein Distance): DP helps in calculating the minimum number of operations
(insertions, deletions, substitutions) required to convert one string into another, commonly used in
spell checkers and text comparison.
7. Game Theory: DP can be used to optimize decision-making strategies in games, such as
minimizing or maximizing a player's advantage.
8. Resource Allocation Problems: In resource scheduling, job scheduling, or project management,
DP can optimize allocation to minimize time, cost, or other factors.
10 mins read
The sliding window algorithm is a powerful and efficient algorithmic approach used to solve problems involving arrays or lists. By
maintaining a subset of elements within a "window" that slides over the data, this technique helps us solve complex problems like finding
maximums, minimums, or specific patterns in a streamlined way. In this article, we'll explore the sliding window algorithm, understand how
it works, and see practical examples to apply it effectively in coding scenarios.
Real-World Analogy
Imagine you're standing in front of a long conveyor belt with boxes of different weights. You need to find the heaviest group of k
consecutive boxes. Instead of weighing every possible group, you start with the first k boxes and then "slide" to the next group by adding the
weight of the next box and removing the weight of the first box. This saves time and effort compared to re-weighing everything from
scratch.
1. Window Definition: The "window" refers to a range of indices in the array or list. The size of the
window can either be fixed or dynamic, depending on the problem.
2. Sliding the Window:
1. If the window size is fixed, move both the start and end of the window
simultaneously.
2.
If the window size is dynamic, adjust the start or end as needed based on
conditions.
Optimize with Each Slide: Compute or update the desired result (like sum, maximum, or unique
count) as the window slides, avoiding redundant computations.
When To Use The Sliding Window Technique?
The Sliding Window Algorithm is useful for problems like:
1. E x a m p l e : " F i n d t h e m a x i m u m s u m o f a n y s u b a r r a y o f s i z e k . "
Fixed or Variable Window Size:
The problem requires:
1. Maximum sum
2. Minimum length
3. Longest/shortest substring
4. N u m b e r o f d i s t i n c t e l e m e n t s
Sequential Processing:
The solution must process elements in a sequential order, and there is no need to revisit previous
elements unnecessarily.
1.Example: Sliding windows work well for problems where overlapping calculations are
required, and only the new and outgoing elements affect the result.
Constraints and Patterns:
The problem often involves constraints like:
Code Example:
return max_sum
# Example Usage
arr = [2, 1, 5, 1, 3, 2]
k=3
result = max_sum_subarray(arr, k)
print(f"Maximum sum of subarray of size {k}: {result}")
Output:
1. We begin by defining a function, max_sum_subarray, that calculates the maximum sum of any
subarray of size k in the input array arr.
2. First, we determine the length of the array using len(arr) and store it in n.
3. If the array size n is smaller than k, we immediately return an error message: "Invalid input:
Array size is smaller than k." This ensures we handle edge cases gracefully.
4. Next, we compute the sum of the first k elements of the array using sum(arr[:k]). This becomes
our initial max_sum and current_sum.
5. Using a sliding window approach, we iterate from the kth index to the last index of the array. In
each iteration:
1. We update current_sum by adding the current element (arr[i]) and removing the first
element of the previous window (arr[i - k]).
2. We compare the current_sum with max_sum and update max_sum if current_sum is
larger. This ensures we keep track of the maximum sum encountered so far.
After iterating through the array, we return the max_sum, which is the maximum sum of a
subarray of size k.
In the example usage, we pass the array [2, 1, 5, 1, 3, 2] and k = 3 to the function. The function
calculates and returns the maximum sum of a subarray of size 3, which is printed as: "Maximum
sum of subarray of size 3: 9".
Variable-Size Sliding Window Example: Smallest Subarray With A Given Sum
In this example, we aim to find the smallest subarray whose sum is greater than or equal to a given value target. The Sliding Window
Algorithm efficiently adjusts the window size dynamically by expanding and contracting it based on the current sum.
Code Example:
# Contract the window until the sum is less than the target
while current_sum >= target:
min_length = min(min_length, end - start + 1) # Update the minimum length
current_sum -= arr[start] # Remove the element at the start
start += 1 # Shrink the window from the left
# Example Usage
arr = [2, 3, 1, 2, 4, 3]
target = 7
result = smallest_subarray_with_sum(arr, target)
print(f"Smallest subarray length with sum >= {target}: {result}")
Output:
1. W e e x p a n d t h e w i n d o w b y a d d i n g a r r [ e n d ] t o c u r r e n t _ s u m .
While the current_sum is greater than or equal to the target, we contract the window from the
left:
1. We update min_length to the smaller of its current value or the current subarray length
(end - start + 1).
2. We subtract arr[start] from current_sum to remove the leftmost element and then
increment start to shrink the window.
After completing the loop, we check if min_length was updated from infinity. If not, it means no
valid subarray was found, so we return 0. Otherwise, we return min_length.
In the example usage, we pass the array [2, 3, 1, 2, 4, 3] and target = 7. The function calculates
and returns the smallest subarray length, which is printed as: "Smallest subarray length with sum
>= 7: 2".
Advantages Of Sliding Window Technique
Some of the advantages are as follows:
Optimized Time Complexity: Reduces time complexity from O(n²) to O(n), especially useful for
large datasets.
Reduced Redundant Computations: Avoids recalculating results for every subarray by reusing
previous computations.
Memory Efficiency: Works in-place, requiring minimal additional memory.
Scalability: Handles large inputs efficiently, ideal for real-time or streaming data.
Simplicity and Easy Implementation: Easy to implement with two pointers to track the window.
Works Well for Contiguous Subarrays/Substrings : Best suited for problems with contiguous
data.
Dynamic Adjustment: Can handle problems where window size adjusts based on conditions.
Disadvantages Of Sliding Window Technique
Some of the disadvantages are as follows:
Limited to Contiguous Subarrays/Substrings : Only works for problems where data must be
contiguous, not applicable for non-contiguous subsets.
May Not Be Ideal for All Problems: For problems involving complex conditions or dependencies
across distant elements, sliding window might not be the best approach.
Requires Precise Window Conditions: The algorithm relies on knowing the exact conditions for
sliding the window, which can be challenging for some problems.
Not Always Memory Efficient: In cases where extra space is needed for auxiliary data structures
(e.g., hashmaps), memory efficiency might be compromised.
Harder to Visualize for Complex Scenarios : For problems with dynamic window sizes or complex
constraints, understanding and visualizing the window's behavior can be tricky.
55+ Data Structure Interview Questions For 2025 (Detailed
Answers)
Boost your tech interview preparation with these essential data structure interview questions. This
comprehensive guide covers key concepts and common queries to help you ace your interview with
confidence.
54 mins read
Having a strong grasp of data structures is crucial when preparing for a technical interview. In this article, we will cover a range of
important data structures interview questions designed to test your understanding and ability to apply these concepts in real-world scenarios.
Whether you're an experienced professional or just starting your career, these data structures and algorithms interview questions will help
you prepare for your next interview and sharpen your skills.
Array A collection of elements of the same type stored in contiguous memory locations.
Structure (struct) A custom data type that groups variables of different types under a single name.
A special data type that allows storing different structured data types in the same memory
Union
location.
Linked List A collection of nodes where each node contains data and a pointer to the next node.
Stack A linear data structure following the Last In, First Out (LIFO) principle.
Queue A linear data structure following the First In, First Out (FIFO) principle.
Tree A hierarchical data structure with nodes connected by edges, with one root node.
Hash Table A data structure that maps keys to values using a hash function for fast access.
File An abstraction for reading from and writing to external storage like hard drives.
Now that we have a basic understanding of data structures and their fundamental concepts let's explore some of the most commonly asked
data structures and algorithm (DSA) interview questions with answers. We have classified these questions into three main categories:
1. Organization: Data structures determine how data is organized and how different data elements are related to each other. This
organization can affect the efficiency of data manipulation and retrieval.
2. Storage: They specify how data is stored in memory, whether it is in a contiguous block (as in arrays) or distributed across
memory (as in linked lists).
3. Operations: Data structures define the operations that can be performed on the data, such as insertion, deletion, traversal,
searching, and sorting. The efficiency of these operations often depends on the choice of data structure.
4. Type of Data: Different data structures are optimized for different types of data and operations. For example, arrays and lists are
suitable for sequential data, while trees and graphs are used for hierarchical and networked data.
2. Differentiate between linear and non-linear data structures. Provide examples of each.
Here's a table that lists the key differences between linear and non-linear data structures:
Data structures where elements are arranged Data structures where elements are arranged
Definition sequentially, with each element connected to its hierarchically, and elements can have
previous and next elements. multiple relationships.
Traversal is done in a single level, usually in a Traversal can be done on multiple levels
Traversal
straight line, from one element to the next. and may require visiting multiple branches.
A binary tree is a type of non-linear data structure in which each node has at most two children, referred to as the left child and the right
child. The structure is hierarchical, with a single node known as the root at the top, and each subsequent node connecting to its child nodes.
Dynamic data structures are types of data structures that can grow and shrink in size during program execution. Unlike static data structures
(like arrays), which have a fixed size determined at compile time, dynamic data structures can adapt to the changing needs of a program
1. Linked Lists: Consists of nodes where each root node contains data and a reference (or link) to the next node. This allows for
efficient insertion and deletion of elements.
2. Stacks: Follow the Last In, First Out (LIFO) principle. Elements are added and removed from the top of the stack. They are often
implemented using linked lists.
3. Queues: Follow the First In, First Out (FIFO) principle. Elements are added to the back and removed from the front. They can
also be implemented with linked lists.
4. Trees: Hierarchical structures where each node has zero or more child nodes. Common types include binary trees, AVL trees,
and red-black trees.
5. Hash Tables: Store key-value pairs and use a hash function to quickly access elements based on their keys.
5. What is a linked list? Explain its types.
A linked list is a linear data structure in which elements are stored in nodes, and each node points to the next node in the sequence.
Unlike data structures like arrays, linked lists do not require contiguous memory allocation; instead, each node contains a reference (or
pointer) to the next node, creating a chain-like structure.
Singly Linked List: Each node has a single pointer to the next node, allowing traversal in one
direction.
Doubly Linked List: Each node has two pointers: one to the next node and one to the previous
node, allowing traversal in both directions.
Circular Linked List: The last node points back to the head node, forming a circular chain.
6. How do you declare an array in C?
An array is declared by specifying the type of its elements and the number of elements it will hold. The general syntax for declaring an array
is:
type arrayName[arraySize];
Here,
type: The data type of the elements in the array (e.g., int, float, char).
arrayName: The name you want to give to the array.
arraySize: The number of elements the array can hold.
7. Differentiate between a stack and a queue.
This is one of the most commonly asked data structures (DS) interview questions. The key differences between a stack and a queue are as
follows:
A linear data structure that follows the A linear data structure that follows the First In, First
Definition
Last In, First Out (LIFO) principle. Out (FIFO) principle.
Deletion pop – Removes the element from the top dequeue – Removes the element from the front of the
Operation of the stack. queue.
Function call management, undo Scheduling tasks, buffering, and queue management in
Use Cases
mechanisms, expression evaluation. algorithms.
Often implemented using arrays or linked Often implemented using arrays or linked lists with
Implementation
lists with a top pointer. front and rear pointers.
Operations (push and pop) are O(1) in Operations (enqueue and dequeue) are O(1) in time
Complexity
time complexity. complexity.
8. What do you understand by recursion?
Recursion in programming is a technique where a function calls itself in order to solve a problem. A recursive function typically breaks
down a problem into smaller, more manageable sub-problems of the same type. It continues to call itself with these smaller sub-problems
until it reaches a base case, which is a condition that terminates the recursion.
For example:
int factorial(int n) {
if (n <= 1) // Base case
return 1;
else // Recursive case
return n * factorial(n - 1);
}
9. How is a one-dimensional array different from a two-dimensional array?
Listed below are the key differences between one-dimensional and two-dimensional arrays:
Structure Linear sequence of elements Grid or matrix with rows and columns
Example int arr[5] (array with 5 elements) int matrix[3][4] (3 rows, 4 columns)
Accessing Elements Access with one index Access with two indices
11. What is the difference between file structure and storage structure?
File structure and storage structure are related but distinct concepts in data management and computer science. Here’s a comparison of the
two:
Refers to the organization and layout of data Refers to how data is physically stored on a storage
Definition within a file. It defines how data is logically medium (e.g., hard drive, SSD). It involves the low-
arranged and accessed. level organization of data blocks.
To manage how data is organized and To manage the efficient use of physical storage
Purpose retrieved from a file. It often focuses on space and retrieval operations, focusing on the
logical aspects such as records and fields. physical arrangement of data blocks or sectors.
Logical organization of data to facilitate Physical layout and efficient management of storage
Focus
access and manipulation. space.
1. Representation of Complex Data: Multidimensional arrays are ideal for sequential representation of tables, matrices, or grids,
where data is organized in rows and columns. For example, a 2D array can represent a matrix in mathematical computations or a
game board in a simulation.
2. Efficient Data Access: They provide a way to access elements efficiently using indices. For instance, in a 2D array, you can
quickly access any element using its row and column indices.
3. Spatial Data Handling: Useful in applications involving spatial data such as geographical maps, image processing (where a 2D
array represents pixel values), and simulations.
4. Multi-dimensional Problems: Multidimensional arrays can handle higher-dimensional problems (e.g., 3D arrays for volumetric
data or simulations in three dimensions), which are common in scientific computations, physics simulations, and complex data
analysis.
5. Simplified Code: They simplify the code needed for managing complex data structures by allowing data to be organized and
accessed in a systematic way. This can make algorithms more straightforward and easier to implement.
6. Algorithm Efficiency: Certain algorithms, like those in dynamic programming, can be implemented more efficiently using
multi-dimensional arrays to store intermediate results and facilitate faster computations.
13. What do you understand by time complexity in data structures and algorithms?
Time Complexity in data structures measures how the execution time grows as the input size increases. It provides an estimate of the
algorithm's efficiency and is used to compare the performance of different coding algorithms.
The most common way to express time complexity is using Big-O notation (O(f(n))), which describes the upper bound of the runtime in the
worst-case scenario. It provides an asymptotic analysis of how the runtime increases with input size.
Common Complexities:
O(1): Constant time, i.e., the runtime does not change with the size of the input. Example:
Accessing an element in an array.
O(log n): Logarithmic time, i.e., the runtime increases logarithmically with the input size.
Example: Binary search.
O(n): Linear time, i.e., the runtime increases linearly with the input size. Example: Linear search.
14. Discuss the advantages and disadvantages of using arrays.
Arrays are one of the most basic and commonly used data structures in computer science. They provide a straightforward way to store and
access a collection of elements. Here's a discussion of the advantages and disadvantages of arrays:
Advantages of Arrays:
1. Direct Access: Provides O(1) time complexity for accessing elements via indexing.
2. Simple Implementation: Easy to implement and use with contiguous memory allocation.
3. Cache Efficiency: Stores elements contiguously, improving cache performance.
4. Fixed Size: Predictable memory usage when the size is known in advance.
5. Ease of Use: Built-in support in most programming languages.
Disadvantages of Arrays:
1. Fixed Size: Cannot dynamically resize, leading to potential wasted or insufficient space.
2. Costly Insertions/Deletions: Inserting or deleting elements (except at the end) requires shifting
elements, resulting in O(n) time complexity.
3. Memory Usage: Can waste memory space if not fully utilized or require resizing if too small.
4. Static Nature: Less suitable for dynamic data sizes and frequent changes.
5. Lack of Flexibility: Inefficient for operations like searching or resizing without additional
algorithms.
Data Structures Interview Questions: Intermediate
15. What is a depth-first search (DFS)?
Depth-first search (DFS) is a fundamental algorithm used for traversing or searching tree or graph data structures. The main idea behind
DFS is to explore as far down a branch as possible before backtracking.
Working:
1. Starting Point: Begin at a selected node (root or any arbitrary node) and mark it as visited.
2. Explore: Recursively explore each unvisited adjacent node, moving deeper into the structure.
3. Backtrack: Once a node has no more unvisited adjacent nodes, backtrack to the previous node and
continue the process.
4. Continue: Repeat the process until all nodes in the structure have been visited.
Key Features:
Stack-Based: DFS can be implemented using a stack data structure, either explicitly with a stack
or implicitly via recursion.
Traversal Order: Visits nodes in a depthward motion before moving horizontally. This means it
explores all descendants of a node before visiting its siblings.
16. Explain the working mechanism of linear search.
Linear search is a simple searching algorithm used to find a specific element in a list or array. It works by sequentially checking each
element until the desired element is found or the end of the list is reached.
1. Start from the Beginning: Begin at the first element of the list or array.
2. Compare Elements: Compare the target value (the element we are searching for) with the current element in the list.
3. Check for Match:
If Match Found: If the target value matches the current element, return the index or
position of that element.
If No Match: If the target value does not match the current element, move to the next
element in the list.
Repeat: Continue the comparison process until either:
The target value is found, or
The end of the list is reached without finding the target.
End of Search:
If Target Found: Return the index or position of the target element.
If Target Not Found: Return a value indicating that the target is not present in the list
(e.g., -1 or null).
Example: Consider searching for the value 7 in the array [2, 4, 7, 1, 9]:
A queue is a data structure that follows the First In, First Out (FIFO) principle. Key characteristics include:
FIFO (First In, First Out): The elements are pushed at the back (enqueue) and pulled from the
front (dequeue). The first added element is the first to be replaced.
Linear Structure: A sequence of elements in order to be taken up in succession (queue).
Enqueue and Dequeue Operations: This data structure operates with two main functions, namely,
the enqueue, which implies adding to the end, and the dequeue, which involves taking away from
the beginning.
Limited Access: Access to elements is typically restricted, and accessing elements often occurs
sequentially.
18. What is the difference between heap and priority queue?
A heap and a priority queue are closely related but distinct concepts in data structures. While heaps are often used to implement priority
queues, they are not synonymous. Here are the key differences between the two:
Used to efficiently manage and Provides a way to manage elements with priorities
Purpose retrieve the minimum or maximum and retrieve the element with the highest or lowest
element. priority efficiently.
Heap Sort: Efficient sorting algorithm. Task Scheduling: Managing tasks with priorities.
Use Cases
Priority Queue: Underlying implementation for Graph Algorithms: Used in algorithms like Dijkstra’s for shortest
priority queues. paths.
Advantages:
Efficient Searching: A sorted list allows for binary search, which is much faster than linear search. Binary search has a time
complexity of O(logn)O(\log n)O(logn), compared to O(n)O(n)O(n) for linear search. This is particularly beneficial for large
datasets.
Easy to Maintain Order: While maintaining order during insertion and deletion can be challenging, some data structures like
balanced binary search trees or skip lists support efficient insertion and deletion while maintaining order. In simpler cases,
maintaining order may require re-sorting or shifting elements, which can be less efficient.
Improved Performance in Specific Operations: Certain algorithms and operations are more efficient when working with
sorted data. For example, algorithms for merging, finding duplicate elements, or computing ranges (like finding the range of
elements within a specific interval) can be performed more efficiently on sorted lists.
A sparse matrix is a matrix in which most of the elements are zero. This type of matrix is commonly used in mathematical computations
and data storage where the non-zero elements are significantly fewer compared to the total number of elements. Its common storage formats
are as follows:
Dynamic memory allocation refers to the process of allocating memory storage during the runtime of a program, rather than at compile time.
This is done using functions or different operators provided by programming languages, such as malloc(), calloc(), realloc(), and free() in C,
or the new and delete operators in C++.
In languages with automatic memory management, like Python or Java, dynamic memory allocation is managed by the language's runtime
environment through a process known as garbage collection.
Importance:
Flexibility: Allocates and reallocates memory dynamically in response to the data storage sizes.
Optimized Memory Usage: It also assists in minimizing wastage through the allocation of an
appropriate size of memory that is needed.
Dynamic Data Structures: Provides support for developing linked lists and trees as dynamic data
structures.
22. What is the difference between a depth-first search and a breadth-first search?
This is one of the most commonly asked data structures and algorithms interview questions. Given below is a detailed comparison between
Depth-First Search and Breadth-First Search:
Traversal Method Explores as far down a branch as Explores all neighbors at the present depth
Feature Depth-First Search (DFS) Breadth-First Search (BFS)
O(V + E) where V is the number of O(V + E) where V is the number of vertices and
Time Complexity
vertices and E is the number of edges. E is the number of edges.
Visits nodes deeper into the tree or Visits nodes level by level, starting from the
Traversal Order
graph first. root.
Finding Shortest Not guaranteed to find the shortest path Guarantees finding the shortest path in
Path in unweighted graphs. unweighted graphs.
23. Explain the pop operation in the context of a stack data structure.
In the context of a stack data structure, the pop operation refers to removing the top element from the stack.
1. Removes Top Element: The pop operation takes away the element that is currently at the top of the stack.
2. Follows LIFO Principle: The last element added is the first one to be removed.
3. Returns Removed Element: Usually returns the value of the top element that is removed.
4. Time Complexity: It operates in constant time, O(1), which means it’s quick regardless of how many elements are in the stack.
5. Handling Empty Stack: If the stack is empty, trying to pop may either cause an error in code or return a special value indicating
the stack is empty.
Example:
Stack before pop: [10, 20, 30]
pop operation removes 30
Stack after pop: [10, 20]
24. What is a double-ended queue, and how does it differ from a regular queue?
A double-ended queue (deque) is a data structure that allows the insertion and removal of elements from both ends, whereas a regular queue
allows operations only at one end (rear for insertion) and the other end (front for removal).
Key Differences:
Insertion/Removal Flexibility: Deques allow insertion and removal from both ends, while regular
queues allow these operations only at one end each.
Usage: Deques are more versatile and can be used in scenarios requiring operations at both ends,
whereas regular queues are used for simple FIFO processing.
Flexibility: Deques are more suitable than ordinary queues in such applications as queuing and
stacking.
25. Explain the concept of space complexity in algorithms.
Space complexity in algorithms measures the total amount of memory required to execute an algorithm relative to the size of the input data.
It includes both the fixed amount of space needed for program instructions and the variable space
used for dynamic data structures, such as arrays or linked lists.
Expressed in Big-O notation (e.g., O(1), O(n), O(n^2)), space complexity helps in understanding
how the memory requirements grow as the input size increases.
For instance, an algorithm with constant space complexity (O(1)) uses a fixed amount of memory
regardless of input size, while an algorithm with linear space complexity (O(n)) uses memory
proportional to the input size.
26. How does a stack data structure work?
A stack is a fundamental data structure that operates on a Last-In-First-Out (LIFO) principle, meaning the most recently added element is
the first one to be removed. A stack works as follows:
1. LIFO Principle: The last element added to the stack is always the first one to be removed. This behavior is akin to a stack of
plates where you can only take the top plate off or add a new plate on top.
2. Operations: A stack supports the following set of core operations:
Push: Add an element to the top of the stack. This operation increases the stack's size by
one.
Pop: Remove the element from the top of the stack. This operation decreases the stack's
size by one and returns the removed element.
Peek/Top: View the element at the top of the stack without removing it. This operation
allows you to examine the most recently added element.
IsEmpty: Check whether the stack is empty. This operation returns a boolean
value indicating if there are no elements in the stack.
27. Describe the applications of graph data structure.
Graphs are versatile data structures with a wide range of applications across various fields. Here are some key applications:
1. Social Networks:
Operation Singly Linked List Doubly Linked List Circular Linked List
30. Explain the concept of a hash table. How does it handle collisions?
This is one of the popularly asked data structures interview questions. A hash table is a data structure that implements an associative array,
mapping keys to values. It uses a hash function to compute an index (also called a hash code) into an array of buckets or slots, from which
the desired value can be found. This allows for efficient data retrieval, typically in constant time, O(1), for both average-case insertion and
lookup operations.
Collision Handling: Collisions occur when two different keys hash to the same index in the array.
Various strategies exist to handle collisions:
Chaining: Each bucket points to a linked list of entries that share the same hash index. When a collision occurs, a new entry is
added to the list. Searching for an entry involves traversing the linked list.
Open Addressing: Instead of linked lists, open addressing stores all elements in the hash table array itself. When a collision
occurs, the algorithm probes the array to find an empty slot using methods like linear probing, quadratic probing, or double
hashing.
Double Hashing: Uses two hash functions to calculate the index and the step size, helping to avoid clustering of keys.
31. How is bubble sort implemented, and what is its time complexity?
1. Starting from the beginning of the array , compare each pair of adjacent elements.
2. Swap the elements if they are in the wrong order (i.e., the first element is greater than the second
element).
3. Repeat the process for each element in the array, moving from the start to the end.
4. After each pass through the array, the largest unsorted element "bubbles up" to its correct
position.
5. Reduce the number of comparisons for the next pass since the largest elements are already
sorted.
Time Complexity: O(n^2) in the worst and common case, in which 'n' is the variety of elements in the array. In the best case (while the
array is already taken care of), it's miles O(n).
32. Discuss the limitations of bubble sorting and provide scenarios where it might be preferable over
other sorting algorithms.
Despite its simplicity, bubble sort has several limitations:
Inefficiency: Bubble kind is inefficient for huge datasets due to its quadratic time complexity.
Lack of Adaptivity: It does not adapt properly to the present order of elements. It performs an
identical variety of comparisons no matter the initial order.
Not Suitable for Large Datasets: Due to its inefficiency, bubble kind isn't the best choice for
sorting huge datasets or datasets with complicated systems.
Scenarios Where Bubble Sort Might Be Preferable:
Educational Purposes: Due to its simplicity, bubble sort is regularly used to introduce the idea of
sorting algorithms in academic settings.
Small Datasets: For very small datasets, wherein simplicity is more vital than performance,
bubble sort might be considered.
Nearly Sorted Data: In instances where the statistics are sorted, bubble sort's adaptive nature in
the nice-case situation may be an advantage.
In actual international scenarios, different extra green sorting algorithms like quicksort, mergesort, or heapsort are commonly favored over
bubble type, particularly for large datasets.
33. What are the advantages and disadvantages of using a hash table for data storage?
Advantages of Hash Tables
1. Fast Access: Hash tables generally offer very fast insertion, deletion, and lookup operations, with an average time complexity of
O(1) due to direct index calculation using the hash function.
2. Flexibility: They can store a wide variety of abstract data types and are versatile in handling different kinds of data.
3. Efficient Memory Usage: When the hash table is not too densely populated, it can be memory-efficient as it typically only
stores the keys and values that are actually used.
4. Efficient Handling of Large Data: Hash tables can efficiently handle large sets of data because their operations do not depend
significantly on the size of the data set.
1. Potential for Collisions: Collisions can degrade the performance of a hash table, especially if the hash function is not well-
designed or if the table becomes too full. Poor collision handling can lead to O(n) time complexity in the worst case.
2. Memory Overhead: Hash tables require additional memory for storing pointers or linked lists (in chaining) or for handling
probing sequences (in open addressing). They also typically require more space than the actual data being stored to avoid
collisions (load factor management).
3. Difficulty in Traversal: Unlike data structures like arrays or linked lists, hash tables do not support efficient ordered traversal.
The order of elements depends on the hash function, and there is no inherent way to traverse the elements in a particular order.
4. Complexity of Hash Functions: Designing an efficient hash function that minimizes collisions and distributes keys uniformly is
often non-trivial. Poor hash functions can lead to clustering and degrade performance.
1. Binary Search Tree (BST) Property: In a BST, the left node of a given node contains values that are less than the value of the
parent node. This property is crucial for maintaining the ordered structure of the tree and enables efficient search operations.
2. Search Operations: When searching for a specific value in a BST:
If the value to be searched is less than the value of the current node, we move to the left
child (left node) of the current node.
This property allows us to eliminate half of the tree from consideration with each step,
resulting in an average time complexity of O(log n) for balanced BSTs.
Traversal Operations: Different traversal methods use the left node in various ways:
In-order Traversal: Visits the left subtree first, then the root, and finally the right
subtree. This traversal method produces a sorted sequence of node values for BSTs.
Pre-order Traversal: Visits the root first, then the left subtree, and finally the right
subtree. In this order, the left node is processed before the right node.
Post-order Traversal: Visits the left subtree first, then the right subtree, and finally the
root. Here, the left node is processed before the right node and the root.
Impact on Operations:
1. Efficiency of Search: The left node helps maintain the BST property, allowing searches to be more efficient. By leveraging the
property that all left children contain values less than their parent, we can quickly narrow down the search space.
2. Insertion and Deletion: When inserting or deleting nodes, the position of the left node plays a crucial role in maintaining the
BST property. The new node must be placed in the correct position relative to existing nodes, and the tree may need to be
rebalanced if it becomes unbalanced.
3. Balancing: In self-balancing trees like AVL trees or Red-Black trees, the left node’s role in maintaining balance is critical. These
trees use rotations to keep the tree balanced and ensure that operations like search, insertion, and deletion remain efficient.
35. How does a binary search tree differ from a regular binary tree? Explain its advantages in terms
of search operations.
Binary Search Tree (BST): A tree data structure in which each node has a maximum of two children, such that all entries in the left subtree
are less than the node, while those of the right subtree have higher values than the node itself.
A tree data structure where each node has at A specific type of binary tree in which the left
Definition most two children, but there are no specific rules child contains values less than the parent and the
regarding the values of nodes. right child contains values greater than the parent.
Efficient Search: A search is carried out more efficiently in a BST than in an ordinary binary tree
because of its ordering property.
Logarithmic Time Complexity: In a balanced BTS, search operations exhibit a time complexity of
O(log n).
Ease of Range Queries: A BST is simple and efficient in-range queries. Navigating through the
tree following a certain order facilitates finding all elements in a certain range.
36. How is expression evaluation performed using a stack data structure? Provide an example of an
expression and step through the evaluation process.
Expression evaluation using a stack data structure is a common method for processing mathematical expressions, particularly in postfix
(Reverse Polish Notation) and infix expressions. The stack helps manage the operands and operators in a systematic way to evaluate
expressions efficiently.
Process:
Bidirectional Links: Each node contains a pointer to the next node (called next) and a pointer to the previous node (called prev).
This enables bidirectional traversal, allowing access to both the subsequent and preceding elements from any given node.
Node Structure: Each node typically consists of three parts: the data it holds, a reference to the next node, and a reference to the
previous node. The structure can be visualized as:
Head and Tail Pointers: The list has a head pointer that points to the first node and a tail pointer
that points to the last node. The prev pointer of the head node is usually set to null (or equivalent
in other languages), and the next pointer of the tail node is also set to null.
Dynamic Size: The size of a doubly-linked list can grow or shrink dynamically as elements are
added or removed, unlike arrays, which have a fixed size.
Insertion and Deletion: Insertion and deletion operations can be efficiently performed from both
ends of the list as well as from any position within the list, provided a reference to the node is
available. These operations typically involve adjusting a constant number of pointers, making them
O(1) operations in the best case.
Memory Overhead: Each node requires additional memory for storing two pointers (next and prev)
compared to a singly-linked list, which only stores one pointer per node.
38. What is an AVL tree, and how does it address the issue of maintaining balance in binary search
trees?
An AVL tree is a type of self-balancing binary search tree named after its inventors, Adelson-Velsky and Landis. It was the first
dynamically balanced binary search tree and is designed to maintain a balanced tree structure, which ensures that the tree remains balanced
at all times after insertions and deletions.
Addressing the Issue of Balance: In a standard binary search tree (BST), the structure can become unbalanced with skewed branches if
elements are inserted in a sorted order. This leads to poor performance for search, insert, and delete operations. This degradation can lead to
a worst-case time complexity of O(n) for these operations, where n is the number of nodes.
39. Explain the basic operations of a heap data structure. How does it differ from a binary search
tree?
A heap is a specialized tree-based data structure that satisfies the heap property. There are two main types of heaps: min-heaps and max-
heaps. The basic operations of heap data structures are as follows:
1. Insertion (Heapify-Up): Adds a new element to the heap and keeps the heap property by
evaluating the detail with its discern and swapping if necessary.
2. Deletion (Heapify-Down): Removes the pinnacle detail from the heap, replaces it with the final
element, and keeps the heap property by evaluating the detail with its children and swapping if
essential.
A binary tree-based data structure A binary tree where for each node, the left
Definition that satisfies the heap property subtree has values less than the node's value
(either min-heap or max-heap). and the right subtree has values greater.
O(log n) due to heapify operations O(log n) on average; can be O(n) in the worst
Insertion Complexity
after insertion. case if the tree is unbalanced.
O(log n) for removing the root (heap O(log n) for balanced BSTs; can be O(n) in
Deletion Complexity
extraction), followed by heapify. the worst case for unbalanced trees.
Complete binary tree; all levels are Binary tree; nodes are arranged based on
Structure fully filled except possibly the last, value, and the structure is not strictly
which is filled from left to right. complete.
Priority queues, heap sort, scheduling Searching, sorting, in-order traversal for
Use Cases
algorithms. ordered data, hierarchical data representation.
Traversal is not typically used or In-order traversal gives sorted order; also
Traversal
required for heaps. supports pre-order and post-order traversal.
1. Priority Queues: Heaps are typically used to implement priority queues where factors with better
precedence (or decreased priority, depending on the heap type) are dequeued first.
2. Heap Sort: Heaps can be used to implement an in-region sorting algorithm called Heap Sort.
3. Dijkstra's Shortest Path Algorithm: Heaps are applied to implement algorithms like Dijkstra's
shortest path algorithm efficiently.
4. Task Scheduling: Heaps can be hired in mission scheduling algorithms wherein duties with higher
priority are finished first.
41. Describe the structure and advantages of using an adjacency list in graph representation. Compare
it with an adjacency matrix in terms of space and time complexity.
An adjacency list represents a graph as an array of connected lists or arrays. Each detail within the array corresponds to a vertex, and the
connected listing or array related to every vertex incorporates its neighboring vertices (adjacent vertices).
Advantages:
Space Efficiency: This is especially beneficial for sparse graphs (with fewer edges) because it
stores facts about existing edges.
Memory Efficiency: Consumes less reminiscence than an adjacency matrix for sparse graphs.
Ease of Traversal: Iterating over the buddies of a vertex is simple and green.
Comparison with Adjacency Matrix:
Space Complexity:
Adjacency List: O(V E), where V is the wide variety of vertices and E is the number of edges. It's
extra green for sparse graphs.
Adjacency Matrix: O(V^2), where V is the number of vertices. It becomes more area-inefficient
for massive graphs and is suitable for dense graphs.
Time Complexity:
Adjacency List: O(V E), where V is the range of vertices and E is the variety of edges between
nodes. This illustration is efficient for traversing the entire graph or finding the friends of a
selected vertex.
Adjacency Matrix: O(V^2). Traversing the complete matrix is time-consuming, making it less
efficient for sparse graphs.
42. How are the elements of a 2D array stored in the memory?
The elements of a 2D array are stored in a contiguous block of memory, with the layout depending on the language and its specific
implementation. The two primary storage layouts for 2D arrays are row-major order and column-major order.
Row-Major Order: In row-major ordering, a 2D array’s rows are stored consecutively in memory. This means the entire first row is stored
first, followed by the entire second row, and so forth, continuing until the final row is saved.
Column-Major Order: In column-major ordering, the 2D array’s columns are stored consecutively in memory. This means the entire first
column is saved first, followed by the entire second column, and so on, until the final column is fully recorded.
43. Explain the concept of an expression tree and its significance in evaluating prefix expressions.
How does a preorder traversal help in processing the tree efficiently?
An expression tree is a binary tree used to symbolize expressions in a way that helps their evaluation. Each leaf node represents an operand,
and every non-leaf node represents an operator. The tree structure reflects the hierarchical order of operations.
In a prefix expression, the operator precedes its operands, making it hard to evaluate directly.
The expression tree allows a natural illustration of the expression's structure, simplifying the
assessment technique.
Preorder Traversal for Efficient Processing
Preorder traversal helps process the expression tree efficaciously by allowing the set of rules to go
to and compare nodes in the appropriate order. This order is primarily based on the expression's
prefix notation.
As the traversal encounters operators at the root of subtrees, it may carry out the corresponding
operation on the values of the operands.
44. Discuss the advantages and disadvantages of using a deque data structure with two-way data
access.
A deque (short for double-ended queue) is a data structure that allows insertion and deletion of elements from both the front and the back. It
combines features of both stacks and queues. Here’s a look at the advantages and disadvantages of using a deque:
Advantages:
1. Flexible Access: Deques support efficient access and modification of elements at both ends. This versatility makes them suitable
for a variety of applications where elements may need to be added or removed from either end.
2. Efficient Operations: Insertions and deletions at both ends of a deque are generally performed in constant time, O(1). This is
beneficial for algorithms that require frequent additions or removals from both ends.
3. Use Cases: Deques can be used to implement both stacks (LIFO) and queues (FIFO). This dual capability makes them versatile
for various algorithmic problems and data manipulation tasks.
4. Memory Efficiency: When implemented with a circular buffer or a doubly linked list, deques can efficiently use memory and
avoid issues like memory fragmentation that might occur with some other data structures.
5. Algorithmic Flexibility: Deques are useful in scenarios that require operations like sliding window algorithms, where elements
are continuously added and removed from both ends.
Disadvantages:
1. Complexity in Implementation: Depending on the underlying implementation (e.g., circular buffer, doubly linked list), deques
can be more complex to implement compared to simpler data structures like arrays or linked lists.
2. Memory Overhead: When using certain implementations, such as doubly linked lists, deques may incur additional memory
overhead for storing pointers (next and previous nodes), which can be less efficient in terms of space compared to contiguous
memory storage.
3. Performance Variability: In some implementations, especially those based on arrays with resizing, operations at the ends might
not always be constant time. For example, resizing the array can involve copying elements, which affects performance.
4. Cache Locality: Deques implemented with linked lists may suffer from poor cache memory locality compared to contiguous
memory structures like arrays, potentially leading to slower access times due to cache misses.
45. Differentiate between a push operation in a stack and a push operation in a costly stack. How does
the choice of push implementation impact the overall efficiency of stack-based algorithms?
Push Operation in a Stack:
Standard Push:
o Efficient and constant time.
o Ideal for maximum stack-primarily based algorithms.
Costly Push:
o It's inefficient for huge stacks.
o Results in a substantial growth in time complexity.
o It can degrade the general performance of stack-based total algorithms.
Choice of Push Implementation:
The preference depends on the algorithm's precise requirements and the facts' characteristics.
For maximum situations, a trendy push operation with constant time complexity is preferred for
retaining green stack-based algorithms.
A luxurious push operation is probably considered in unique conditions where the order of
elements wishes to be preserved, and the set of rules needs a bottom-to-pinnacle technique.
46. In the context of storage units and memory size, elaborate on the considerations for memory
utilization in a static data structure. Compare this with the memory utilization in a hash map.
Static Data Structures are data structures with a fixed size determined at compile-time. Examples include arrays and fixed-size matrices.
Fixed Size: The shape's length is predetermined and can not be altered throughout runtime.
Memory Efficiency: Memory is allocated statically, keeping off dynamic memory management
overhead.
Predictable Memory Usage: The memory footprint is thought earlier, assisting in memory-making
plans.
Comparison with a Hash Map:
1. Directory Traversal: A directory can contain files and subdirectories. Each subdirectory itself may contain files and more
subdirectories. This forms a tree-like structure where each directory is a node that can have children (files and subdirectories).
A deque (double-ended queue) is a records structure that lets in insertion and deletion at both
ends.
Two-way records get entry to a method that factors can be accessed and manipulated from the front
and the rear.
Operations encompass push and pop at each end, imparting flexibility in enforcing various
algorithms.
Effect of Left Child in a Tree Structure on Searching and Traversal
The presence of a left child in a tree shape influences the sequence of operations like looking and
postorder traversal, especially in binary timber.
During an in-order traversal (left-root-right), the left infant is visited earlier than the foundation
and its right toddler.
In a binary seek tree (BST), the left baby normally holds values smaller than the determined node,
affecting the traversal order.
Searching can be optimized primarily based on the binary seek assets, where values within the left
subtree are smaller and values within the proper subtree are larger.
In contrast, dynamic structures like linked lists, which use scattered memory locations, have elements stored at non-contiguous addresses.
This can lead to:
1. Slower Access: Accessing elements requires traversal from one node to another, resulting in O(n) time complexity for searching.
2. Cache Inefficiency: Scattered locations lead to poorer cache performance, as elements are not likely to be loaded together into
the cache.
Thus, while static structures offer better memory and access efficiency, they lack the flexibility of dynamic structures in handling variable-
sized data and frequent insertions or deletions.
1. Ordered Structure: In a BST, each node has at most two children. The left child node contains values that are less than its
parent node, while the right child node contains values that are greater. This property helps maintain an ordered structure.
2. Efficient Searching: Due to the ordering property, searching for a value can be done quickly. Starting from the root node, you
can decide whether to move to the left or right child based on the comparison of the target value with the current node’s value.
3. Dynamic Operations: BSTs support dynamic data operations. You can insert and delete nodes while maintaining the ordering
property, which ensures that searching remains efficient even as the tree grows or shrinks.
4. Sorted Data: Performing an in-order traversal (left subtree, root, right subtree) of a BST produces a sorted sequence of node
values. This makes it easy to retrieve data in a sorted manner.
51. How does the binary search method work within the context of a binary search tree?
Binary search in the context of a BST leverages the tree’s ordering properties to quickly locate a value:
1. Start at the Root: Begin the search at the root node of the BST.
2. Compare Values:
If the target value is equal to the value of the current node, you’ve found the target.
If the target value is less than the value of the current node, move to the left child node.
If the target value is greater than the value of the current node, move to the right child
node.
Continue the Search: Repeat the comparison and movement process with the next node (left or right child) until you either find
the target value or reach a leaf node (a single node with no children), indicating that the target value is not in the tree.
A self-balancing binary search tree (BST) is a type of binary tree that is used to organize and manage data in a way that facilitates efficient
searching, insertion, and deletion operations. The BST has a specific structure that makes it particularly useful for dynamic sets of data
where quick access to elements is required.
52. How do you find the maximum subarray sum using divide and conquer?
To find the maximum subarray sum using the divide and conquer approach, we can break down the problem into smaller subproblems, solve
them independently, and then combine the results. The key idea is to recursively divide the array into two halves, solve for each half, and
also consider the maximum subarray that spans the boundary between the two halves.
// Function to find the maximum subarray sum using divide and conquer
public static int maxSubarraySum(int[] array) {
return maxSubarraySum(array, 0, array.length - 1);
}
// Recursive function to find the maximum subarray sum within a range
private static int maxSubarraySum(int[] array, int left, int right) {
if (left == right) {
// Base case: only one element
return array[left];
}
// Function to find the maximum subarray sum that crosses the midpoint
private static int maxCrossingSubarray(int[] array, int left, int mid, int right) {
int leftSum = Integer.MIN_VALUE;
int rightSum = Integer.MIN_VALUE;
// Return the sum of the maximum subarray that crosses the midpoint
return leftSum + rightSum;
}
The implementation of a link-cut tree data structure involves several key concepts:
1. Splay Trees: The link-cut tree uses splay trees as its underlying data structure. Splay trees are self-balancing binary search trees
that allow for efficient access, insertion, and deletion operations.
2. Node Representation: Each node in the link-cut tree represents a vertex in the forest and has pointers to its parent and children,
as well as a path-parent pointer used during the splay operation.
3. Key Operations:
Access(v): This operation makes v the root of its represented tree in the forest, splaying v
and its ancestors in the splay tree, effectively making the preferred path containing v
accessible.
Link(v, w): Links the tree containing v to the tree containing w by making w the parent of
v. This operation requires v to be a root node.
Cut(v): Cuts the edge between v and its parent, separating v and its subtree from the rest
of the tree.
FindRoot(v): Finds the root of the tree containing v.
LCA(v, w): Finds the lowest common ancestor of v and w.
Evert(v): Makes v the root of its tree, effectively inverting the path from v to the root.
Link-cut trees are useful in several scenarios where dynamic trees need to be managed efficiently:
1. Dynamic Graph Algorithms: Link-cut trees are used to dynamically maintain the structure of a graph as edges are added or
removed, which is essential in algorithms for finding minimum spanning trees, dynamic connectivity, and program flow analysis
problems.
2. Network Design and Analysis: In telecommunications and computer networks, link-cut trees can manage network topologies,
allowing for quick adjustments to the network structure.
3. Dynamic Trees in Game Theory: Link-cut trees can be used to manage game states and transitions, especially in games
involving dynamic environments or where game trees change frequently.
4. Data Structure Optimization: They are used in optimizing data structures for basic operations that involve dynamic set updates,
such as maintaining sets of elements under union and find operations.
1. Data Structure: Each header node contains a point (x,y) and maintains a list of points in the rectangle where this node’s y-
coordinate is the maximum.
2. Building the Tree:
o Left Set: Points with x-coordinates less than the root’s x-coordinate.
o Right Set: Points with x-coordinates greater than or equal to the root’s x-
coordinate.
Recursion: Recursively build the tree for the left and right sets.
Searching: To find points within a query rectangle, traverse the tree:
o Check Node: If the query rectangle intersects with the node’s rectangle, include
the node’s points in the result.
o
Recursive Search: Recursively search the left and right subtrees if their
rectangles intersect with the query rectangle.
55. Explain the concept of a suffix tree and its applications.
A suffix tree is a specialized data structure used to efficiently store and analyze all suffixes of a given string in a program. It is particularly
useful for solving various string processing problems.
Structure:
Nodes: Each separate node in a suffix tree represents a substring of the original string. Nodes can
represent either single characters or longer substrings.
Edges: Edges between single root nodes are labelled with substrings of the original string.
Leaves: Each leaf node represents a suffix of the string, and the path from the root to the leaf
encodes the suffix.
Root: The root of the suffix tree represents the empty string.
Construction:
Build Process: Constructing a suffix tree involves inserting all suffixes of the string into a tree.
This process typically involves the use of advanced algorithms such as Ukkonen's algorithm or
McCreight's algorithm to build the tree in linear time.
Edge Labeling: The edges are labeled with substrings of the original string, and each edge label is
associated with the start and end indices of the substring in the original string.
Applications:
Code:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}