Book Title: Java Data Structures and Algorithms for Beginners
Author: Anirudha Anil Gaikwad
Co-Author: Atit Anil Gaikwad
Name of Publisher: Anirudha Anil Gaikwad
ISBN Number: 978-93-343-8254-9
Language: English
Country of Publication: India
Date of Publication: 30/08/2025
Product Form: Digital online
Product Composition: Single-component retail product
Copyright © 2025 Anirudha Anil Gaikwad
All rights reserved by the author(s). No part of this digital publication may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, electronic or mechanical, including photocopy, recording, or any information storage and retrieval system, without prior written permission of the author(s).
Design & Layout: Anirudha Anil Gaikwad
Digital Edition | eBook | Published in India
Introduction
Welcome to Java Data Structures and Algorithms for Beginners, an introductory guide to one of the most important topics in computer science — data structures — using the Java programming language.
This book, Java Data Structures and Algorithms for Beginners, is designed for students, new programmers, and anyone starting their journey into understanding how data structures work in Java. It aims to simplify concepts while providing practical examples you can run and explore.
Data structures form the backbone of efficient software. They allow you to store, organize, and manipulate data in ways that make programs faster, more reliable, and easier to maintain. Whether you’re creating a simple to-do app or building a large-scale web service, understanding the right data structure for the job is essential.
Java provides a rich collection of built-in data structures in its standard library, from basic arrays to advanced collections like HashMap and TreeSet. This book will take you step-by-step from the very basics to slightly more advanced topics, with a strong focus on clear explanations and practical coding examples.
Who This Book Is For
This book is ideal for:
Students
If you are learning Java or preparing for university/diploma-level programming courses, this book will give you a solid understanding of arrays, lists, stacks, queues, trees, graphs, and more.
Beginner Java Developers
If you know the Java syntax but feel confused when it comes to selecting or implementing the right data structure, this book will help you build confidence.
Programming Enthusiasts
Even if Java isn’t your main programming language, the concepts you learn here are transferable to many other languages like Python, C++, or Rust.
Job Seekers
If you are preparing for interviews, this book will serve as a gentle starting point before diving into more challenging algorithmic problems.
How to Use This Book
We recommend reading the chapters in sequence, as each chapter builds upon concepts introduced earlier. However, you can also jump to specific topics when needed.
You’ll find two types of chapters in this book:
- Concept Chapters – Explain a data structure’s purpose, how it works internally, its operations, and time complexity.
- Practical Chapters – Show you how to implement the data structure in Java, with examples and small exercises.
Chapter Overview
This book is designed to guide beginners through Java Data Structures and Algorithms (DSA) step-by-step.
Here’s a quick overview of what each chapter covers:
1️⃣ Getting Started with Data Structures, Algorithms, and Complexity
Learn the basics of what DSA is, why it’s important, and how it affects the efficiency of your programs.
We’ll also cover pseudocode, complexity analysis, and Big-O notation so you can evaluate algorithm performance.
2️⃣ Recursion
Understand the concept of recursion, where a function calls itself to solve smaller subproblems.
You’ll learn:
- How recursion works
- Real-world examples like factorials
- Difference between recursion and iteration
- Tail recursion and when to use it
3️⃣ Core Data Structures
Dive into the most important data structures:
- Arrays – store data in a fixed-size list
- Strings – work with text
- Linked Lists – dynamic lists with connected nodes
- Stacks & Queues – process data in LIFO/FIFO order
- Hashing – fast data lookup
- Trees & Graphs – store and connect data in hierarchical and network structures
4️⃣ Sorting Algorithms
Learn how to arrange data efficiently:
- Bubble Sort – simple but slow
- Selection Sort – select smallest/largest repeatedly
- Insertion Sort – build sorted lists step-by-step
- Merge Sort – efficient divide-and-conquer approach
5️⃣ Searching Algorithms
Discover how to find data in a collection:
- Linear Search – check every element
- Binary Search – fast search in sorted lists
6️⃣ Problem Solving with DSA
Practice solving real coding challenges:
- Array problems
- String problems
- Linked list problems
- Stack & queue problems
📎 Appendix
Extra resources to help you:
- Glossary of terms
- Pseudocode symbols
- Flowchart symbols
- Java code templates for quick development
Practice-Oriented Learning
This book takes a hands-on, practical approach to understanding data structure concepts for beginners. Rather than focusing only on theory, each chapter pairs clear explanations with step-by-step Java code examples, followed by small exercises you can try yourself. By actively implementing and experimenting with arrays, stacks, queues, and other structures, you’ll develop a stronger intuition for how they work and when to use them—turning abstract ideas into real programming skills.
By the time you finish this book, you’ll not only know how each Java data structure works but also when and why to use it — a skill that will make your programs more efficient and your problem-solving sharper.
Let’s begin your journey into the world of Java Data Structures and Algorithms!
Getting Started with Data Structures, Algorithms, and Complexity
This chapter provides a beginner-friendly overview of what Data Structures and Algorithms (DSA) are, why they are important, and how they are measured in terms of efficiency.
By the end of this chapter, you will have a clear understanding of the building blocks you’ll need before diving into each specific data structure or algorithm.
📌 What You’ll Learn in This Chapter
- What is DSA? – Understanding the combination of Data Structures and Algorithms.
- What is an Algorithm? – Step-by-step instructions to solve a problem.
- Why Learn DSA? – Benefits in programming, efficiency, and career growth.
- Applications of DSA – Real-world areas where DSA is used.
- Impact of Using vs. Not Using DSA – How it affects program performance.
- Big-O Notation – Introduction to how we measure algorithm performance.
- Common Big-O Complexities – Examples like O(1), O(log n), O(n), O(n²).
- Algorithmic Complexity – Understanding time and space usage.
- Types of Complexity – Time Complexity vs. Space Complexity.
💡 Why This Chapter Matters
Before you start coding, you need to understand why efficiency matters.
Two solutions may solve the same problem, but one could take seconds while the other takes hours.
By learning DSA fundamentals and complexity analysis, you’ll be able to:
- Choose the right approach for a problem.
- Write programs that are faster and use less memory.
- Develop skills that are highly valued in interviews and competitive programming.
🚀 Next Steps
Start with What is DSA? and gradually work through each section.
What is DSA?
DSA stands for Data Structures and Algorithms. It refers to the study and implementation of data organization (data structures) and computational procedures (algorithms) to solve problems efficiently.
- Data Structures: Ways to store and organize data in a computer so that it can be accessed and modified efficiently. Examples include arrays, linked lists, stacks, queues, trees, graphs, hash tables, etc.
- Algorithms: Step-by-step procedures or formulas for solving problems, such as sorting, searching, graph traversal, or dynamic programming.
DSA is a cornerstone of efficient programming and problem-solving, applicable in virtually every area of software development and technology. Using DSA leads to faster, scalable, and cost-effective solutions, while neglecting it risks poor performance and limited career opportunities. Its scope spans from everyday apps to cutting-edge AI, making it an essential skill for developers and engineers.
▶ Next: What is an Algorithm?
What is an Algorithm?
An algorithm is a step-by-step procedure or a set of instructions designed to perform a specific task or solve a problem.
It is like a recipe in cooking — a list of steps you follow in a specific order to achieve the desired outcome.
Characteristics of a Good Algorithm
- Clear and Unambiguous – Each step is well-defined.
- Input – Takes zero or more inputs.
- Output – Produces at least one output.
- Finiteness – Completes after a finite number of steps.
- Effectiveness – Each step is basic enough to be performed exactly and in a reasonable time.
Example (Real-World)
Making Tea Algorithm:
- Boil water.
- Add tea leaves.
- Add sugar and milk.
- Pour into a cup and serve.
Here, each step is clear, ordered, and finite — just like in programming.
Example of an Algorithm – Adding Two Numbers
Let’s take a very simple problem:
Problem:
Write an algorithm to add two numbers and display the result.
Step-by-Step Algorithm
- Start
- Input the first number.
- Input the second number.
- Add the two numbers.
- Output the result.
- End
Flowchart Representation
Flowcharts are visual diagrams that show the steps of an algorithm using shapes and arrows.
Benefits:
- Easy to understand for beginners.
- Shows the flow of logic visually.
- Useful for planning before coding.
Common Flowchart Symbols
| Symbol | Name | Purpose |
|---|---|---|
| 🔵 Oval | Start / End | Indicates the start or end of the process |
| ▭ Rectangle | Process | Shows a task, step, or action to be performed |
| 🔷 Diamond | Decision | Represents a question or condition (Yes/No) |
| ▱ Parallelogram | Input / Output | Used for reading inputs or displaying outputs |
| ➡ Arrow | Flow Line | Shows the direction of process flow |
| 🗂 Document Shape | Predefined Process | Indicates a subroutine or pre-defined process |
Explanation of the Flowchart
- Start (A): Represented by an oval, indicating the beginning of the algorithm.
- Input first number (B): A parallelogram, showing the user inputs the first number.
- Input second number (C): Another parallelogram, for inputting the second number.
- Add numbers (D): A rectangle, representing the processing step where the two numbers are added.
- Display sum (E): A parallelogram, indicating the output of the sum to the user.
- End (F): An oval, marking the end of the algorithm.
- Arrows: Show the flow of execution from one step to the next.
import java.util.Scanner;
public class AddTwoNumbers {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// Input
System.out.print("Enter first number: ");
int num1 = sc.nextInt();
System.out.print("Enter second number: ");
int num2 = sc.nextInt();
// Processing
int sum = num1 + num2;
// Output
System.out.println("Sum = " + sum);
sc.close();
}
}
🔍 Examples of Algorithms in Computer Science
- Searching: Linear Search, Binary Search
- Sorting: Bubble Sort, Merge Sort
- Graph Traversal: BFS, DFS
- Dynamic Programming: Fibonacci, Knapsack
💡 Key Takeaway
An algorithm is not code — it’s the logic and steps to solve a problem.
You can write algorithms in plain language, pseudocode, or flowcharts before turning them into Java code.
▶ Next: What is Pseudocode and Flowchart?
What is Pseudocode?
Pseudocode is a human-readable way to describe the steps of an algorithm without worrying about syntax of a specific programming language.
It is written in plain English (or any natural language) but structured like code.
Why use pseudocode?
- Easy to understand for both technical and non-technical people.
- Helps in planning before coding.
- Reduces errors in implementation.
Rules for Writing Good Pseudocode
- Use plain, simple language – avoid unnecessary jargon.
- Write one action per line – keep it clean.
- Use indentation – to represent loops or conditional blocks.
- Use standard keywords like:
START,ENDIF,ELSE,ENDIFFOR,WHILE,REPEATREAD,PRINT,RETURN
- Keep it language-independent – no need for Java, Python, or C++ syntax.
- Number your steps if sequence matters.
Pseudocode and Java Implementations
Example 1 – Find if a number is even or odd
Problem:
Write an algorithm to read a number and determine whether it is even or odd.
Pseudocode:
1. START
2. READ number N
3. IF N mod 2 = 0 THEN
PRINT "Even"
ELSE
PRINT "Odd"
4. END
import java.util.Scanner;
public class EvenOddCheck {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter a number: ");
int N = sc.nextInt();
if (N % 2 == 0) {
System.out.println("Even");
} else {
System.out.println("Odd");
}
}
}
Example 2 – Sum of Two Numbers
Problem: Read two numbers and print their sum.
Pseudocode:
START
READ A, B
SUM ← A + B
PRINT SUM
END
import java.util.Scanner;
public class SumOfTwoNumbers {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter first number: ");
int A = sc.nextInt();
System.out.print("Enter second number: ");
int B = sc.nextInt();
int SUM = A + B;
System.out.println("Sum = " + SUM);
}
}
Example 3 – IF Statement
Problem: Check if a number is positive.
Pseudocode:
START
READ num
IF num > 0 THEN
PRINT "Positive Number"
ENDIF
END
import java.util.Scanner;
public class PositiveCheck {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter a number: ");
int num = sc.nextInt();
if (num > 0) {
System.out.println("Positive Number");
}
}
}
Example 4 – IF...ELSE Statement
Problem: Check if a person is eligible to vote.
Pseudocode:
START
READ age
IF age >= 18 THEN
PRINT "Eligible to Vote"
ELSE
PRINT "Not Eligible to Vote"
ENDIF
END
import java.util.Scanner;
public class VotingEligibility {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter your age: ");
int age = sc.nextInt();
if (age >= 18) {
System.out.println("Eligible to Vote");
} else {
System.out.println("Not Eligible to Vote");
}
}
}
Example 5 – Nested IF
Problem: Find the largest among three numbers.
Pseudocode:
START
READ A, B, C
IF A > B THEN
IF A > C THEN
PRINT "A is the largest"
ELSE
PRINT "C is the largest"
ENDIF
ELSE
IF B > C THEN
PRINT "B is the largest"
ELSE
PRINT "C is the largest"
ENDIF
ENDIF
END
import java.util.Scanner;
public class LargestNumber {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter three numbers: ");
int A = sc.nextInt();
int B = sc.nextInt();
int C = sc.nextInt();
if (A > B) {
if (A > C) {
System.out.println("A is the largest");
} else {
System.out.println("C is the largest");
}
} else {
if (B > C) {
System.out.println("B is the largest");
} else {
System.out.println("C is the largest");
}
}
}
}
Example 6 – Nested FOR Loop
Problem: Print a multiplication table from 1 to 5.
Pseudocode:
START
FOR i ← 1 TO 5 DO
FOR j ← 1 TO 10 DO
PRINT i × j
ENDFOR
ENDFOR
END
public class MultiplicationTable {
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= 10; j++) {
System.out.println(i + " x " + j + " = " + (i * j));
}
System.out.println();
}
}
}
Example 7 – WHILE Loop
Problem: Print numbers from 1 to N.
Pseudocode:
START
READ N
i ← 1
WHILE i <= N DO
PRINT i
i ← i + 1
ENDWHILE
END
import java.util.Scanner;
public class PrintNumbers {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter N: ");
int N = sc.nextInt();
int i = 1;
while (i <= N) {
System.out.println(i);
i++;
}
}
}
Example 8 – REPEAT UNTIL Loop (Simulated with do-while in Java)
Problem: Keep reading numbers until the user enters 0.
Pseudocode:
START
REPEAT
READ num
PRINT num
UNTIL num = 0
END
import java.util.Scanner;
public class RepeatUntilExample {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int num;
do {
System.out.print("Enter a number (0 to stop): ");
num = sc.nextInt();
System.out.println("You entered: " + num);
} while (num != 0);
}
}
Example 9 – IF with AND/OR Condition
Problem: Check if a number is between 10 and 50.
Pseudocode:
START
READ num
IF num >= 10 AND num <= 50 THEN
PRINT "Number is in range"
ELSE
PRINT "Number is out of range"
ENDIF
END
import java.util.Scanner;
public class NumberRangeCheck {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter a number: ");
int num = sc.nextInt();
if (num >= 10 && num <= 50) {
System.out.println("Number is in range");
} else {
System.out.println("Number is out of range");
}
}
}
Example 10 – Combining IF and FOR
Problem: Print only even numbers between 1 and N.
Pseudocode:
START
READ N
FOR i ← 1 TO N DO
IF i MOD 2 = 0 THEN
PRINT i
ENDIF
ENDFOR
END
import java.util.Scanner;
public class EvenNumbers {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter N: ");
int N = sc.nextInt();
for (int i = 1; i <= N; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
▶ Next: Why Learn DSA?
Why Learn DSA?
DSA is fundamental in computer science and programming because it enables efficient problem-solving and resource management. Here’s why it’s important:
- Efficiency: DSA optimizes the use of time and space resources. For example, choosing a hash table for quick lookups or a binary search tree for sorted data retrieval reduces computation time compared to less efficient structures like unsorted arrays.
- Scalability: Well-designed data structures and algorithms handle large datasets and complex computations effectively, critical for real-world applications like databases or machine learning models.
- Problem-Solving: DSA provides reusable frameworks to tackle recurring problems, such as finding the shortest path in a graph (e.g., Dijkstra’s algorithm) or sorting data (e.g., quicksort).
- Performance: Efficient algorithms reduce runtime and memory usage, which is vital for applications requiring real-time processing, like navigation systems or gaming.
- Career Advantage: Mastery of DSA is often a key requirement in technical interviews for software engineering roles at top companies, as it demonstrates problem-solving and analytical skills.
▶ Next: Applications of DSA (Where It’s Used)
Applications of DSA (Where It’s Used)
DSA is used across various domains in computer science and software development. Its scope includes:
-
Software Development:
- Web Applications: Efficient data retrieval (e.g., databases using B-trees or hash indexes) and caching (e.g., LRU cache with hash maps and doubly linked lists).
- Mobile Apps: Optimizing battery and memory usage with compact data structures like arrays or efficient algorithms like greedy methods.
- Game Development: Pathfinding (A* algorithm), collision detection (spatial partitioning with quadtrees), and rendering optimizations.
-
System Design:
- Databases: Indexing (B-trees, hash tables) and query optimization.
- Operating Systems: Process scheduling (priority queues), memory management (linked lists), and file systems (trees).
- Networking: Routing algorithms (Dijkstra’s, Bellman-Ford) and packet scheduling (queues).
-
Artificial Intelligence and Machine Learning:
- AI: Graph algorithms for path planning in robotics or game AI (e.g., minimax with alpha-beta pruning).
- ML: Data preprocessing (sorting, hashing), clustering (k-d trees), and recommendation systems (matrix factorization, graphs).
-
Competitive Programming:
- DSA is the backbone of solving problems on platforms like LeetCode, Codeforces, or HackerRank, where efficiency is critical.
-
Big Data and Cloud Computing:
- Handling massive datasets with distributed algorithms (e.g., MapReduce) or optimized storage (e.g., Bloom filters for quick membership tests).
-
Embedded Systems and IoT:
- Resource-constrained environments rely on lightweight data structures (e.g., circular buffers) and algorithms to manage sensor data or communication.
-
Cybersecurity:
- Cryptographic algorithms (e.g., RSA, hash functions) and pattern matching for intrusion detection (e.g., KMP algorithm).
-
Everyday Tools:
- Search engines (inverted indexes, tries), GPS navigation (shortest path algorithms), and compression tools (Huffman coding).
Scope in Practice
- Tech Companies: FAANG (Google, Amazon, etc.) and startups emphasize DSA in interviews and product development for scalable systems.
- Open-Source Projects: Contributors use DSA to optimize codebases, e.g., in libraries like Apache Spark or TensorFlow.
- Research: DSA underpins advancements in fields like bioinformatics (sequence alignment with dynamic programming) or quantum computing.
▶ Next: Impact of Using vs. Not Using DSA
Impact of Using vs. Not Using DSA
If You Use DSA:
- Pros:
- Faster execution: For example, using a binary search (O(log n)) instead of a linear search (O(n)) drastically reduces search time for large datasets.
- Lower resource consumption: Efficient data structures like hash maps minimize memory usage compared to naive approaches.
- Scalable solutions: Applications can handle increased data or user load without performance degradation.
- Competitive edge: Strong DSA skills improve your ability to pass coding interviews and build robust software.
- Example: In a social media app, using a graph data structure for friend recommendations (via BFS or DFS) is much faster than scanning all user data sequentially.
If You Don’t Use DSA:
- Cons:
- Poor performance: Inefficient solutions, like using nested loops for tasks that could be optimized with a hash table, lead to slow execution.
- Scalability issues: Applications may crash or lag with large datasets due to unoptimized code.
- Higher costs: Inefficient resource usage can increase server costs in cloud-based applications.
- Limited career growth: Lack of DSA knowledge may hinder success in technical interviews or building high-performance systems.
- Example: Without DSA, a search feature in an e-commerce app might scan every product linearly, causing delays for users, whereas a trie or inverted index could provide instant results.
▶ Next: Introduction to Big-O Notation
Introduction to Big-O Notation
Big O notation is a mathematical way to describe the performance or complexity of an algorithm, focusing on how it scales with input size ( n ). It provides an upper bound on the growth rate of an algorithm’s time or space requirements, typically in the worst-case scenario. It helps compare algorithms by abstracting away constants and lower-order terms to focus on the dominant behavior as ( n ) grows large.
By ignoring constants and less significant terms, Big-O focuses only on the dominant factor that affects performance when ( n ) becomes large.
Key Points:
- Purpose: Measures efficiency time complexity (execution time) or space complexity (memory usage) of algorithms.
- Focus: Describes worst-case performance unless specified otherwise.
- Notation: Expressed as ( O(f(n)) ), where ( f(n) ) is a function describing the upper bound.
- Why ignore constants?: Because as ( n ) grows, large-scale trends matter more than small-scale differences.
📌 Example
- A simple loop from ( 1 ) to ( n ) → O(n) (linear time).
- Nested loops each running ( n ) times → O(n²) (quadratic time).
Key takeaway:
Big-O notation helps you predict scalability, not actual execution time. Two algorithms with different Big-O complexities might perform differently for small inputs, but the lower complexity will usually win for large ( n ).
▶ Next: Common Big-O Complexities
Common Big-O Complexities
Big O notation categorizes algorithms based on their growth rates. Here are the most common types, ordered from fastest (most efficient) to slowest (least efficient):
-
O(1) - Constant Time
- Execution time doesn’t depend on input size.
- Example: Accessing an array element by index.
- Graph: Flat line (no growth).
-
O(log n) - Logarithmic Time
- Time grows logarithmically with input size.
- Example: Binary search.
- Graph: Very slow growth, flattens out.
-
O(n) - Linear Time
- Time grows linearly with input size.
- Example: Linear search through an array.
- Graph: Straight line.
-
O(n log n) - Linearithmic Time
- Time grows as ( n ) times the logarithm of ( n ).
- Example: Efficient sorting algorithms like Merge Sort or Quick Sort.
- Graph: Slightly steeper than linear.
-
O(n²) - Quadratic Time
- Time grows quadratically with input size.
- Example: Nested loops, like in Bubble Sort.
- Graph: Parabolic curve.
-
O(n³) - Cubic Time
- Time grows cubically with input size.
- Example: Matrix multiplication with three nested loops.
- Graph: Steep parabolic curve.
-
O(2ⁿ) - Exponential Time
- Time doubles with each additional input.
- Example: Solving the traveling salesman problem via brute force.
- Graph: Extremely steep, quickly becomes impractical.
-
O(n!) - Factorial Time
- Time grows factorially, extremely inefficient for large ( n ).
- Example: Generating all permutations of a set.
- Graph: Explosive growth, worst-case scenario.
📊 Frequently Used Big-O Complexities
| Notation | Name | Example Algorithm |
|---|---|---|
| O(1) | Constant | Accessing an array element |
| O(log n) | Logarithmic | Binary search |
| O(n) | Linear | Iterating through a list |
| O(n log n) | Log-linear | Merge sort, Quick sort (average) |
| O(n²) | Quadratic | Bubble sort |
| O(2ⁿ) | Exponential | Recursive Fibonacci |
🔍 Related Notations
While Big-O describes the upper bound of growth rate:
- Big Theta (Θ) → Tight bound (exact growth rate).
- Big Omega (Ω) → Lower bound (best-case or minimum growth).
These are less commonly used in casual discussions but provide more precise performance characterizations.
💡 Practical Use
- Algorithm Selection: Big-O helps developers pick algorithms that scale well with large datasets.
- Space Complexity: Big-O also applies to memory usage, not just time.
- Focus on Trends: Ignore constants and small terms — large input size behavior is what matters most.
▶ Next: What is Complexity?
What is Complexity?
Complexity in computer science refers to the measure of resources an algorithm requires to solve a problem. These resources are typically time (how long the algorithm takes to run) and space (how much memory the algorithm uses). Complexity analysis helps evaluate an algorithm's efficiency, especially as the input size grows, allowing developers to choose the most suitable algorithm for a task.
What is Time Complexity?
Time complexity describes the amount of time an algorithm takes to complete as a function of the input size ( n ). It focuses on the number of operations executed, ignoring constant factors and hardware-specific details. Time complexity is usually expressed using Big O notation, which provides an upper bound on the growth rate in the worst-case scenario.
- Key Points:
- Measures how the runtime scales with input size.
- Expressed as ( O(f(n)) ), e.g., ( O(n) ), ( O(n^2) ), etc.
- Common in analyzing loops, recursive calls, or operations like comparisons and assignments.
- Example: A linear search checking each element in an array has ( O(n) ) time complexity because it may need to inspect all ( n ) elements.
What is Space Complexity?
Space complexity measures the amount of memory or storage an algorithm uses as a function of the input size ( n ). It includes both the auxiliary space (extra memory allocated during execution, like temporary arrays or recursion stacks) and the input space (memory for the input data).
- Key Points:
- Focuses on memory usage, including variables, data structures, and recursion stacks.
- Also expressed using Big O notation.
- Does not include the input size in some analyses if the algorithm modifies the input in place.
- Example: A recursive algorithm like naive Fibonacci has ( O(n) ) space complexity due to the recursion stack, even though its time complexity is ( O(2^n) ).
Examples with Java Code
Below are examples illustrating time and space complexity, with comments for clarity.
1. Linear Search (O(n) Time, O(1) Space)
public class LinearSearchExample {
public static int linearSearch(int[] array, int target) {
// Loop through each element
for (int i = 0; i < array.length; i++) {
if (array[i] == target) {
return i; // Return index if found
}
}
return -1; // Not found
}
public static void main(String[] args) {
int[] array = {10, 20, 30, 40, 50};
int target = 30;
// Time complexity: O(n) - may need to check all n elements
// Space complexity: O(1) - uses only a few variables (i, target)
int result = linearSearch(array, target);
System.out.println("Index of " + target + ": " + result);
}
}
- Time Complexity: ( O(n) ), as it may iterate through all ( n ) elements.
- Space Complexity: ( O(1) ), as it uses only a constant amount of extra memory (variables
iandtarget).
2. Merge Sort (O(n log n) Time, O(n) Space)
public class MergeSortExample {
private static void merge(int[] array, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int[] leftArray = new int[n1]; // Temporary array
int[] rightArray = new int[n2]; // Temporary array
for (int i = 0; i < n1; i++) leftArray[i] = array[left + i];
for (int j = 0; j < n2; j++) rightArray[j] = array[mid + 1 + j];
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArray[i] <= rightArray[j]) {
array[k++] = leftArray[i++];
} else {
array[k++] = rightArray[j++];
}
}
while (i < n1) array[k++] = leftArray[i++];
while (j < n2) array[k++] = rightArray[j++];
}
public static void mergeSort(int[] array, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(array, left, mid); // Recurse on left half
mergeSort(array, mid + 1, right); // Recurse on right half
merge(array, left, mid, right); // Merge results
}
}
public static void main(String[] args) {
int[] array = {12, 11, 13, 5, 6, 7};
// Time complexity: O(n log n) - log n levels of recursion, each doing O(n) work
// Space complexity: O(n) - temporary arrays for merging
mergeSort(array, 0, array.length - 1);
System.out.print("Sorted array: ");
for (int num : array) System.out.print(num + " ");
}
}
- Time Complexity: ( O(n \log n) ), as the array is divided into ( \log n ) levels, and each level involves ( O(n) ) merging work.
- Space Complexity: ( O(n) ), due to the temporary arrays (
leftArrayandrightArray) used during merging.
Key Differences Between Time and Space Complexity
- Time Complexity:
- Concerns the number of operations or runtime.
- Affected by loops, recursion, or function calls.
- Example: A nested loop over ( n ) elements results in ( O(n^2) ) time.
- Space Complexity:
- Concerns memory usage.
- Affected by variables, data structures, or recursion stacks.
- Example: Creating a new array of size ( n ) results in ( O(n) ) space.
Why Analyze Complexity?
- Scalability: Predicts how an algorithm performs with large inputs.
- Trade-offs: Helps balance time vs. space (e.g., a faster algorithm might use more memory).
- Optimization: Guides developers to choose or design efficient algorithms.
▶ Next: Common Algorithmic Complexities with Examples
Common Algorithmic Complexities with Examples
1. O(1) - Constant Time
- Example Algorithm: Accessing an element in an array by index.
- Time Complexity: O(1) - The operation takes a fixed amount of time regardless of the array size.
- Space Complexity: O(1) - No additional space is used beyond the input array.
public class ConstantTimeExample {
public static void main(String[] args) {
int[] array = {10, 20, 30, 40, 50}; // Sample array
int index = 2; // Index to access
// Accessing the element at the given index
// This operation is O(1) because it directly jumps to the memory location
// without iterating or depending on the array size.
int element = array[index];
System.out.println("Element at index " + index + ": " + element);
}
}
2. O(log n) - Logarithmic Time
- Example Algorithm: Binary search on a sorted array.
- Time Complexity: O(log n) - The search space is halved with each step.
- Space Complexity: O(1) - Only a few variables are used, no extra space proportional to input.
public class LogarithmicTimeExample {
public static int binarySearch(int[] sortedArray, int target) {
int left = 0; // Start of the search range
int right = sortedArray.length - 1; // End of the search range
// Loop until the search range is exhausted
while (left <= right) {
int mid = left + (right - left) / 2; // Calculate middle index to avoid overflow
// If target is found at mid, return the index
if (sortedArray[mid] == target) {
return mid;
}
// If target is larger, ignore left half
else if (sortedArray[mid] < target) {
left = mid + 1;
}
// If target is smaller, ignore right half
else {
right = mid - 1;
}
}
// Target not found
return -1;
}
public static void main(String[] args) {
int[] sortedArray = {2, 3, 4, 10, 40}; // Sorted array required for binary search
int target = 10;
// Binary search call
// Time complexity is O(log n) because each step halves the search space,
// e.g., for n=1024, it takes at most 10 comparisons.
int result = binarySearch(sortedArray, target);
System.out.println("Index of " + target + ": " + result);
}
}
3. O(n) - Linear Time
- Example Algorithm: Linear search in an array.
- Time Complexity: O(n) - In the worst case, it checks every element.
- Space Complexity: O(1) - Only constant extra space for variables.
public class LinearTimeExample {
public static int linearSearch(int[] array, int target) {
// Iterate through each element in the array
for (int i = 0; i < array.length; i++) {
// Check if current element matches the target
if (array[i] == target) {
return i; // Return index if found
}
}
return -1; // Return -1 if not found
}
public static void main(String[] args) {
int[] array = {10, 20, 30, 40, 50}; // Sample array
int target = 30;
// Linear search call
// Time complexity is O(n) because it may need to check all n elements
// in the worst case (target not present or at the end).
int result = linearSearch(array, target);
System.out.println("Index of " + target + ": " + result);
}
}
4. O(n log n) - Linearithmic Time
- Example Algorithm: Merge sort.
- Time Complexity: O(n log n) - Divides the array (log n levels) and merges (n work per level).
- Space Complexity: O(n) - Requires extra space for temporary arrays during merging.
public class LinearithmicTimeExample {
// Merge two sorted subarrays
private static void merge(int[] array, int left, int mid, int right) {
int n1 = mid - left + 1; // Size of left subarray
int n2 = right - mid; // Size of right subarray
int[] leftArray = new int[n1]; // Temporary array for left
int[] rightArray = new int[n2]; // Temporary array for right
// Copy data to temporary arrays
for (int i = 0; i < n1; i++) leftArray[i] = array[left + i];
for (int j = 0; j < n2; j++) rightArray[j] = array[mid + 1 + j];
// Merge the temporary arrays back into the original
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArray[i] <= rightArray[j]) {
array[k++] = leftArray[i++];
} else {
array[k++] = rightArray[j++];
}
}
// Copy remaining elements if any
while (i < n1) array[k++] = leftArray[i++];
while (j < n2) array[k++] = rightArray[j++];
}
// Recursive merge sort function
public static void mergeSort(int[] array, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2; // Find midpoint
// Recursively sort left and right halves
mergeSort(array, left, mid);
mergeSort(array, mid + 1, right);
// Merge the sorted halves
merge(array, left, mid, right);
}
}
public static void main(String[] args) {
int[] array = {12, 11, 13, 5, 6, 7}; // Sample unsorted array
// Merge sort call
// Time complexity is O(n log n): log n recursive levels, each doing O(n) work in merging.
// Space is O(n) due to temporary arrays.
mergeSort(array, 0, array.length - 1);
System.out.print("Sorted array: ");
for (int num : array) System.out.print(num + " ");
}
}
5. O(n²) - Quadratic Time
- Example Algorithm: Bubble sort.
- Time Complexity: O(n²) - Two nested loops, each running up to n times.
- Space Complexity: O(1) - Sorts in place, no extra space needed.
public class QuadraticTimeExample {
public static void bubbleSort(int[] array) {
int n = array.length;
// Outer loop for each pass
for (int i = 0; i < n - 1; i++) {
// Inner loop for comparing adjacent elements
for (int j = 0; j < n - i - 1; j++) {
// Swap if current element is greater than next
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
public static void main(String[] args) {
int[] array = {64, 34, 25, 12, 22, 11, 90}; // Sample unsorted array
// Bubble sort call
// Time complexity is O(n²) due to nested loops: outer runs n-1 times,
// inner runs up to n times per pass, leading to quadratic growth.
bubbleSort(array);
System.out.print("Sorted array: ");
for (int num : array) System.out.print(num + " ");
}
}
6. O(n³) - Cubic Time
- Example Algorithm: Naive matrix multiplication.
- Time Complexity: O(n³) - Three nested loops for n x n matrices.
- Space Complexity: O(n²) - Space for input and output matrices.
public class CubicTimeExample {
public static int[][] multiplyMatrices(int[][] A, int[][] B) {
int n = A.length; // Assume square matrices
int[][] result = new int[n][n]; // Output matrix
// Three nested loops: i for rows of A, j for columns of B, k for dot product
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
for (int k = 0; k < n; k++) {
result[i][j] += A[i][k] * B[k][j]; // Accumulate product
}
}
}
return result;
}
public static void main(String[] args) {
int[][] A = {{1, 2}, {3, 4}}; // Sample matrix A
int[][] B = {{5, 6}, {7, 8}}; // Sample matrix B
// Matrix multiplication call
// Time complexity is O(n³) because of three nested loops, each iterating n times.
// Space is O(n²) for storing the result matrix.
int[][] result = multiplyMatrices(A, B);
System.out.println("Result matrix:");
for (int[] row : result) {
for (int val : row) System.out.print(val + " ");
System.out.println();
}
}
}
7. O(2ⁿ) - Exponential Time
- Example Algorithm: Recursive Fibonacci (naive).
- Time Complexity: O(2ⁿ) - Each call branches into two more, leading to exponential calls.
- Space Complexity: O(n) - Due to recursion stack depth.
public class ExponentialTimeExample {
public static int fibonacci(int n) {
// Base cases
if (n <= 1) return n;
// Recursive calls: each branches into two
// This leads to redundant computations and exponential time.
return fibonacci(n - 1) + fibonacci(n - 2);
}
public static void main(String[] args) {
int n = 10; // Small n to avoid long computation
// Fibonacci call
// Time complexity is O(2^n) because the recursion tree has about 2^n nodes.
// Space is O(n) for the maximum recursion depth.
int result = fibonacci(n);
System.out.println("Fibonacci of " + n + ": " + result);
}
}
8. O(n!) - Factorial Time
- Example Algorithm: Generating all permutations of an array (backtracking).
- Time Complexity: O(n!) - There are n! permutations, and each is generated in O(n) time.
- Space Complexity: O(n) - Recursion stack and temporary storage.
import java.util.Arrays;
public class FactorialTimeExample {
// Helper to swap elements
private static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// Recursive function to generate permutations
public static void generatePermutations(int[] array, int start) {
if (start == array.length - 1) {
// Print permutation when base case is reached
System.out.println(Arrays.toString(array));
return;
}
// For each position, try swapping with all remaining elements
for (int i = start; i < array.length; i++) {
swap(array, start, i); // Swap
generatePermutations(array, start + 1); // Recurse
swap(array, start, i); // Backtrack
}
}
public static void main(String[] args) {
int[] array = {1, 2, 3}; // Small array (n=3 has 6 permutations)
// Permutations generation call
// Time complexity is O(n!) because there are n! possible permutations,
// and each is processed in O(n) time (printing/copying).
// Space is O(n) for recursion depth.
generatePermutations(array, 0);
}
}
Recursion
Definition and Concepts
Recursion is a programming technique where a function solves a problem by calling itself with a smaller or simpler instance of the same problem. Each recursive call processes a subset of the input until a base case is reached, which provides a direct solution without further recursion. You can visualize recursion as a stack of nested function calls, where each call handles a smaller piece of the problem, and the stack unwinds as base cases are resolved. Recursion is a powerful approach for problems with a naturally hierarchical or repetitive structure, such as tree traversals or mathematical computations. In Java, recursion relies on the call stack to manage function calls and their local variables.
Why Use It?
Recursion is used to simplify the solution to complex problems by breaking them into smaller, identical subproblems. It provides elegant and concise code for tasks like tree traversals, divide-and-conquer algorithms, and combinatorial problems. Recursion is particularly effective when a problem’s solution can be expressed in terms of itself, reducing the need for complex iterative loops. However, it requires careful design to avoid excessive memory usage or stack overflow errors.
Where to Use? (Real-Life Examples)
- File System Traversal: Operating systems use recursion to traverse directory structures, processing subdirectories and files hierarchically.
- Mathematical Computations: Recursion is used to compute factorials, Fibonacci numbers, or power functions in mathematical software.
- Tree and Graph Algorithms: Algorithms like depth-first search (DFS) or binary tree traversals (e.g., inorder, preorder) use recursion to explore nodes.
- Parsing Expressions: Compilers use recursion to parse nested expressions, such as evaluating
(2 + (3 * 4)), by breaking them into sub-expressions.
SVG Diagram
The diagram for recursion would depict a stack of function calls for computing the factorial of 4 (e.g., factorial(4)). Each call would be a rectangular box labeled factorial(n), with n decreasing from 4 to 0. Arrows would show the recursive calls downward (e.g., factorial(4) -> factorial(3) -> factorial(2) -> factorial(1) -> factorial(0)) and the return values upward (e.g., 1, 1, 2, 6, 24). The base case (factorial(0) = 1) would be highlighted. A caption would note: "Recursion breaks a problem into smaller subproblems, with each call stored on the call stack until the base case is reached."
Explain Operations
- Recursive Call: This operation involves a function calling itself with modified parameters to solve a smaller subproblem. The time complexity depends on the number of recursive calls and the work done per call.
- Base Case Check: This operation checks if the current input meets the base case condition, returning a direct result to terminate recursion. It has a time complexity of O(1).
- Parameter Modification: This operation adjusts the input parameters for the next recursive call to reduce the problem size. It typically has a time complexity of O(1).
- Stack Management: The Java call stack automatically manages recursive calls by storing each call’s state (parameters and local variables). The space complexity depends on the recursion depth.
Java Implementation
The following Java code implements two common recursive algorithms: factorial and Fibonacci.
public class RecursionExamples {
// Factorial: Computes n! recursively
public int factorial(int n) {
if (n < 0) { // Checks for invalid input
throw new IllegalArgumentException("Factorial is not defined for negative numbers.");
}
if (n == 0 || n == 1) { // Base case: 0! = 1, 1! = 1
return 1;
}
return n * factorial(n - 1); // Recursive case: n! = n * (n-1)!
}
// Fibonacci: Computes the nth Fibonacci number recursively
public int fibonacci(int n) {
if (n < 0) { // Checks for invalid input
throw new IllegalArgumentException("Fibonacci is not defined for negative numbers.");
}
if (n == 0) { // Base case: F(0) = 0
return 0;
}
if (n == 1) { // Base case: F(1) = 1
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2); // Recursive case: F(n) = F(n-1) + F(n-2)
}
}
How It Works
- Factorial Implementation:
- The method checks if the input
nis negative, throwing an exception if true. - If
nis 0 or 1 (base case), it returns 1. - Otherwise, it recursively calls
factorial(n-1)and multiplies the result byn. - For example,
factorial(4)computes4 * factorial(3), which computes3 * factorial(2), and so on, untilfactorial(0)returns 1.
- The method checks if the input
- Fibonacci Implementation:
- The method checks if the input
nis negative, throwing an exception if true. - If
nis 0 (base case), it returns 0; ifnis 1 (base case), it returns 1. - Otherwise, it recursively calls
fibonacci(n-1)andfibonacci(n-2)and returns their sum. - For example,
fibonacci(5)computesfibonacci(4) + fibonacci(3), which further breaks down until base cases are reached.
- The method checks if the input
- Stack Management: Each recursive call is pushed onto the Java call stack with its parameters and local variables. When a base case is reached, the stack unwinds, combining results to produce the final answer.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Factorial | O(n) | O(n) |
| Fibonacci | O(2^n) | O(n) |
| Base Case Check | O(1) | O(1) |
| Recursive Call | Varies | O(1) per call |
Note:
- Factorial has O(n) time complexity due to n recursive calls, each performing O(1) work.
- Fibonacci has O(2^n) time complexity due to the exponential growth of recursive calls (two per level).
- Space complexity for both is O(n) due to the maximum depth of the call stack.
Key Differences / Notes
- Recursion vs. Iteration:
- The implementation above uses recursion for simplicity and clarity. Iterative solutions (e.g., loops) can solve the same problems with O(1) space complexity but may be less intuitive for hierarchical problems.
- Tail Recursion: Java does not optimize tail recursion, so recursive calls consume stack space. Tail-recursive algorithms (where the recursive call is the last operation) can be optimized in other languages but not in Java.
- Memoization: The Fibonacci implementation is inefficient due to redundant calculations. Memoization (caching results) can reduce its time complexity to O(n).
- Java’s Stack Limitations: Deep recursion can cause a
StackOverflowError. Use iteration or increase the stack size for large inputs.
Static Storage Allocation in Recursion
Static storage allocation refers to memory allocation that is fixed at compile time and remains constant throughout a program’s execution. In the context of recursion, static storage allocation applies to variables declared as static in Java, which are shared across all calls to a recursive function. These variables are allocated once in the program’s data segment and persist for the program’s lifetime. For recursive algorithms, static variables can be used to store shared state across recursive calls, such as a counter or accumulator, but they are not typically used for local variables in recursion. In the provided factorial and fibonacci implementations, static storage allocation is not used, as each recursive call relies on local variables stored on the call stack. Static allocation is memory-efficient for shared data but can lead to issues in recursive functions if not managed carefully, as all recursive calls access the same variable, potentially causing unintended side effects.
Dynamic Storage Allocation
Dynamic storage allocation refers to memory allocation that occurs at runtime, typically on the heap or call stack, and can vary in size during program execution. In recursion, dynamic storage allocation is critical because each recursive call allocates memory on the Java call stack for its local variables, parameters, and return address. In the provided factorial implementation, each call allocates space for the parameter n and temporary results. In the fibonacci implementation, each call allocates space for n and intermediate sums. This dynamic allocation enables recursion to handle varying problem sizes but increases space complexity, as the stack grows with each recursive call until the base case is reached. Excessive recursion depth can lead to a StackOverflowError if the stack size is exceeded. Dynamic allocation is essential for recursion but requires careful design to manage memory usage effectively
✅ Tip: Use recursion for problems with a natural recursive structure, like tree traversals or divide-and-conquer algorithms. For efficiency, consider memoization or iteration for problems with overlapping subproblems, like Fibonacci.
⚠ Warning: Be cautious with deep recursion in Java, as it can lead to a
StackOverflowErrorfor large inputs due to dynamic allocation on the call stack. Always ensure base cases are reachable and consider iterative alternatives for performance-critical applications.
Exercises
- Recursive Sum of Array: Write a Java program that uses recursion to compute the sum of elements in an array. Test it with arrays of different sizes.
- Reverse a String Recursively: Create a recursive method to reverse a string by breaking it into smaller substrings. Test with various strings, including empty and single-character cases.
- Binary Search Recursion: Implement a recursive binary search algorithm for a sorted array. Test it with arrays containing both existing and non-existing elements.
- Factorial with Memoization: Modify the factorial implementation to use memoization (e.g., with a HashMap) to cache results. Compare its performance with the original for large inputs.
- Tower of Hanoi: Write a recursive program to solve the Tower of Hanoi problem for n disks. Print the sequence of moves and test with different values of n.
Factorial Example using Recursion
What is Factorial?
The factorial of a number n (written as n!) is the product of all positive integers from 1 to n.
Example: 5! = 5 × 4 × 3 × 2 × 1 = 120
Why use Recursion for Factorial?
- Factorial definition is naturally recursive:
n! = n × (n-1)! - Short and clean code compared to iterative loops.
Where to use Factorial (Real-world scenarios)
- Probability & statistics (e.g., permutations and combinations)
- Scientific calculations
- Combinatorics problems
Java Example – Recursive Factorial
public class FactorialRecursion {
// Recursive function to find factorial
static int factorial(int n) {
if (n == 0) { // Base case: factorial of 0 is 1
return 1;
}
return n * factorial(n - 1); // Recursive case
}
public static void main(String[] args) {
int num = 5;
System.out.println(num + "! = " + factorial(num));
}
}
5! = 120
How it works:
- You call factorial(5) in main().
- Since n is not 0, it does 5 × factorial(4).
- To find factorial(4), it does 4 × factorial(3).
- This keeps going until factorial(0) is called.
- factorial(0) returns 1 (base case).
- Now Java calculates: factorial(1) → 1 × 1 = 1 factorial(2) → 2 × 1 = 2 factorial(3) → 3 × 2 = 6 factorial(4) → 4 × 6 = 24 factorial(5) → 5 × 24 = 120
Recursion vs. Iteration
Definition and Concepts
Recursion and iteration are two fundamental programming techniques for solving repetitive problems. Recursion involves a function calling itself with a smaller or simpler instance of the same problem until a base case is reached, which provides a direct solution. Each recursive call is managed on the call stack, storing parameters and local variables. Iteration, in contrast, uses loops (e.g., for or while loops) to repeatedly execute a block of code until a condition is met, relying on variables to track state within a single function. You can visualize recursion as a stack of nested function calls that unwind, while iteration is a single loop cycling through updates. Both approaches can solve similar problems, but they differ in code structure, memory usage, and readability.
Why Use It?
Recursion is used to simplify the solution to problems with a naturally hierarchical or self-referential structure, such as tree traversals or divide-and-conquer algorithms, offering concise and elegant code. Iteration is used when performance and memory efficiency are critical, as it avoids the overhead of multiple function calls and stack management. Choosing between recursion and iteration depends on the problem’s structure, performance requirements, and readability preferences. For example, recursion excels in problems like factorial computation for clarity, while iteration is preferred for large inputs to avoid stack overflow.
Where to Use? (Real-Life Examples)
- Recursion in Tree Traversal: File systems use recursion to traverse directory trees, processing subdirectories hierarchically, as recursion naturally handles nested structures.
- Iteration in Data Processing: Data pipelines use iteration to process large datasets, such as summing values in a list, for efficiency and minimal memory usage.
- Recursion in Parsing: Compilers use recursion to parse nested expressions, like
(2 + (3 * 4)), breaking them into sub-expressions. - Iteration in Simulations: Game loops use iteration to update game states repeatedly, such as moving objects in a physics simulation, avoiding stack overhead.
SVG Diagram
The diagram would depict two side-by-side representations for computing factorial(4). On the left, recursion would show a stack of function calls (factorial(4) -> factorial(3) -> factorial(2) -> factorial(1)), with arrows indicating recursive calls downward and return values (1, 2, 6, 24) upward. On the right, iteration would show a single loop with a variable result updating (1, 2, 6, 24) over iterations. A caption would note: "Recursion uses a call stack for nested calls, while iteration updates state in a loop, avoiding stack overhead."
Explain Operations
- Recursive Call (Recursion): This operation involves a function calling itself with modified parameters to solve a smaller subproblem. It has a time complexity dependent on the number of calls and work per call.
- Base Case Check (Recursion): This operation checks if the input meets a condition to terminate recursion, returning a direct result. It has a time complexity of O(1).
- Loop Execution (Iteration): This operation repeatedly executes a block of code, updating variables until a condition is met. It has a time complexity dependent on the number of iterations.
- State Update (Iteration): This operation modifies variables within a loop to track progress. It typically has a time complexity of O(1) per update.
- Stack Management (Recursion): The Java call stack manages recursive calls, storing state for each call. It contributes to space complexity based on recursion depth.
Java Implementation
The following Java code implements the factorial algorithm using both recursion and iteration for comparison.
public class RecursionVsIteration {
// Recursive Factorial: Computes n! recursively
public int factorialRecursive(int n) {
if (n < 0) { // Checks for invalid input
throw new IllegalArgumentException("Factorial is not defined for negative numbers.");
}
if (n == 0 || n == 1) { // Base case: 0! = 1, 1! = 1
return 1;
}
return n * factorialRecursive(n - 1); // Recursive case: n! = n * (n-1)!
}
// Iterative Factorial: Computes n! iteratively
public int factorialIterative(int n) {
if (n < 0) { // Checks for invalid input
throw new IllegalArgumentException("Factorial is not defined for negative numbers.");
}
int result = 1; // Initializes result for factorial
for (int i = 1; i <= n; i++) { // Loops from 1 to n
result *= i; // Updates result by multiplying with i
}
return result; // Returns final result
}
}
How It Works
- Recursive Factorial:
- The method checks if the input
nis negative, throwing an exception if true. - If
nis 0 or 1 (base case), it returns 1. - Otherwise, it recursively calls
factorialRecursive(n-1)and multiplies the result byn. - For example,
factorialRecursive(4)computes4 * factorialRecursive(3), which computes3 * factorialRecursive(2), untilfactorialRecursive(1)returns 1, yielding 24. - Each call is pushed onto the call stack, consuming memory until the base case is reached.
- The method checks if the input
- Iterative Factorial:
- The method checks if the input
nis negative, throwing an exception if true. - It initializes a
resultvariable to 1 and uses a for loop to multiplyresultby each integer from 1 ton. - For example,
factorialIterative(4)iterates withresult = 1 * 1 * 2 * 3 * 4, yielding 24. - The loop uses a single function frame, avoiding additional stack memory.
- The method checks if the input
- Comparison: Recursion creates multiple stack frames, increasing space complexity, while iteration uses a single frame, making it more memory-efficient.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Recursive Factorial | O(n) | O(n) |
| Iterative Factorial | O(n) | O(1) |
| Base Case Check (Recursion) | O(1) | O(1) |
| Loop Execution (Iteration) | O(1) per iteration | O(1) |
| Stack Management (Recursion) | O(1) per call | O(n) |
Note:
- Both recursive and iterative factorial have O(n) time complexity due to n multiplications.
- Recursive factorial has O(n) space complexity due to n stack frames.
- Iterative factorial has O(1) space complexity, as it uses only a few variables.
Key Differences / Notes
- Code Readability:
- Recursion offers concise, elegant code for problems with self-similar structures, like the recursive factorial, which mirrors the mathematical definition (n! = n * (n-1)!).
- Iteration may require more lines of code but is often clearer for linear tasks, like the iterative factorial, avoiding stack management.
- Performance:
- Recursion incurs overhead from multiple function calls and stack management, which can lead to
StackOverflowErrorfor deep recursion in Java. - Iteration avoids this overhead, making it faster and more memory-efficient for large inputs.
- Recursion incurs overhead from multiple function calls and stack management, which can lead to
- Tail Recursion: Java does not optimize tail recursion, so recursive calls always consume stack space. Iterative solutions are preferred in Java for deep recursion.
- Use Cases:
- Recursion is ideal for hierarchical problems (e.g., tree traversals, divide-and-conquer).
- Iteration is better for linear or sequential tasks (e.g., summing arrays, simple loops).
✅ Tip: Choose recursion for problems with a natural recursive structure, like tree traversals or factorial, to improve readability. Use iteration for performance-critical tasks or large inputs to minimize memory usage and avoid stack overflow.
⚠ Warning: Deep recursion in Java can cause a
StackOverflowErrordue to limited stack size. Always ensure recursive base cases are reachable and consider iterative alternatives for large-scale problems to optimize performance.
Exercises
- Recursive vs. Iterative Sum: Write a Java program that computes the sum of an array using both recursive and iterative approaches. Compare their performance with large arrays.
- Reverse String Comparison: Implement string reversal using both recursion and iteration. Test with various strings and compare code readability and execution time.
- Power Function: Create recursive and iterative methods to compute x^n (x raised to the power n). Test with different inputs and analyze their space complexity.
- Fibonacci Optimization: Modify the recursive Fibonacci algorithm to use memoization, then compare it with an iterative Fibonacci implementation for performance on large inputs.
- Linked List Traversal: Using a singly linked list, implement recursive and iterative methods to traverse and print the list. Test both approaches and discuss their trade-offs.
Tail Recursion
Definition and Concepts
Tail recursion is a special form of recursion where the recursive call is the last operation in the function, meaning no additional computation is performed after the recursive call returns. This allows certain programming languages to optimize tail-recursive calls by reusing the current function’s stack frame, avoiding the creation of new stack frames for each call. In a tail-recursive function, the result of the recursive call is directly returned, often with an accumulator parameter to track intermediate results. You can visualize tail recursion as a loop-like process where each recursive call updates the state and passes it to the next call until a base case is reached. In Java, however, tail recursion optimization is not supported, so tail-recursive functions still consume stack space, behaving like regular recursion.
Why Use It?
Tail recursion is used to write recursive algorithms that can be optimized in languages that support tail call optimization (TCO), such as functional programming languages like Scala or Haskell, to prevent stack overflow and improve performance. It provides a clean, recursive approach to problems that might otherwise require iteration, maintaining readability while potentially achieving loop-like efficiency in TCO-supporting environments. In Java, tail recursion is less common due to the lack of TCO, but understanding it is valuable for designing recursive algorithms and preparing for languages or systems that optimize it. Tail recursion is particularly useful for problems with linear recursive structures, such as factorial or list traversal.
Where to Use? (Real-Life Examples)
- Functional Programming: Languages like Scala use tail recursion to process large datasets, such as summing a list, to avoid stack overflow in recursive algorithms.
- Compiler Optimizations: Compilers for functional languages use tail recursion to optimize recursive parsing of expressions, like evaluating nested function calls.
- Event Loops in Simulations: Tail recursion can model event loops in simulations, such as cycling through states in a state machine, in TCO-supporting environments.
- Recursive Data Processing: Tail recursion is used in processing pipelines, like traversing a linked list to compute its length, in systems where TCO is available.
SVG Diagram
The diagram for tail recursion would depict a stack of function calls for computing the factorial of 4 using tail recursion (e.g., factorialTail(4, 1)). Each call would be a rectangular box labeled factorialTail(n, acc), with n decreasing from 4 to 1 and acc (accumulator) updating (1, 4, 12, 24). Arrows would show recursive calls downward, with the base case (n == 1) returning the accumulator (24). A note would indicate that in TCO-supporting languages, the stack frame is reused, but in Java, new frames are created. A caption would note: "Tail recursion places the recursive call as the last operation, enabling stack frame reuse in TCO-supporting languages."
Explain Operations
- Tail-Recursive Call: This operation involves the function calling itself as its last action, passing updated parameters (often an accumulator) to the next call. The time complexity depends on the number of calls and work per call.
- Base Case Check: This operation checks if the input meets a condition to terminate recursion, returning the accumulator or result directly. It has a time complexity of O(1).
- Accumulator Update: This operation updates an accumulator parameter to track intermediate results, avoiding post-call computations. It typically has a time complexity of O(1).
- Stack Management: In TCO-supporting languages, the stack frame is reused for tail-recursive calls, resulting in O(1) space complexity. In Java, each call creates a new stack frame, leading to O(n) space complexity.
Java Implementation
The following Java code implements a tail-recursive factorial algorithm, using an accumulator to make it tail-recursive. A helper method is used to manage the accumulator.
public class TailRecursionExamples {
// Factorial: Wrapper method for tail-recursive factorial
public int factorial(int n) {
if (n < 0) { // Checks for invalid input
throw new IllegalArgumentException("Factorial is not defined for negative numbers.");
}
return factorialTail(n, 1); // Calls tail-recursive helper with initial accumulator
}
// FactorialTail: Tail-recursive helper method for factorial
private int factorialTail(int n, int acc) {
if (n == 0 || n == 1) { // Base case: returns accumulator when n is 0 or 1
return acc;
}
return factorialTail(n - 1, n * acc); // Tail-recursive call with updated accumulator
}
}
How It Works
- Factorial Wrapper Method:
- The
factorialmethod validates the inputn, throwing an exception if negative. - It calls the
factorialTailhelper method withnand an initial accumulator value of 1.
- The
- Tail-Recursive Factorial:
- The
factorialTailmethod checks ifnis 0 or 1 (base case), returning the accumulatoraccif true. - Otherwise, it makes a tail-recursive call to
factorialTailwithn-1and an updated accumulatorn * acc. - For example,
factorialTail(4, 1)callsfactorialTail(3, 4), thenfactorialTail(2, 12), thenfactorialTail(1, 24), which returns 24. - In Java, each call creates a new stack frame, unlike TCO-supporting languages where the stack frame would be reused.
- The
- Stack Management: In Java, the call stack grows with each recursive call, storing
nandaccfor each frame. When the base case is reached, the stack unwinds, returning the final accumulator value.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity (Java) | Space Complexity (TCO) |
|---|---|---|---|
| Factorial (Tail) | O(n) | O(n) | O(1) |
| Base Case Check | O(1) | O(1) | O(1) |
| Accumulator Update | O(1) | O(1) | O(1) |
| Tail-Recursive Call | O(1) per call | O(1) per call | O(1) |
Note:
- Time complexity is O(n) due to n recursive calls, each performing O(1) work.
- In Java, space complexity is O(n) due to n stack frames.
- In TCO-supporting languages, space complexity is O(1) due to stack frame reuse.
Key Differences / Notes
- Tail Recursion vs. Regular Recursion:
- The implementation above uses tail recursion, where the recursive call is the last operation, enabling potential optimization in TCO-supporting languages.
- Regular recursion (e.g.,
n * factorial(n-1)) performs computations after the recursive call, requiring stack frames to store intermediate results.
- Java’s Lack of TCO: Java does not optimize tail recursion, so tail-recursive functions consume O(n) stack space, similar to regular recursion. Use iteration in Java for deep recursion.
- TCO-Supporting Languages: Languages like Scala or Haskell optimize tail recursion, making it as efficient as iteration in terms of space complexity.
- Accumulator Usage: Tail recursion often uses an accumulator to pass intermediate results, avoiding post-call computations and simplifying the function’s structure.
✅ Tip: Use tail recursion when designing recursive algorithms for clarity, especially if targeting languages with tail call optimization. In Java, consider converting tail-recursive functions to iterative versions for better performance with large inputs.
⚠ Warning: In Java, tail recursion offers no performance benefit over regular recursion due to the lack of tail call optimization. Deep tail recursion can still cause a
StackOverflowError, so use iteration for large-scale problems.
Exercises
- Tail-Recursive Sum: Write a Java program that computes the sum of an array using a tail-recursive function with an accumulator. Test it with arrays of different sizes.
- Tail-Recursive List Length: Implement a tail-recursive method to compute the length of a singly linked list. Compare it with an iterative version for readability and performance.
- Tail-Recursive Power: Create a tail-recursive method to compute x^n (x raised to the power n) using an accumulator. Test with various inputs and compare with an iterative approach.
- Reverse String Tail Recursion: Write a tail-recursive method to reverse a string using an accumulator to build the result. Test with different strings and discuss Java’s stack usage.
- Factorial Conversion: Convert the tail-recursive factorial implementation to an iterative version. Test both versions with large inputs and measure stack usage (e.g., by catching
StackOverflowError).
Core Data Structures
What are Core Data Structures?
Core data structures are the basic ways of organizing and storing data so that it can be used efficiently.
They are the building blocks of all programs — from a simple calculator app to large-scale social media platforms.
Why Learn Core Data Structures?
- Efficiency: Choosing the right data structure can make programs run much faster.
- Problem Solving: Many real-world problems map directly to these structures.
- Foundation for Advanced DSA: You can’t master algorithms without knowing how data is stored and accessed.
- Interview Readiness: Core data structures are a favorite topic in technical interviews.
📖 What You Will Learn in This Chapter
This chapter introduces you to the most commonly used data structures, their uses, advantages, disadvantages, and Java implementations.
1. Array
- What is an Array and how it works.
- How to store and access elements using indexes.
- Limitations of arrays.
- Real-life example: Managing a list of student marks.
2. Strings
- How strings store sequences of characters.
- String manipulation in Java.
- Common operations like concatenation, searching, and substring extraction.
- Real-life example: Checking if a password contains a special character.
3. Linked Lists
- How nodes are connected using references.
- Singly vs Doubly linked lists.
- Adding, deleting, and traversing nodes.
- Real-life example: Music playlist navigation.
4. Stacks
- Last In, First Out (LIFO) principle.
- Push, Pop, Peek operations.
- Real-life example: Undo/Redo feature in editors.
5. Queues
- First In, First Out (FIFO) principle.
- Enqueue and Dequeue operations.
- Variations like Circular Queue and Priority Queue.
- Real-life example: Ticket booking system.
6. Hashing
- Storing and retrieving data using key–value pairs.
- Hash functions and hash tables.
- Handling collisions.
- Real-life example: Fast dictionary lookup.
7. Trees
- Hierarchical data representation.
- Types of trees: Binary, Binary Search Tree (BST), etc.
- Traversal methods (Inorder, Preorder, Postorder).
- Real-life example: Folder structure in your computer.
8. Graphs
- Nodes (vertices) and connections (edges).
- Directed vs Undirected graphs.
- Representations: Adjacency Matrix and Adjacency List.
- Real-life example: Social network connections.
🛠 How We Will Learn
For each data structure, we will cover:
- Definition – What it is.
- Why – Advantages and limitations.
- Where to Use – Real-life scenarios.
- Java Implementation – With step-by-step explanations.
- Complexity Analysis – Understanding performance.
Key Takeaway
By the end of this chapter, you will be able to:
- Identify the right data structure for a problem.
- Implement it in Java.
- Analyze its performance.
- Use it in real-world applications.
Array
Definition and Concepts
An array in Java is a fixed-size, ordered collection of elements of the same data type, stored in contiguous memory locations. Each element is accessed using an index, starting from 0. Arrays are one of the simplest and most fundamental data structures, providing direct access to elements through their indices. You can visualize an array as a row of boxes, where each box holds a value and is labeled with an index. In Java, arrays are objects, created using the new keyword, and their size is set at initialization and cannot be changed. Arrays are versatile and used for storing lists of data, such as numbers or objects, when the size is known in advance.
Key Points:
- Arrays store elements of a single data type (e.g.,
int,String). - Elements are accessed using an index (e.g.,
arr[0]for the first element). - Arrays are fixed-size, meaning their length cannot be modified after creation.
- Declaration uses square brackets:
dataType[] arrayName;. - Memory allocation uses
new:arrayName = new dataType[size];.
Why Use It?
Arrays are used to store and manipulate a fixed number of elements efficiently, offering O(1) time complexity for accessing and modifying elements due to their contiguous memory layout. They are ideal for scenarios where the data size is known and constant, providing a simple and fast way to organize data. Arrays serve as the foundation for many algorithms and data structures, such as sorting, searching, and matrix operations, due to their straightforward indexing and predictable performance.
Where to Use? (Real-Life Examples)
- Data Storage: Spreadsheets use arrays to store numerical data in cells, enabling quick calculations and lookups.
- Image Processing: Image processing applications use arrays to store pixel intensities for grayscale images, with each element representing a pixel value.
- Game Development: Games use arrays to store player scores, level data, or coordinates, allowing fast access during gameplay.
- Algorithm Implementation: Sorting and searching algorithms, like quicksort or binary search, use arrays to process ordered or unordered data efficiently.
Explain Operations
- Initialization: This operation allocates memory for an array of a specified size and data type. It has a time complexity of O(n), where n is the array size.
- Access Element: This operation retrieves an element at a specific index. It has a time complexity of O(1).
- Modify Element: This operation updates an element at a specific index. It has a time complexity of O(1).
- Traverse Array: This operation visits all elements in the array, typically using a loop. It has a time complexity of O(n).
- Get Length: This operation returns the size of the array. It has a time complexity of O(1).
Java Implementation
The following Java code demonstrates common array operations, including initialization, access, modification, traversal, and getting the length.
public class ArrayExamples {
// Initialize: Creates an array with a specified size
public int[] initializeArray(int size) {
if (size <= 0) {
throw new IllegalArgumentException("Array size must be positive.");
}
return new int[size]; // Allocates array of specified size
}
// Access Element: Retrieves an element at a given index
public int accessElement(int[] array, int index) {
if (array == null || index < 0 || index >= array.length) {
throw new IllegalArgumentException("Invalid index.");
}
return array[index]; // Returns element at specified index
}
// Modify Element: Updates an element at a given index
public void modifyElement(int[] array, int index, int value) {
if (array == null || index < 0 || index >= array.length) {
throw new IllegalArgumentException("Invalid index.");
}
array[index] = value; // Updates element at specified index
}
// Traverse Array: Visits all elements in the array
public void traverseArray(int[] array) {
if (array == null || array.length == 0) {
throw new IllegalArgumentException("Array is null or empty.");
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " "); // Prints each element
}
System.out.println(); // New line after traversal
}
// Get Length: Returns the size of the array
public int getLength(int[] array) {
if (array == null) {
throw new IllegalArgumentException("Array is null.");
}
return array.length; // Returns the array length
}
}
How It Works
- Initialization:
- The
initializeArraymethod allocates an array of the specified size usingnew int[size]. All elements are initialized to 0 forintarrays. - For example,
initializeArray(5)creates an array[0, 0, 0, 0, 0].
- The
- Access Element:
- The
accessElementmethod retrieves the value atarray[index]after validating the index to prevent exceptions. - For example,
accessElement(array, 2)returns the element at index 2.
- The
- Modify Element:
- The
modifyElementmethod updatesarray[index]with the givenvalueafter validating the index. - For example,
modifyElement(array, 2, 8)sets the element at index 2 to 8.
- The
- Traverse Array:
- The
traverseArraymethod uses a loop to visit each element, printing them in sequence. - For example, traversing
[5, 3, 8, 1, 9]prints "5 3 8 1 9".
- The
- Get Length:
- The
getLengthmethod returnsarray.length, the size of the array. - For example,
getLength(array)for an array of 5 elements returns 5.
- The
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Initialization | O(n) | O(n) |
| Access Element | O(1) | O(1) |
| Modify Element | O(1) | O(1) |
| Traverse Array | O(n) | O(1) |
| Get Length | O(1) | O(1) |
Note:
- n is the number of elements in the array.
- Space complexity for initialization accounts for the memory allocated for all elements.
Key Differences / Notes
- Array vs. ArrayList:
- Arrays are fixed-size, while
ArrayListsupports dynamic resizing, making it suitable for collections that grow or shrink. - Arrays offer O(1) access and modification, while
ArrayListhas similar performance but with additional overhead for resizing.
- Arrays are fixed-size, while
- Array vs. Jagged/Multidimensional Arrays:
- A simple array is one-dimensional, while multidimensional arrays (e.g., 2D, 3D) organize data in multiple dimensions, and jagged arrays allow rows of varying lengths.
- Multidimensional arrays are used for grid-like data, while simple arrays are for linear lists.
- Memory Allocation:
- Arrays allocate contiguous memory, ensuring fast access but requiring a fixed size at creation.
- Primitive vs. Reference Types:
- Arrays can store primitive types (e.g.,
int,double) or reference types (e.g.,String, objects), with null initialization for reference types.
- Arrays can store primitive types (e.g.,
✅ Tip: Use arrays when the size of the data is fixed and known in advance to leverage their O(1) access and modification times. For simple linear data, arrays are more efficient than dynamic structures like
ArrayList.
⚠ Warning: Always validate array indices before accessing or modifying elements to prevent
ArrayIndexOutOfBoundsException. Avoid using arrays for collections that require frequent resizing, as this requires creating a new array and copying elements.
Exercises
- Array Reversal: Write a Java program that reverses an array in-place (without using extra space). Test with arrays of different sizes.
- Maximum Element: Implement a method to find the maximum element in an array. Test with arrays containing positive and negative numbers.
- Array Rotation: Create a program that rotates an array by k positions to the left. Test with different values of k and array sizes.
- Duplicate Finder: Write a method to check if an array contains duplicate elements. Test with sorted and unsorted arrays.
- Array Sorting: Implement a simple sorting algorithm (e.g., bubble sort) to sort an array in ascending order. Test with various input arrays.
Multidimensional Array
Definition and Concepts
A multidimensional array in Java is an array of arrays. It allows storing data in a tabular (row-column) format or even higher dimensions. You can visualize a multidimensional array as a grid for two-dimensional (2D) arrays, where each cell is accessed by row and column indices, or as a cube for three-dimensional (3D) arrays, with additional depth indices. This structure is ideal for representing structured data, such as matrices, tensors, or multi-level datasets. In Java, multidimensional arrays are implemented as arrays of references to other arrays, with each dimension adding a level of nesting. They are fixed-size, meaning dimensions are set during initialization and cannot be resized dynamically.
What is a Multidimensional Array?
A multidimensional array in Java is an array of arrays. It allows storing data in a tabular (row-column) format or even higher dimensions.
Key Points:
- Two-dimensional (2D) arrays store data in a matrix format (rows and columns).
- Three-dimensional (3D) arrays extend the concept further.
- Each dimension is represented as an array of arrays.
- Elements are accessed using multiple indices (e.g.,
arr[row][col]for 2D arrays).
Declaration and Initialization
A multidimensional array is declared using multiple square brackets [ ].
dataType[][] arrayName; // 2D Array declaration
dataType[][][] arrayName; // 3D Array declaration
Memory Allocation (Instantiation)
Once declared, memory needs to be allocated before usage.
arrayName = new dataType[rows][columns]; // 2D Array
arrayName = new dataType[depth][rows][columns]; // 3D Array
Multidimensional arrays provide efficient O(1) access to elements but require careful initialization to avoid null pointer exceptions. Higher-dimensional arrays (e.g., 3D or beyond) are less common but follow the same principle of nested arrays.
Why Use It?
Multidimensional arrays are used to organize data in a structured, grid-like format for applications requiring tabular or hierarchical data representation, such as matrices or 3D models. They offer fast O(1) access and modification times due to contiguous memory allocation, making them suitable for performance-critical tasks. Multidimensional arrays are ideal when the data size is known in advance and does not require dynamic resizing, providing a simple and efficient way to handle multi-level data.
Where to Use? (Real-Life Examples)
- Matrix Operations: Mathematical software uses multidimensional arrays to perform operations like matrix multiplication or determinant calculation in linear algebra.
- Image Processing: Image processing applications use 2D arrays to store pixel values, where each element represents a color or intensity at a specific row and column.
- Game Development: Board games or grid-based simulations use 2D arrays to represent game states, such as a chessboard or a grid-based maze.
- Scientific Simulations: Physics engines use 3D arrays to model spatial data, such as temperature distributions in a 3D space or voxel grids in simulations.
Explain Operations
- Initialization: This operation allocates memory for a multidimensional array by specifying the size of each dimension. It has a time complexity of O(n), where n is the total number of elements across all dimensions.
- Access Element: This operation retrieves an element at a specific set of indices (e.g.,
arr[row][col]for a 2D array). It has a time complexity of O(1). - Modify Element: This operation updates an element at a specific set of indices. It has a time complexity of O(1).
- Traverse Array: This operation visits all elements in the multidimensional array using nested loops. It has a time complexity of O(n), where n is the total number of elements.
- Get Dimension Sizes: This operation returns the size of a specific dimension (e.g., number of rows or columns in a 2D array). It has a time complexity of O(1).
Java Implementation
The following Java code demonstrates common operations on a 2D multidimensional array, including initialization, access, modification, traversal, and getting dimension sizes.
public class MultidimensionalArrayExamples {
// Initialize: Creates a 2D array with specified rows and columns
public int[][] initialize2DArray(int rows, int cols) {
if (rows <= 0 || cols <= 0) {
throw new IllegalArgumentException("Rows and columns must be positive.");
}
int[][] array = new int[rows][cols]; // Allocates 2D array
return array;
}
// Access Element: Retrieves an element at [row][col]
public int accessElement(int[][] array, int row, int col) {
if (array == null || row < 0 || row >= array.length || col < 0 || col >= array[0].length) {
throw new IllegalArgumentException("Invalid row or column index.");
}
return array[row][col]; // Returns element at specified position
}
// Modify Element: Updates an element at [row][col]
public void modifyElement(int[][] array, int row, int col, int value) {
if (array == null || row < 0 || row >= array.length || col < 0 || col >= array[0].length) {
throw new IllegalArgumentException("Invalid row or column index.");
}
array[row][col] = value; // Updates element at specified position
}
// Traverse Array: Visits all elements in the 2D array
public void traverseArray(int[][] array) {
if (array == null || array.length == 0) {
throw new IllegalArgumentException("Array is null or empty.");
}
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
System.out.print(array[i][j] + " "); // Prints each element
}
System.out.println(); // New line after each row
}
}
// Get Dimension Sizes: Returns the number of rows and columns
public int[] getDimensionSizes(int[][] array) {
if (array == null || array.length == 0) {
throw new IllegalArgumentException("Array is null or empty.");
}
return new int[]{array.length, array[0].length}; // Returns [rows, cols]
}
}
How It Works
- Initialization:
- The
initialize2DArraymethod allocates a 2D array with the specified number of rows and columns usingnew int[rows][cols]. All elements are initialized to 0 forintarrays. - For example,
initialize2DArray(3, 4)creates a 3x4 matrix. For a 3D array, you would usenew int[depth][rows][cols].
- The
- Access Element:
- The
accessElementmethod retrieves the value atarray[row][col]after validating the indices to prevent exceptions. - For example,
accessElement(array, 1, 2)returns the element at row 1, column 2 in a 2D array.
- The
- Modify Element:
- The
modifyElementmethod updatesarray[row][col]with the givenvalueafter validating indices. - For example,
modifyElement(array, 1, 2, 5)sets the element at row 1, column 2 to 5.
- The
- Traverse Array:
- The
traverseArraymethod uses nested loops to visit each element in the 2D array, printing them row by row. For a 3D array, an additional loop would iterate over the depth dimension. - For example, traversing a 2x3 array prints all elements in a grid format.
- The
- Get Dimension Sizes:
- The
getDimensionSizesmethod returns an array containing the number of rows (array.length) and columns (array[0].length) for a 2D array. For higher dimensions, additional lengths would be accessed (e.g.,array[0][0].lengthfor a 3D array). - For example,
getDimensionSizes(array)for a 3x4 array returns[3, 4].
- The
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Initialization | O(n) | O(n) |
| Access Element | O(1) | O(1) |
| Modify Element | O(1) | O(1) |
| Traverse Array | O(n) | O(1) |
| Get Dimension Sizes | O(1) | O(1) |
Note:
- n is the total number of elements in the array (e.g., rows * columns for 2D arrays, depth * rows * columns for 3D arrays).
- Space complexity for initialization accounts for the memory allocated for all elements.
Key Differences / Notes
- Multidimensional vs. Jagged Arrays:
- Multidimensional arrays have fixed dimensions, with all rows having the same number of columns in a 2D array, ensuring a rectangular structure.
Jagged arrays allow rows of different lengths, offering memory efficiency for irregular data but requiring additional index validation.
- Memory Allocation:
- Multidimensional arrays allocate contiguous memory for all elements, providing predictable O(1) access but potentially wasting memory for sparse data.
- In Java, a 2D array is an array of references to 1D arrays, and a 3D array adds another level of nesting, but the sub-arrays are typically uniform in size.
- Higher Dimensions:
- While 2D arrays are common for matrices, 3D or higher-dimensional arrays are used for complex data, such as 3D spatial models, but increase memory and complexity.
- Dynamic Resizing:
- Java arrays are fixed-size, so resizing a multidimensional array requires creating a new array and copying elements. Use
ArrayListfor dynamic sizing if needed.
- Java arrays are fixed-size, so resizing a multidimensional array requires creating a new array and copying elements. Use
✅ Tip: Use multidimensional arrays for structured data with fixed dimensions, such as matrices or 3D models, to leverage fast O(1) access and modification. Initialize all dimensions at creation to ensure proper memory allocation.
⚠ Warning: Always validate indices when accessing or modifying elements in a multidimensional array to prevent
ArrayIndexOutOfBoundsException. Be cautious with higher-dimensional arrays, as their memory usage grows exponentially with each dimension, potentially impacting performance.
Exercises
- Matrix Multiplication: Write a Java program that multiplies two 2D arrays (matrices) and returns the result as a new 2D array. Test with matrices of different compatible sizes.
- Transpose Matrix: Implement a method to transpose a 2D array (swap rows and columns). Test with square and non-square matrices.
- 3D Array Summation: Create a program that initializes a 3D array and computes the sum of all elements using nested loops. Test with different dimensions.
- Diagonal Elements: Write a method to extract the main diagonal elements of a square 2D array into a 1D array. Test with various square matrices.
- Wave Traversal: Implement a program that traverses a 2D array in a wave pattern (e.g., top-to-bottom for even columns, bottom-to-top for odd columns). Test with different matrix sizes.
Jagged Array
Definition and Concepts
A jagged array in Java is an array of arrays where each sub-array can have a different length, unlike a regular (rectangular) two-dimensional array where all rows have the same number of columns. Also known as a ragged array, it is a collection of one-dimensional arrays stored in a single array, where each element is itself an array of varying sizes. You can visualize a jagged array as a table where each row can have a different number of columns, providing flexibility in memory allocation. In Java, jagged arrays are implemented by creating an array of references to other arrays, allowing dynamic sizing for each row. This structure is useful when data sizes vary across rows, optimizing memory usage compared to fixed-size rectangular arrays.
Why Use It?
Jagged arrays are used to efficiently store and manipulate data when the number of elements in each row varies, reducing memory waste compared to rectangular arrays. They provide flexibility in handling irregular datasets, such as matrices with uneven dimensions or collections of variable-length lists. Jagged arrays are ideal for applications where memory efficiency is critical, and the data structure naturally aligns with varying row lengths. They also simplify operations like adding or removing elements in specific rows without affecting others.
Where to Use? (Real-Life Examples)
- Sparse Matrix Representation: Scientific computing applications use jagged arrays to store sparse matrices, where most elements are zero, to save memory by only storing non-zero elements per row.
- Graph Adjacency Lists: Graph algorithms use jagged arrays to represent adjacency lists, where each vertex has a list of neighbors with varying lengths.
- Text Processing: Natural language processing applications use jagged arrays to store sentences of different lengths from a document.
- Dynamic Data Storage: Database applications use jagged arrays to store query results with variable numbers of fields per record.
Explain Operations
- Initialization: This operation creates a jagged array by allocating the main array and then allocating each sub-array with a specific length. It has a time complexity of O(1) for the main array and O(n) for sub-arrays, where n is the total number of elements.
- Access Element: This operation retrieves an element at a specific row and column index. It has a time complexity of O(1).
- Modify Element: This operation updates an element at a specific row and column index. It has a time complexity of O(1).
- Add Row: This operation adds a new row by allocating a new sub-array and assigning it to the main array. It has a time complexity of O(n) for the sub-array allocation, where n is the row length.
- Get Row Length: This operation returns the length of a specific row. It has a time complexity of O(1).
Java Implementation
The following Java code demonstrates common operations on a jagged array, including initialization, access, modification, adding a row, and getting row length.
public class JaggedArrayExamples {
// Initialize: Creates a jagged array with specified row lengths
public int[][] initializeJaggedArray(int[] rowLengths) {
int[][] jaggedArray = new int[rowLengths.length][]; // Allocates main array
for (int i = 0; i < rowLengths.length; i++) {
jaggedArray[i] = new int[rowLengths[i]]; // Allocates each sub-array
}
return jaggedArray;
}
// Access Element: Retrieves an element at [row][col]
public int accessElement(int[][] jaggedArray, int row, int col) {
if (row < 0 || row >= jaggedArray.length || col < 0 || col >= jaggedArray[row].length) {
throw new IllegalArgumentException("Invalid row or column index.");
}
return jaggedArray[row][col]; // Returns element at specified position
}
// Modify Element: Updates an element at [row][col]
public void modifyElement(int[][] jaggedArray, int row, int col, int value) {
if (row < 0 || row >= jaggedArray.length || col < 0 || col >= jaggedArray[row].length) {
throw new IllegalArgumentException("Invalid row or column index.");
}
jaggedArray[row][col] = value; // Updates element at specified position
}
// Add Row: Adds a new row to the jagged array
public int[][] addRow(int[][] jaggedArray, int[] newRow) {
int[][] newJaggedArray = new int[jaggedArray.length + 1][]; // Creates new main array
for (int i = 0; i < jaggedArray.length; i++) {
newJaggedArray[i] = jaggedArray[i]; // Copies existing rows
}
newJaggedArray[jaggedArray.length] = newRow; // Adds new row
return newJaggedArray;
}
// Get Row Length: Returns the length of a specific row
public int getRowLength(int[][] jaggedArray, int row) {
if (row < 0 || row >= jaggedArray.length) {
throw new IllegalArgumentException("Invalid row index.");
}
return jaggedArray[row].length; // Returns length of specified row
}
}
How It Works
- Initialization:
- The
initializeJaggedArraymethod creates a main array of size equal torowLengths.lengthand allocates each sub-array with the length specified inrowLengths. - For example,
initializeJaggedArray(new int[]{2, 4, 1})creates a jagged array with three rows of lengths 2, 4, and 1.
- The
- Access Element:
- The
accessElementmethod usesjaggedArray[row][col]to retrieve the element at the specified position after validating indices. - For example,
accessElement(jaggedArray, 1, 2)returns the element at row 1, column 2.
- The
- Modify Element:
- The
modifyElementmethod updatesjaggedArray[row][col]with the givenvalueafter validating indices. - For example,
modifyElement(jaggedArray, 1, 2, 5)sets the element at row 1, column 2 to 5.
- The
- Add Row:
- The
addRowmethod creates a new main array with one additional slot, copies existing rows, and assigns thenewRowto the last slot. - For example,
addRow(jaggedArray, new int[]{7, 8})adds a new row[7, 8]to the jagged array.
- The
- Get Row Length:
- The
getRowLengthmethod returnsjaggedArray[row].lengthafter validating the row index. - For example,
getRowLength(jaggedArray, 1)returns the length of row 1.
- The
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Initialization | O(n) | O(n) |
| Access Element | O(1) | O(1) |
| Modify Element | O(1) | O(1) |
| Add Row | O(m) | O(m) |
| Get Row Length | O(1) | O(1) |
Note:
- n is the total number of elements across all sub-arrays for initialization.
- m is the number of rows in the jagged array for the add row operation.
- Space complexity accounts for the memory allocated for the arrays.
Key Differences / Notes
- Jagged Array vs. Rectangular Array:
- Jagged arrays allow rows of different lengths, saving memory for irregular data, while rectangular arrays (
int[][]) have fixed row and column sizes. - Jagged arrays are implemented as arrays of arrays, while rectangular arrays are contiguous 2D structures.
- Jagged arrays allow rows of different lengths, saving memory for irregular data, while rectangular arrays (
- Memory Efficiency:
- Jagged arrays are more memory-efficient for sparse or irregular data, as unused elements are not allocated, unlike rectangular arrays.
- Dynamic Resizing:
- Adding rows requires creating a new main array, as Java arrays are fixed-size. Use
ArrayList<int[]>for dynamic resizing if needed.
- Adding rows requires creating a new main array, as Java arrays are fixed-size. Use
- Use in Algorithms:
- Jagged arrays are commonly used in graph algorithms (e.g., adjacency lists) and dynamic programming with variable-sized rows.
✅ Tip: Use jagged arrays when dealing with irregular data, such as sparse matrices or adjacency lists, to optimize memory usage. Initialize row lengths based on the specific needs of each row to avoid unnecessary allocation.
⚠ Warning: Be cautious when accessing elements in a jagged array, as each row has a different length. Always validate column indices to avoid
ArrayIndexOutOfBoundsException. Avoid frequent row additions, as they require creating a new main array, which is costly.
Exercises
- Sparse Matrix Sum: Write a Java program that uses a jagged array to represent a sparse matrix and computes the sum of all non-zero elements. Test with matrices of varying row lengths.
- Adjacency List Representation: Implement a graph using a jagged array as an adjacency list. Add edges and print the neighbors of each vertex. Test with a sample graph.
- Row Sorting: Create a program that sorts each row of a jagged array independently. Test with a jagged array containing rows of different lengths.
- Dynamic Row Addition: Write a program that allows users to add rows to a jagged array interactively, specifying row lengths and elements. Test with multiple additions.
- Jagged Array Transpose: Implement a program that transposes a jagged array (converting rows to columns) into another jagged array. Test with irregular input arrays.
String Data Structure
Definition and Concepts
A string is a sequence of characters used to represent text in programming. In Java, the String class is a fundamental data structure that is immutable, meaning its content cannot be changed after creation. Strings are stored as arrays of characters internally, but Java’s String class provides a high-level interface for manipulating text. You can visualize a string as a chain of characters, such as "Hello", where each character occupies a specific position (index) starting from 0. Strings support operations like concatenation, substring extraction, and searching, making them essential for text processing. Java also provides mutable alternatives like StringBuilder and StringBuffer for scenarios requiring frequent modifications.
Why Use It?
Strings are used to handle and manipulate textual data in a wide range of applications, from user input processing to file parsing. Their immutability in Java ensures thread safety and consistency, making them ideal for constant or shared text data. Strings provide a rich set of methods for searching, modifying, and comparing text, simplifying complex text operations. For performance-critical applications, mutable classes like StringBuilder are used to reduce overhead from creating multiple string objects.
Where to Use? (Real-Life Examples)
- Text Processing: Text editors use strings to store and manipulate document content, such as formatting or searching text.
- Web Development: Web applications use strings to process URLs, HTML content, and user input, such as form data.
- Data Parsing: CSV or JSON parsers use strings to extract fields and values from structured data files.
- Search and Validation: Search engines and form validators use strings to match patterns (e.g., email validation) or search for keywords.
Explain Operations
- Concatenation: This operation combines two strings into a new string. It has a time complexity of O(n), where n is the total length of the resulting string.
- Substring Extraction: This operation extracts a portion of the string based on start and end indices. It has a time complexity of O(n) in Java due to string immutability.
- Search (Index Of): This operation finds the index of a substring or character within the string. It has a time complexity of O(n*m) in the worst case, where n is the string length and m is the substring length.
- Length: This operation returns the number of characters in the string. It has a time complexity of O(1).
- Character Access: This operation retrieves the character at a specific index. It has a time complexity of O(1).
Java Implementation
The following Java code demonstrates common string operations using the String class and introduces StringBuilder for mutable string manipulation.
public class StringExamples {
// Concatenation: Combines two strings
public String concatenate(String str1, String str2) {
return str1 + str2; // Uses + operator for concatenation
}
// Substring: Extracts a portion of the string
public String getSubstring(String str, int start, int end) {
if (start < 0 || end > str.length() || start > end) {
throw new IllegalArgumentException("Invalid start or end index.");
}
return str.substring(start, end); // Returns substring from start to end-1
}
// Search: Finds the index of a substring
public int findIndex(String str, String target) {
return str.indexOf(target); // Returns index of first occurrence or -1 if not found
}
// Length: Returns the number of characters
public int getLength(String str) {
return str.length(); // Returns the string length
}
// Character Access: Gets character at index
public char getCharAt(String str, int index) {
if (index < 0 || index >= str.length()) {
throw new IllegalArgumentException("Invalid index.");
}
return str.charAt(index); // Returns character at specified index
}
// StringBuilder Example: Demonstrates mutable string manipulation
public String buildString(String[] parts) {
StringBuilder builder = new StringBuilder();
for (String part : parts) {
builder.append(part); // Appends each part to the builder
}
return builder.toString(); // Converts StringBuilder to String
}
}
How It Works
- Concatenation:
- The
concatenatemethod uses the+operator, which internally creates a newStringobject combiningstr1andstr2. - For example,
concatenate("Hello", " World")creates a new string "Hello World".
- The
- Substring Extraction:
- The
getSubstringmethod callsstr.substring(start, end), which creates a new string containing characters from indexstarttoend-1. - For example,
getSubstring("Hello", 1, 4)returns "ell".
- The
- Search (Index Of):
- The
findIndexmethod usesstr.indexOf(target)to return the starting index oftargetinstror -1 if not found. - For example,
findIndex("Hello", "ll")returns 2.
- The
- Length:
- The
getLengthmethod callsstr.length()to return the number of characters. - For example,
getLength("Hello")returns 5.
- The
- Character Access:
- The
getCharAtmethod usesstr.charAt(index)to return the character at the specified index. - For example,
getCharAt("Hello", 1)returns 'e'.
- The
- StringBuilder Example:
- The
buildStringmethod usesStringBuilderto efficiently append multiple strings from an array, then converts the result to aString. - For example,
buildString(new String[]{"He", "llo"})returns "Hello".
- The
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Concatenation | O(n) | O(n) |
| Substring Extraction | O(n) | O(n) |
| Search (Index Of) | O(n*m) worst case | O(1) |
| Length | O(1) | O(1) |
| Character Access | O(1) | O(1) |
| StringBuilder Append | O(1) amortized | O(n) |
Note:
- n is the length of the string (or total length for concatenation).
- m is the length of the substring in
indexOf. StringBuilderappend is O(1) amortized due to dynamic resizing.
Key Differences / Notes
- String Immutability:
- Java’s
Stringclass is immutable, so operations like concatenation and substring create new strings, increasing space complexity. StringBuilderandStringBufferare mutable, reducing overhead for frequent modifications.
- Java’s
- String vs. StringBuilder/StringBuffer:
- Use
Stringfor constant or infrequently modified text due to its immutability and thread safety. - Use
StringBuilderfor single-threaded, mutable string operations, orStringBufferfor thread-safe mutable operations.
- Use
- Thread Safety:
StringandStringBuilderare not thread-safe for modifications (thoughStringis immutable).StringBufferis thread-safe but slower.
- String Pool:
- Java maintains a string pool, a special area in the heap (part of the constant pool in the PermGen or Metaspace, depending on the Java version), to store unique string literals and interned strings. When a string literal (e.g.,
"Hello") is created, Java checks the string pool. If the string exists, the reference is reused; otherwise, a new string is added to the pool. This optimizes memory usage for repeated literals. - The
intern()method can manually add a string to the pool, ensuring that identical strings share the same reference. For example,new String("Hello").intern()returns the pooled reference if"Hello"exists. - String pool usage reduces memory overhead but requires careful handling in performance-critical applications, as excessive interning can increase pool size and lookup time.
- Java maintains a string pool, a special area in the heap (part of the constant pool in the PermGen or Metaspace, depending on the Java version), to store unique string literals and interned strings. When a string literal (e.g.,
✅ Tip: Use
StringBuilderfor concatenating strings in a loop to avoid the overhead of creating multipleStringobjects. For simple, one-time concatenations, the+operator is sufficient and readable. UseString.intern()to leverage the string pool for memory optimization when dealing with frequently reused strings.
⚠ Warning: Avoid excessive string concatenation in loops using the
+operator, as it creates multiple intermediateStringobjects, leading to O(n²) time complexity. UseStringBuilderfor better performance. Be cautious withString.intern(), as overusing it can bloat the string pool and degrade performance.
Exercises
- Reverse a String: Write a Java program that reverses a string using both
Stringmethods andStringBuilder. Compare their performance for large strings. - Palindrome Checker: Implement a method to check if a string is a palindrome (ignoring case and non-alphanumeric characters). Test with various inputs.
- String Compression: Create a program that compresses a string by replacing repeated characters with their count (e.g., "aabbb" becomes "a2b3"). Use
StringBuilderfor efficiency. - Substring Frequency: Write a method to count the occurrences of a substring in a string using
indexOf. Test with overlapping and non-overlapping cases. - String Pool Experiment: Write a Java program that demonstrates the string pool by comparing string literals,
new String()objects, and interned strings using==andequals(). Analyze memory usage and equality behavior.
Mutable Strings
Definition and Concepts
Mutable strings in Java refer to string-like data structures that can be modified after creation, unlike the immutable String class. Java provides two primary classes for mutable strings: StringBuilder and StringBuffer. Both classes allow operations like appending, inserting, or deleting characters without creating new objects, making them efficient for text manipulation. StringBuilder is designed for single-threaded environments, offering better performance, while StringBuffer is thread-safe, suitable for multi-threaded applications. You can visualize a mutable string as a dynamic sequence of characters that can be altered in place, such as changing "Hello" to "Hello World" by appending characters directly. Mutable strings are essential for performance-critical applications requiring frequent text modifications.
Why Use It?
Mutable strings are used to efficiently manipulate text in scenarios where frequent modifications, such as appending or inserting characters, are needed. Unlike immutable String objects, which create new instances for each operation, StringBuilder and StringBuffer modify their internal character arrays, reducing memory overhead and improving performance. StringBuilder is preferred for single-threaded applications due to its speed, while StringBuffer is used when thread safety is required. These classes are ideal for building strings incrementally, such as in loops or dynamic text generation.
Where to Use? (Real-Life Examples)
- Log Message Construction: Logging frameworks use
StringBuilderto construct log messages by appending data, avoiding the overhead of multipleStringconcatenations. - Text File Generation: Report generators use
StringBuilderto build large text outputs, such as CSV or JSON files, by incrementally adding content. - String Manipulation in Editors: Text editors use mutable strings to handle user edits, such as inserting or deleting text in a document, in real-time.
- Thread-Safe Text Processing: Multi-threaded server applications use
StringBufferto build responses concurrently, ensuring thread safety during string modifications.
Explain Operations
- Append: This operation adds characters, strings, or other data types to the end of the mutable string. It has a time complexity of O(1) amortized.
- Insert: This operation inserts characters or strings at a specified index, shifting subsequent characters. It has a time complexity of O(n) due to shifting.
- Delete: This operation removes a range of characters from the mutable string, shifting remaining characters. It has a time complexity of O(n) due to shifting.
- Replace: This operation replaces a range of characters with a new string. It has a time complexity of O(n) due to shifting and copying.
- Length: This operation returns the number of characters in the mutable string. It has a time complexity of O(1).
Java Implementation
The following Java code demonstrates common operations using StringBuilder and StringBuffer.
public class MutableStringExamples {
// StringBuilder Append: Appends a string to a StringBuilder
public StringBuilder appendStringBuilder(StringBuilder builder, String str) {
return builder.append(str); // Appends str to the builder
}
// StringBuilder Insert: Inserts a string at a specified index
public StringBuilder insertStringBuilder(StringBuilder builder, int index, String str) {
if (index < 0 || index > builder.length()) {
throw new IllegalArgumentException("Invalid index.");
}
return builder.insert(index, str); // Inserts str at the specified index
}
// StringBuilder Delete: Deletes a range of characters
public StringBuilder deleteStringBuilder(StringBuilder builder, int start, int end) {
if (start < 0 || end > builder.length() || start > end) {
throw new IllegalArgumentException("Invalid start or end index.");
}
return builder.delete(start, end); // Deletes characters from start to end-1
}
// StringBuilder Replace: Replaces a range of characters
public StringBuilder replaceStringBuilder(StringBuilder builder, int start, int end, String str) {
if (start < 0 || end > builder.length() || start > end) {
throw new IllegalArgumentException("Invalid start or end index.");
}
return builder.replace(start, end, str); // Replaces characters from start to end-1 with str
}
// StringBuilder Length: Returns the number of characters
public int getLengthStringBuilder(StringBuilder builder) {
return builder.length(); // Returns the current length
}
// StringBuffer Example: Demonstrates thread-safe append
public StringBuffer appendStringBuffer(StringBuffer buffer, String str) {
return buffer.append(str); // Appends str to the thread-safe buffer
}
}
How It Works
- Append:
- The
appendStringBuildermethod callsbuilder.append(str)to addstrto the end of theStringBuilder’s internal character array. - For example,
appendStringBuilder(new StringBuilder("Hello"), " World")results in "Hello World". - The
appendStringBuffermethod works similarly forStringBufferwith thread-safe synchronization.
- The
- Insert:
- The
insertStringBuildermethod callsbuilder.insert(index, str)to insertstrat the specifiedindex, shifting subsequent characters. - For example,
insertStringBuilder(new StringBuilder("Hlo"), 1, "el")results in "Hello".
- The
- Delete:
- The
deleteStringBuildermethod callsbuilder.delete(start, end)to remove characters fromstarttoend-1, shifting remaining characters. - For example,
deleteStringBuilder(new StringBuilder("Hello"), 1, 4)results in "Ho".
- The
- Replace:
- The
replaceStringBuildermethod callsbuilder.replace(start, end, str)to replace characters fromstarttoend-1withstr. - For example,
replaceStringBuilder(new StringBuilder("Halo"), 1, 3, "el")results in "Hello".
- The
- Length:
- The
getLengthStringBuildermethod callsbuilder.length()to return the number of characters. - For example,
getLengthStringBuilder(new StringBuilder("Hello"))returns 5.
- The
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Append | O(1) amortized | O(n) |
| Insert | O(n) | O(n) |
| Delete | O(n) | O(n) |
| Replace | O(n) | O(n) |
| Length | O(1) | O(1) |
Note:
- n is the length of the mutable string or the resulting string after the operation.
- Append is O(1) amortized due to occasional resizing of the internal array.
- Space complexity accounts for the internal character array, which may resize dynamically.
Key Differences / Notes
- StringBuilder vs. StringBuffer:
StringBuilderis not thread-safe but faster, making it suitable for single-threaded applications.StringBufferis thread-safe due to synchronized methods, making it slower but appropriate for multi-threaded environments.
- Mutable vs. Immutable Strings:
- Unlike the immutable
Stringclass, which creates new objects for modifications,StringBuilderandStringBuffermodify their internal arrays, reducing memory overhead. - Operations like concatenation with
Stringhave O(n) space complexity per operation, whileStringBuilder/StringBufferappends are more efficient.
- Unlike the immutable
- Internal Array Resizing: Both classes use a dynamic array that doubles in size when capacity is exceeded, leading to O(1) amortized append time but occasional O(n) resizing.
- Java’s String Pool: Mutable strings (
StringBuilder/StringBuffer) are not stored in the string pool, unlikeStringliterals. Converting toStringviatoString()creates a newStringthat may be interned into the pool if needed.
✅ Tip: Use
StringBuilderfor most mutable string operations in single-threaded applications to maximize performance. ReserveStringBufferfor scenarios requiring thread safety, such as concurrent server applications.
⚠ Warning: Avoid using
StringBuilderin multi-threaded environments without synchronization, as it can lead to data corruption. UseStringBufferor explicit synchronization for thread-safe string manipulation.
Exercises
- String Reversal: Write a Java program that reverses a string using
StringBuilderandStringBuffer. Compare their performance for large inputs. - Dynamic Text Builder: Create a program that builds a formatted string (e.g., a CSV row) using
StringBuilder, appending elements from an array. Test with varying array sizes. - Thread-Safe Concatenation: Implement a multi-threaded program that uses
StringBufferto append strings concurrently from multiple threads. Verify thread safety with test cases. - Insert and Delete Simulation: Write a program that simulates text editing using
StringBuilder, performing a sequence of insert and delete operations. Test with different sequences. - StringBuilder Capacity Management: Create a program that demonstrates
StringBuilder’s capacity resizing by appending strings and monitoring capacity changes usingcapacity(). Analyze when resizing occurs.
Stack Data Structure
Definition and Concepts
A stack is a linear data structure that operates on the Last In, First Out (LIFO) principle. This means that the last element added to the stack is the first one removed. You can visualize a stack as a stack of plates, where you add a plate to the top and remove the topmost plate first. The primary operations of a stack include push, which adds an element to the top, and pop, which removes the top element. Additional operations include peek, which views the top element without removing it, and isEmpty, which checks if the stack is empty. Stacks are simple to implement and are highly efficient for problems requiring reversal or backtracking.
Why Use It?
A stack is useful when you need to process data in a Last In, First Out order. It provides an efficient way to manage data where the most recently added element is the first to be accessed or removed. Stacks are memory-efficient because they restrict operations to one end, known as the top, which simplifies data management and reduces operational complexity.
Where to Use? (Real-Life Examples)
- Undo Functionality in Software: Text editors and graphic design tools use stacks to store user actions, such as typing or drawing, allowing the most recent action to be undone.
- Browser History Navigation: Web browsers employ stacks to manage back and forward navigation, where the most recently visited page is removed when you click the "back" button.
- Function Call Management: Programming languages utilize stacks to handle function calls, storing return addresses and local variables for each function.
- Expression Evaluation: Stacks are used to evaluate mathematical expressions, such as
3 + 4 * 2, by managing operator precedence and parentheses.
Explain Operations
- Push: This operation adds an element to the top of the stack. It has a time complexity of O(1).
- Pop: This operation removes and returns the top element from the stack. If the stack is empty, it may throw an exception or return null. It has a time complexity of O(1).
- Peek: This operation returns the top element without removing it. It has a time complexity of O(1).
- isEmpty: This operation checks whether the stack is empty. It has a time complexity of O(1).
- Size: This operation returns the number of elements currently in the stack. It has a time complexity of O(1).
Java Implementation
The following Java code implements a stack using an array.
public class Stack {
private int maxSize; // Represents the maximum size of the stack
private int[] stackArray; // Array to store the stack elements
private int top; // Index of the top element in the stack
// Constructor to initialize the stack with a given size
public Stack(int size) {
maxSize = size;
stackArray = new int[size];
top = -1; // Indicates that the stack is initially empty
}
// Push: Adds an element to the top of the stack
public void push(int value) {
if (喧
if (top < maxSize - 1) { // Checks if the stack is not full
stackArray[++top] = value; // Increments top and adds the value
} else {
throw new IllegalStateException("The stack is full.");
}
}
// Pop: Removes and returns the top element
public int pop() {
if (isEmpty()) { // Checks if the stack is empty
throw new IllegalStateException("The stack is empty.");
}
return stackArray[top--]; // Returns the top element and decrements top
}
// Peek: Returns the top element without removing it
public int peek() {
if (isEmpty()) { // Checks if the stack is empty
throw new IllegalStateException("The stack is empty.");
}
return stackArray[top];
}
// isEmpty: Checks if the stack is empty
public boolean isEmpty() {
return (top == -1);
}
// Size: Returns the number of elements in the stack
public int size() {
return top + 1;
}
}
How It Works
- Initialization: The constructor initializes an array of size
maxSizeand setstopto -1, indicating an empty stack. - Push Operation:
- The method checks if the stack is not full by comparing
toptomaxSize - 1. - If there is space, it increments
topand stores the new value instackArray[top]. - If the stack is full, it throws an exception.
- The method checks if the stack is not full by comparing
- Pop Operation:
- The method verifies that the stack is not empty using
isEmpty(). - It returns the element at
stackArray[top]and decrementstop. - If the stack is empty, it throws an exception.
- The method verifies that the stack is not empty using
- Peek Operation: The method returns the value at
stackArray[top]without modifyingtop. - isEmpty Operation: The method returns true if
topequals -1, indicating an empty stack, and false otherwise. - Size Operation: The method returns
top + 1, which represents the current number of elements in the stack.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Push | O(1) | O(1) |
| Pop | O(1) | O(1) |
| Peek | O(1) | O(1) |
| isEmpty | O(1) | O(1) |
| Size | O(1) | O(1) |
Key Differences / Notes
- Array-Based vs. Linked List-Based Stack:
- The array-based stack, as implemented above, has a fixed size and offers O(1) access but is limited by its maximum size.
- A linked list-based stack supports dynamic sizing but may have slightly slower operations due to node creation and deletion.
- Thread Safety: The provided implementation is not thread-safe. For concurrent applications, consider using Java’s
Stackclass orArrayDequewith proper synchronization. - Java’s Built-in Options: Java’s
java.util.Stackclass is available but is less efficient thanArrayDeque, which is recommended for stack implementations due to its performance.
✅ Tip: Always check if the stack is empty before performing pop or peek operations to avoid exceptions. If you need a stack with dynamic sizing, consider using Java’s
ArrayDequeor implementing a linked list-based stack.
⚠ Warning: An array-based stack can overflow if you exceed its
maxSize. Carefully estimate the required size for your application to prevent errors.
Exercises
- Reverse a String: Write a Java program that uses the stack implementation above to reverse a given string by pushing each character onto the stack and then popping them to form the reversed string.
- Parentheses Checker: Create a program that uses a stack to check if a string of parentheses (e.g.,
"{[()]}") is balanced. Push opening brackets onto the stack and pop them when a matching closing bracket is found. - Stack Min Function: Extend the stack implementation to include a
min()function that returns the minimum element in the stack in O(1) time. Hint: Use an additional stack to track minimums. - Infix to Postfix Conversion: Write a program that converts an infix expression (e.g.,
A + B * C) to postfix (e.g.,A B C * +) using a stack. Test it with at least three different expressions. - Real-World Simulation: Simulate a browser’s back button functionality using the stack. Allow users to input a list of URLs to push onto the stack and print the current page each time the back button (pop) is pressed.
Queue Data Structure
Definition and Concepts
A queue is a linear data structure that operates on the First In, First Out (FIFO) principle. This means that the first element added to the queue is the first one to be removed. You can visualize a queue as a line of people waiting at a ticket counter, where the person who arrives first is served first. The primary operations of a queue include enqueue, which adds an element to the rear of the queue, and dequeue, which removes and returns the element at the front. Additional operations include peek, which views the front element without removing it, and isEmpty, which checks if the queue is empty. Queues are essential for managing data in a sequential, order-preserving manner.
Why Use It?
A queue is useful when you need to process data in the order it was received. It ensures fairness by maintaining the sequence of elements, making it ideal for tasks that require sequential processing or scheduling. Queues are efficient because they restrict operations to two ends: the front for removal and the rear for addition, simplifying data management.
Where to Use? (Real-Life Examples)
- Task Scheduling in Operating Systems: Operating systems use queues to manage processes waiting for CPU time, ensuring each process is executed in the order it was submitted.
- Print Job Management: Printers use queues to handle multiple print jobs, processing them in the order they were received.
- Customer Service Systems: Call centers or ticket counters use queues to serve customers in the order of their arrival.
- Breadth-First Search (BFS) in Graphs: Queues are used in algorithms like BFS to explore nodes level by level in a graph or tree.
Explain Operations
- Enqueue: This operation adds an element to the rear of the queue. It has a time complexity of O(1).
- Dequeue: This operation removes and returns the front element of the queue. If the queue is empty, it may throw an exception or return null. It has a time complexity of O(1).
- Peek: This operation returns the front element without removing it. It has a time complexity of O(1).
- isEmpty: This operation checks whether the queue is empty. It has a time complexity of O(1).
- Size: This operation returns the number of elements currently in the queue. It has a time complexity of O(1).
Java Implementation
The following Java code implements a queue using an array with a fixed size.
public class Queue {
private int maxSize; // Represents the maximum size of the queue
private int[] queueArray; // Array to store the queue elements
private int front; // Index of the front element
private int rear; // Index where the next element will be added
private int currentSize; // Number of elements in the queue
// Constructor to initialize the queue with a given size
public Queue(int size) {
maxSize = size;
queueArray = new int[size];
front = 0; // Front starts at index 0
rear = -1; // Rear starts before the first element
currentSize = 0; // Queue is initially empty
}
// Enqueue: Adds an element to the rear of the queue
public void enqueue(int value) {
if (currentSize < maxSize) { // Checks if the queue is not full
rear = (rear + 1) % maxSize; // Increments rear (circularly)
queueArray[rear] = value; // Adds the value at rear
currentSize++; // Increments the size
} else {
throw new IllegalStateException("The queue is full.");
}
}
// Dequeue: Removes and returns the front element
public int dequeue() {
if (isEmpty()) { // Checks if the queue is empty
throw new IllegalStateException("The queue is empty.");
}
int value = queueArray[front]; // Retrieves the front element
front = (front + 1) % maxSize; // Moves front to the next element (circularly)
currentSize--; // Decrements the size
return value;
}
// Peek: Returns the front element without removing it
public int peek() {
if (isEmpty()) { // Checks if the queue is empty
throw new IllegalStateException("The queue is empty.");
}
return queueArray[front];
}
// isEmpty: Checks if the queue is empty
public boolean isEmpty() {
return (currentSize == 0);
}
// Size: Returns the number of elements in the queue
public int size() {
return currentSize;
}
}
How It Works
- Initialization: The constructor initializes an array of size
maxSize, setsfrontto 0,rearto -1, andcurrentSizeto 0, indicating an empty queue. - Enqueue Operation:
- The method checks if the queue is not full by comparing
currentSizetomaxSize. - It increments
rearusing modulo (% maxSize) to support a circular queue, adds the value toqueueArray[rear], and incrementscurrentSize. - If the queue is full, it throws an exception.
- The method checks if the queue is not full by comparing
- Dequeue Operation:
- The method verifies that the queue is not empty using
isEmpty(). - It retrieves the value at
queueArray[front], movesfrontto the next position using modulo, decrementscurrentSize, and returns the value. - If the queue is empty, it throws an exception.
- The method verifies that the queue is not empty using
- Peek Operation: The method returns the value at
queueArray[front]without modifyingfrontorcurrentSize. - isEmpty Operation: The method returns true if
currentSizeequals 0, indicating an empty queue, and false otherwise. - Size Operation: The method returns
currentSize, which represents the number of elements in the queue.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Enqueue | O(1) | O(1) |
| Dequeue | O(1) | O(1) |
| Peek | O(1) | O(1) |
| isEmpty | O(1) | O(1) |
| Size | O(1) | O(1) |
Key Differences / Notes
- Array-Based vs. Linked List-Based Queue:
- The array-based queue, as implemented above, has a fixed size and offers O(1) access but is limited by its maximum size.
- A linked list-based queue supports dynamic sizing but may involve additional overhead for node creation and deletion.
- Circular Queue: The implementation above uses a circular queue to optimize space usage, allowing the array to wrap around when
rearorfrontreaches the end. - Thread Safety: The provided implementation is not thread-safe. For concurrent applications, consider using Java’s
ConcurrentLinkedQueueorArrayBlockingQueue. - Java’s Built-in Options: Java provides
Queueas an interface injava.util, with implementations likeLinkedListandArrayDeque. TheArrayDequeis recommended for better performance in most cases.
✅ Tip: Use a circular queue, as shown in the implementation, to make efficient use of array space and avoid wasting memory when elements are dequeued.
⚠ Warning: An array-based queue can overflow if you exceed its
maxSize. Ensure the queue size is sufficient for your application, or consider a linked list-based queue for dynamic sizing.
Exercises
- Print Job Simulator: Write a Java program that simulates a printer queue using the queue implementation above. Allow users to enqueue print jobs (represented as strings) and dequeue them in order, printing each job as it is processed.
- Queue Reversal: Create a program that reverses the elements of a queue using a stack. Enqueue a set of numbers, reverse them, and print the reversed queue.
- Circular Queue Test: Extend the queue implementation to handle edge cases, such as enqueueing and dequeuing in a loop, and test it with a sequence of operations to verify circular behavior.
- Queue-Based BFS: Implement a simple Breadth-First Search (BFS) algorithm for a graph using the queue. Represent the graph as an adjacency list and print the nodes visited in BFS order.
- Ticket Counter Simulation: Simulate a ticket counter system where customers (represented by names) join a queue and are served in order. Allow users to enqueue customers, dequeue them, and display the current queue size.
Priority Queue Data Structure
Definition and Concepts
A priority queue is an abstract data structure that operates similarly to a regular queue but assigns a priority to each element. Elements with higher priority are dequeued before those with lower priority, regardless of their order of insertion. You can visualize a priority queue as a hospital emergency room where patients with more severe conditions are treated first, even if they arrived later. The primary operations include enqueue, which adds an element with a specified priority, and dequeue, which removes and returns the element with the highest priority. Additional operations include peek, which views the highest-priority element without removing it, and isEmpty, which checks if the priority queue is empty. Priority queues are typically implemented using a heap (often a binary heap) to ensure efficient priority-based operations.
Why Use It?
A priority queue is useful when you need to process elements based on their priority rather than their arrival order. It ensures that the most critical or highest-priority elements are handled first, making it ideal for scheduling, resource allocation, and optimization problems. Priority queues are efficient because they use a heap structure, which provides logarithmic time complexity for most operations.
Where to Use? (Real-Life Examples)
- Task Scheduling in Operating Systems: Operating systems use priority queues to schedule processes based on their priority levels, ensuring critical tasks are executed first.
- Hospital Emergency Systems: Emergency rooms prioritize patients based on the severity of their condition, using a priority queue to manage treatment order.
- Dijkstra’s Algorithm: Priority queues are used in graph algorithms like Dijkstra’s to select the next node with the smallest distance.
- Huffman Coding: Priority queues are used to build optimal prefix codes for data compression by repeatedly selecting the two nodes with the smallest frequencies.
Explain Operations
- Enqueue: This operation adds an element with a specified priority to the priority queue. The element is inserted into the heap, and the heap property is restored by bubbling up. It has a time complexity of O(log n).
- Dequeue: This operation removes and returns the element with the highest priority (the root in a min-heap). The last element is moved to the root, and the heap property is restored by bubbling down. It has a time complexity of O(log n).
- Peek: This operation returns the highest-priority element (the root) without removing it. It has a time complexity of O(1).
- isEmpty: This operation checks whether the priority queue is empty. It has a time complexity of O(1).
- Size: This operation returns the number of elements in the priority queue. It has a time complexity of O(1).
Java Implementation
The following Java code implements a priority queue using a binary min-heap, where smaller values have higher priority.
public class PriorityQueue {
private int[] heap; // Array to store the heap elements
private int maxSize; // Maximum size of the priority queue
private int size; // Current number of elements in the priority queue
// Constructor to initialize the priority queue with a given size
public PriorityQueue(int capacity) {
maxSize = capacity;
heap = new int[maxSize];
size = 0; // Priority queue is initially empty
}
// Enqueue: Adds an element to the priority queue
public void enqueue(int value) {
if (size >= maxSize) { // Checks if the priority queue is full
throw new IllegalStateException("The priority queue is full.");
}
heap[size] = value; // Adds the new element at the end
bubbleUp(size); // Restores heap property by bubbling up
size++; // Increments the size
}
// Dequeue: Removes and returns the highest-priority element
public int dequeue() {
if (isEmpty()) { // Checks if the priority queue is empty
throw new IllegalStateException("The priority queue is empty.");
}
int result = heap[0]; // Stores the root (highest-priority element)
heap[0] = heap[size - 1]; // Moves the last element to the root
size--; // Decrements the size
if (size > 0) { // Avoids bubbling down if the heap is empty
bubbleDown(0); // Restores heap property by bubbling down
}
return result;
}
// Peek: Returns the highest-priority element without removing it
public int peek() {
if (isEmpty()) { // Checks if the priority queue is empty
throw new IllegalStateException("The priority queue is empty.");
}
return heap[0]; // Returns the root element
}
// isEmpty: Checks if the priority queue is empty
public boolean isEmpty() {
return size == 0;
}
// Size: Returns the number of elements in the priority queue
public int size() {
return size;
}
// Helper method to bubble up an element to restore min-heap property
private void bubbleUp(int index) {
int parent = (index - 1) / 2; // Calculates parent index
while (index > 0 && heap[index] < heap[parent]) { // Compares with parent
swap(index, parent); // Swaps if child is smaller
index = parent; // Updates index to parent
parent = (index - 1) / 2; // Recalculates parent
}
}
// Helper method to bubble down an element to restore min-heap property
private void bubbleDown(int index) {
int minIndex = index; // Tracks the smallest element
while (true) {
int leftChild = 2 * index + 1; // Left child index
int rightChild = 2 * index + 2; // Right child index
if (leftChild < size && heap[leftChild] < heap[minIndex]) {
minIndex = leftChild; // Updates if left child is smaller
}
if (rightChild < size && heap[rightChild] < heap[minIndex]) {
minIndex = rightChild; // Updates if right child is smaller
}
if (minIndex == index) break; // Stops if no smaller child found
swap(index, minIndex); // Swaps with the smaller child
index = minIndex; // Updates index to continue bubbling down
}
}
// Helper method to swap two elements in the heap
private void swap(int i, int j) {
int temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
}
How It Works
- Initialization: The constructor initializes an array of size
maxSizeand setssizeto 0, indicating an empty priority queue. - Enqueue Operation:
- The method checks if the priority queue is full by comparing
sizetomaxSize. - It adds the new element at the end of the heap (
heap[size]), callsbubbleUpto restore the min-heap property by moving the element up if it’s smaller than its parent, and incrementssize. - If the queue is full, it throws an exception.
- The method checks if the priority queue is full by comparing
- Dequeue Operation:
- The method verifies that the priority queue is not empty using
isEmpty(). - It saves the root element (
heap[0]), moves the last element (heap[size-1]) to the root, decrementssize, and callsbubbleDownto restore the min-heap property by moving the root down if it’s larger than its children. - If the queue is empty, it throws an exception.
- The method verifies that the priority queue is not empty using
- Peek Operation: The method returns the root element (
heap[0]) without modifying the heap. - isEmpty Operation: The method returns true if
sizeequals 0, indicating an empty priority queue, and false otherwise. - Size Operation: The method returns
size, which represents the number of elements in the priority queue. - BubbleUp Helper: This method moves a newly added element up the heap by comparing it with its parent and swapping if it’s smaller, continuing until the heap property is restored.
- BubbleDown Helper: This method moves the root element down by comparing it with its children, swapping with the smaller child if necessary, and continuing until the heap property is restored.
- Swap Helper: This method exchanges two elements in the heap array.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Enqueue | O(log n) | O(1) |
| Dequeue | O(log n) | O(1) |
| Peek | O(1) | O(1) |
| isEmpty | O(1) | O(1) |
| Size | O(1) | O(1) |
Key Differences / Notes
- Min-Heap vs. Max-Heap:
- The implementation above uses a min-heap, where smaller values have higher priority. A max-heap can be implemented by reversing the comparison logic, where larger values have higher priority.
- Array-Based vs. Other Implementations:
- The array-based heap implementation is memory-efficient and provides O(log n) for enqueue and dequeue operations.
- Other implementations, like a binary search tree or linked list, are less common due to higher complexity or overhead.
- Thread Safety: The provided implementation is not thread-safe. For concurrent applications, use Java’s
PriorityBlockingQueueor synchronize access. - Java’s Built-in Priority Queue: Java provides
java.util.PriorityQueue, which is a min-heap by default but can be customized for max-heap behavior using a comparator.
✅ Tip: Use a min-heap for scenarios where the smallest element has the highest priority (e.g., shortest path algorithms) and a max-heap for scenarios where the largest element has priority (e.g., task prioritization).
⚠ Warning: Ensure the priority queue’s capacity is sufficient for your use case to avoid overflow errors in the array-based implementation. For dynamic sizing, consider Java’s
PriorityQueueclass.
Exercises
- Task Scheduler: Write a Java program that uses the priority queue implementation above to simulate a task scheduler. Enqueue tasks with priorities (represented as integers) and dequeue them in order of highest priority, printing each task as it’s processed.
- Kth Largest Element: Create a program that uses a priority queue to find the kth largest element in an array of integers. Test it with at least three different arrays and k values.
- Min-Heap to Max-Heap: Modify the priority queue implementation to support a max-heap (where larger values have higher priority). Test the modified version with enqueue and dequeue operations.
- Dijkstra’s Algorithm: Implement Dijkstra’s algorithm for a weighted graph using the priority queue. Represent the graph as an adjacency list and compute the shortest paths from a source node.
- Merge K Sorted Lists: Write a program that merges k sorted arrays into a single sorted array using a priority queue. Enqueue the first element of each array with its array index and value, and repeatedly dequeue and enqueue the next element from the same array.
Linked List Data Structure
Definition and Concepts
A linked list is a linear data structure consisting of a sequence of nodes, where each node contains data and a reference (or link) to the next node. Unlike arrays, linked lists do not store elements in contiguous memory locations, allowing dynamic memory allocation. In a singly linked list, each node points to the next node, with the last node pointing to null. You can visualize a linked list as a chain of boxes, where each box holds a value and an arrow pointing to the next box. Linked lists are flexible for insertions and deletions but require traversal to access elements, making them suitable for dynamic data.
Why Use It?
Linked lists are used when you need a dynamic data structure that can grow or shrink efficiently without requiring contiguous memory. They allow fast insertions and deletions at known positions (e.g., head or tail) compared to arrays. Linked lists are ideal for applications where the size of the list is unknown or changes frequently, and they serve as a foundation for other data structures like stacks and queues.
Where to Use? (Real-Life Examples)
- Dynamic Data Storage: Linked lists are used in applications like music playlists, where songs can be added or removed dynamically.
- Implementation of Stacks and Queues: Stacks and queues are often implemented using linked lists due to their flexibility in size.
- Browser History: Web browsers use linked lists to maintain a history of visited pages, allowing easy navigation forward and backward.
- Memory Management: Operating systems use linked lists to manage free memory blocks in dynamic memory allocation.
Explain Operations
- Insert at Head: This operation adds a new node at the beginning of the list. It has a time complexity of O(1).
- Insert at Tail: This operation adds a new node at the end of the list. It has a time complexity of O(n) due to traversal.
- Delete at Head: This operation removes the first node. It has a time complexity of O(1).
- Delete by Value: This operation removes the first node with a given value. It has a time complexity of O(n) due to traversal.
- Search: This operation finds a node with a given value. It has a time complexity of O(n).
- isEmpty: This operation checks whether the linked list is empty. It has a time complexity of O(1).
- Size: This operation returns the number of nodes in the linked list. It has a time complexity of O(1) if maintained.
Java Implementation
The following Java code implements a singly linked list with basic operations.
public class LinkedList {
private class Node {
int value; // Value stored in the node
Node next; // Reference to the next node
Node(int value) {
this.value = value;
this.next = null;
}
}
private Node head; // Head of the linked list
private int size; // Number of nodes in the linked list
// Constructor to initialize an empty linked list
public LinkedList() {
head = null;
size = 0;
}
// Insert at Head: Adds a new node at the beginning
public void insertAtHead(int value) {
Node newNode = new Node(value); // Creates a new node
newNode.next = head; // Points new node to current head
head = newNode; // Updates head to new node
size++; // Increments size
}
// Insert at Tail: Adds a new node at the end
public void insertAtTail(int value) {
Node newNode = new Node(value); // Creates a new node
if (isEmpty()) { // If list is empty, sets head to new node
head = newNode;
} else {
Node current = head;
while (current.next != null) { // Traverses to the last node
current = current.next;
}
current.next = newNode; // Adds new node at the end
}
size++; // Increments size
}
// Delete at Head: Removes the first node
public void deleteAtHead() {
if (isEmpty()) { // Checks if the list is empty
throw new IllegalStateException("The linked list is empty.");
}
head = head.next; // Moves head to the next node
size--; // Decrements size
}
// Delete by Value: Removes the first node with the given value
public void deleteByValue(int value) {
if (isEmpty()) { // Checks if the list is empty
throw new IllegalStateException("The linked list is empty.");
}
if (head.value == value) { // If head has the value, removes head
head = head.next;
size--;
return;
}
Node current = head;
Node prev = null;
while (current != null && current.value != value) { // Traverses to find the value
prev = current;
current = current.next;
}
if (current == null) { // If value not found
throw new IllegalArgumentException("Value not found in the linked list.");
}
prev.next = current.next; // Bypasses the node to delete
size--; // Decrements size
}
// Search: Checks if a value exists in the linked list
public boolean search(int value) {
Node current = head;
while (current != null) { // Traverses the list
if (current.value == value) {
return true; // Returns true if value is found
}
current = current.next;
}
return false; // Returns false if value is not found
}
// isEmpty: Checks if the linked list is empty
public boolean isEmpty() {
return size == 0;
}
// Size: Returns the number of nodes in the linked list
public int size() {
return size;
}
}
How It Works
- Initialization: The constructor initializes
headas null andsizeas 0, indicating an empty linked list. - Insert at Head:
- The method creates a new node with the given value, sets its
nextto the currenthead, updatesheadto the new node, and incrementssize.
- The method creates a new node with the given value, sets its
- Insert at Tail:
- The method creates a new node. If the list is empty, it sets
headto the new node. Otherwise, it traverses to the last node and sets itsnextto the new node, then incrementssize.
- The method creates a new node. If the list is empty, it sets
- Delete at Head:
- The method checks if the list is empty. If not, it moves
headto the next node and decrementssize. If empty, it throws an exception.
- The method checks if the list is empty. If not, it moves
- Delete by Value:
- The method checks if the list is empty or if the head node has the value. If the head is the target, it updates
headand decrementssize. - Otherwise, it traverses the list to find the value, keeping track of the previous node, and bypasses the target node by updating the previous node’s
next. It decrementssizeor throws an exception if the value is not found.
- The method checks if the list is empty or if the head node has the value. If the head is the target, it updates
- Search Operation: The method traverses the list, returning true if the value is found and false if the end (null) is reached.
- isEmpty Operation: The method returns true if
sizeequals 0, indicating an empty list, and false otherwise. - Size Operation: The method returns
size, which tracks the number of nodes.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Insert at Head | O(1) | O(1) |
| Insert at Tail | O(n) | O(1) |
| Delete at Head | O(1) | O(1) |
| Delete by Value | O(n) | O(1) |
| Search | O(n) | O(1) |
| isEmpty | O(1) | O(1) |
| Size | O(1) | O(1) |
Note: n is the number of nodes in the linked list.
Key Differences / Notes
- Singly vs. Doubly Linked List:
- The implementation above is a singly linked list, where each node points to the next. It is memory-efficient but slow for tail operations (O(n)).
- A doubly linked list has nodes with pointers to both the next and previous nodes, enabling O(1) tail deletions but using more memory.
- Linked List vs. Array:
- Linked lists support dynamic sizing and efficient insertions/deletions at the head, but random access and tail operations are O(n).
- Arrays offer O(1) random access but require contiguous memory and have O(n) insertions/deletions in the middle.
- Thread Safety: The implementation is not thread-safe. For concurrent applications, use Java’s
ConcurrentLinkedDequeor synchronize access. - Java’s Built-in Linked List: Java provides
LinkedListinjava.util, which is a doubly linked list supporting both List and Deque interfaces.
✅ Tip: Use a linked list when frequent insertions or deletions at the head are needed, or when the size of the list is unknown. For tail operations, consider a doubly linked list or maintain a tail pointer.
⚠ Warning: Avoid using a singly linked list for applications requiring frequent access to the tail or random positions, as traversal is O(n). Use arrays or other structures for such cases.
Exercises
- Reverse a Linked List: Write a Java program that reverses the linked list using the implementation above. Test it with lists of varying sizes.
- Cycle Detection: Extend the linked list implementation to detect if it contains a cycle (e.g., using Floyd’s Tortoise and Hare algorithm). Test with cyclic and acyclic lists.
- Merge Two Sorted Lists: Create a program that merges two sorted linked lists into a single sorted linked list. Use the implementation above and test with different inputs.
- Middle Element Finder: Implement a method to find the middle element of the linked list in a single pass. Use the fast-and-slow pointer technique and test it.
- Playlist Manager: Simulate a music playlist using the linked list. Allow users to add songs (insert at head or tail), remove songs (delete by value), and print the playlist.
Doubly Linked List Data Structure
Definition and Concepts
A doubly linked list is a linear data structure consisting of a sequence of nodes, where each node contains data, a reference to the next node, and a reference to the previous node. Unlike a singly linked list, which only allows traversal in one direction, a doubly linked list enables bidirectional traversal, making operations like deletion and insertion at both ends more efficient. The first node (head) has a null previous reference, and the last node (tail) has a null next reference. You can visualize a doubly linked list as a chain of boxes, where each box holds a value and arrows point to both the next and previous boxes. This structure is ideal for applications requiring frequent insertions, deletions, or reverse traversal.
Why Use It?
A doubly linked list is used when you need a dynamic data structure that supports efficient insertions and deletions at both the head and tail, as well as bidirectional traversal. It provides more flexibility than a singly linked list by allowing navigation in both directions, which is useful for applications like undo/redo functionality or browsing history. Although it uses more memory due to the additional previous pointers, it offers O(1) time complexity for operations at the head and tail, making it suitable for dynamic datasets.
Where to Use? (Real-Life Examples)
- Undo/Redo Functionality: Text editors use doubly linked lists to track actions, allowing users to move forward (redo) or backward (undo) through the action history.
- Browser Navigation: Web browsers use doubly linked lists to manage browsing history, enabling users to navigate back and forward between visited pages.
- Music or Video Playlists: Media players use doubly linked lists to manage playlists, allowing users to move to the next or previous song efficiently.
- Deque Implementation: Doubly linked lists are used to implement double-ended queues (deques), supporting insertions and deletions at both ends.
Explain Operations
- Insert at Head: This operation adds a new node at the beginning of the list. It has a time complexity of O(1).
- Insert at Tail: This operation adds a new node at the end of the list. It has a time complexity of O(1) if a tail pointer is maintained.
- Delete at Head: This operation removes the first node. It has a time complexity of O(1).
- Delete at Tail: This operation removes the last node. It has a time complexity of O(1) if a tail pointer is maintained.
- Search: This operation finds a node with a given value. It has a time complexity of O(n).
- isEmpty: This operation checks whether the doubly linked list is empty. It has a time complexity of O(1).
- Size: This operation returns the number of nodes in the doubly linked list. It has a time complexity of O(1) if maintained.
Java Implementation
The following Java code implements a doubly linked list with basic operations, maintaining both head and tail pointers for efficiency.
public class DoublyLinkedList {
private class Node {
int value; // Value stored in the node
Node next; // Reference to the next node
Node prev; // Reference to the previous node
Node(int value) {
this.value = value;
this.next = null;
this.prev = null;
}
}
private Node head; // Head of the doubly linked list
private Node tail; // Tail of the doubly linked list
private int size; // Number of nodes in the doubly linked list
// Constructor to initialize an empty doubly linked list
public DoublyLinkedList() {
head = null;
tail = null;
size = 0;
}
// Insert at Head: Adds a new node at the beginning
public void insertAtHead(int value) {
Node newNode = new Node(value); // Creates a new node
if (isEmpty()) { // If list is empty, sets both head and tail
head = newNode;
tail = newNode;
} else {
newNode.next = head; // Points new node to current head
head.prev = newNode; // Points current head back to new node
head = newNode; // Updates head to new node
}
size++; // Increments size
}
// Insert at Tail: Adds a new node at the end
public void insertAtTail(int value) {
Node newNode = new Node(value); // Creates a new node
if (isEmpty()) { // If list is empty, sets both head and tail
head = newNode;
tail = newNode;
} else {
tail.next = newNode; // Points current tail to new node
newNode.prev = tail; // Points new node back to current tail
tail = newNode; // Updates tail to new node
}
size++; // Increments size
}
// Delete at Head: Removes the first node
public void deleteAtHead() {
if (isEmpty()) { // Checks if the list is empty
throw new IllegalStateException("The doubly linked list is empty.");
}
head = head.next; // Moves head to the next node
if (head == null) { // If list becomes empty, updates tail
tail = null;
} else {
head.prev = null; // Sets new head's previous to null
}
size--; // Decrements size
}
// Delete at Tail: Removes the last node
public void deleteAtTail() {
if (isEmpty()) { // Checks if the list is empty
throw new IllegalStateException("The doubly linked list is empty.");
}
tail = tail.prev; // Moves tail to the previous node
if (tail == null) { // If list becomes empty, updates head
head = null;
} else {
tail.next = null; // Sets new tail's next to null
}
size--; // Decrements size
}
// Search: Checks if a value exists in the doubly linked list
public boolean search(int value) {
Node current = head;
while (current != null) { // Traverses the list
if (current.value == value) {
return true; // Returns true if value is found
}
current = current.next;
}
return false; // Returns false if value is not found
}
// isEmpty: Checks if the doubly linked list is empty
public boolean isEmpty() {
return size == 0;
}
// Size: Returns the number of nodes in the doubly linked list
public int size() {
return size;
}
}
How It Works
- Initialization: The constructor initializes
headandtailas null andsizeas 0, indicating an empty doubly linked list. - Insert at Head:
- The method creates a new node with the given value.
- If the list is empty, it sets both
headandtailto the new node. - Otherwise, it links the new node to the current
head, updates thehead’s previous pointer, setsheadto the new node, and incrementssize.
- Insert at Tail:
- The method creates a new node.
- If the list is empty, it sets both
headandtailto the new node. - Otherwise, it links the current
tailto the new node, sets the new node’s previous pointer to thetail, updatestailto the new node, and incrementssize.
- Delete at Head:
- The method checks if the list is empty. If not, it moves
headto the next node, sets the newhead’s previous pointer to null (if not null), updatestailif the list becomes empty, and decrementssize. If empty, it throws an exception.
- The method checks if the list is empty. If not, it moves
- Delete at Tail:
- The method checks if the list is empty. If not, it moves
tailto the previous node, sets the newtail’s next pointer to null (if not null), updatesheadif the list becomes empty, and decrementssize. If empty, it throws an exception.
- The method checks if the list is empty. If not, it moves
- Search Operation: The method traverses the list from
headtotail, returning true if the value is found and false if the end (null) is reached. - isEmpty Operation: The method returns true if
sizeequals 0, indicating an empty list, and false otherwise. - Size Operation: The method returns
size, which tracks the number of nodes.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Insert at Head | O(1) | O(1) |
| Insert at Tail | O(1) | O(1) |
| Delete at Head | O(1) | O(1) |
| Delete at Tail | O(1) | O(1) |
| Search | O(n) | O(1) |
| isEmpty | O(1) | O(1) |
| Size | O(1) | O(1) |
Note: n is the number of nodes in the doubly linked list.
Key Differences / Notes
- Doubly vs. Singly Linked List:
- The implementation above is a doubly linked list, which allows O(1) insertions and deletions at both head and tail due to the
tailpointer and bidirectional links. - A singly linked list only has next pointers, making tail operations O(n) unless a tail pointer is maintained, but it uses less memory.
- The implementation above is a doubly linked list, which allows O(1) insertions and deletions at both head and tail due to the
- Doubly Linked List vs. Array:
- Doubly linked lists support dynamic sizing and O(1) operations at both ends but have O(n) access and search times.
- Arrays offer O(1) random access but require contiguous memory and have O(n) insertions/deletions in the middle.
- Thread Safety: The implementation is not thread-safe. For concurrent applications, use Java’s
ConcurrentLinkedDequeor synchronize access. - Java’s Built-in Doubly Linked List: Java provides
LinkedListinjava.util, which is a doubly linked list implementing both List and Deque interfaces.
✅ Tip: Use a doubly linked list when you need efficient insertions and deletions at both ends or bidirectional traversal, such as in navigation systems or deques.
⚠ Warning: Doubly linked lists use more memory than singly linked lists due to the additional previous pointers. Ensure memory constraints are considered for large datasets.
Exercises
- Reverse a Doubly Linked List: Write a Java program that reverses the doubly linked list using the implementation above. Test it with lists of varying sizes.
- Bidirectional Traversal: Extend the implementation to include methods for printing the list in both forward (head to tail) and backward (tail to head) directions. Test with a sample list.
- Insert After Value: Add a method to insert a new node after the first occurrence of a given value. Test it with cases where the value exists and does not exist.
- Deque Implementation: Use the doubly linked list to implement a double-ended queue (deque) with methods for adding and removing elements at both ends. Test with a sequence of operations.
- Browser History Simulator: Create a program that simulates a browser’s navigation history using the doubly linked list. Allow users to add pages (insert at tail), navigate back (delete at tail), and navigate forward (re-insert).
Circular Linked List Data Structure
Definition and Concepts
A circular linked list is a linear data structure where nodes are arranged in a sequence, and the last node points back to the first node, forming a loop. In a singly circular linked list, each node contains data and a reference to the next node, with the last node’s next pointer linking to the head node instead of null. Unlike a standard singly linked list, which has a definite end, a circular linked list allows continuous traversal. You can visualize a circular linked list as a ring of boxes, where each box holds a value and an arrow points to the next box, with the last box pointing back to the first. This structure is ideal for applications requiring cyclic or round-robin processing.
Why Use It?
A circular linked list is used when you need a data structure that supports continuous looping through elements, such as in round-robin scheduling or cyclic data processing. It eliminates the need to reset traversal to the beginning after reaching the end, making it efficient for repetitive tasks. The circular nature simplifies certain operations, like rotating the list, and it can be used as a foundation for structures like circular buffers.
Where to Use? (Real-Life Examples)
- Round-Robin Scheduling: Operating systems use circular linked lists to manage processes in a round-robin fashion, cycling through tasks to allocate CPU time.
- Music Playlist Looping: Music players use circular linked lists to implement a looping playlist, where playback restarts from the first song after the last one.
- Circular Buffers: Network applications use circular linked lists to implement buffers for streaming data, reusing space in a fixed-size structure.
- Multiplayer Game Turns: Board games or turn-based applications use circular linked lists to cycle through players’ turns indefinitely.
Explain Operations
- Insert at Head: This operation adds a new node at the beginning of the list, updating the last node’s pointer to the new head. It has a time complexity of O(1) if a tail pointer is maintained.
- Insert at Tail: This operation adds a new node at the end of the list, updating the last node’s pointer to the new node and linking it to the head. It has a time complexity of O(1) if a tail pointer is maintained.
- Delete at Head: This operation removes the first node, updating the last node’s pointer to the new head. It has a time complexity of O(1) if a tail pointer is maintained.
- Delete by Value: This operation removes the first node with a given value. It has a time complexity of O(n) due to traversal.
- Search: This operation finds a node with a given value. It has a time complexity of O(n).
- isEmpty: This operation checks whether the circular linked list is empty. It has a time complexity of O(1).
- Size: This operation returns the number of nodes in the circular linked list. It has a time complexity of O(1) if maintained.
Java Implementation
The following Java code implements a singly circular linked list with basic operations, maintaining a tail pointer for efficiency.
public class CircularLinkedList {
private class Node {
int value; // Value stored in the node
Node next; // Reference to the next node
Node(int value) {
this.value = value;
this.next = null;
}
}
private Node tail; // Tail of the circular linked list
private int size; // Number of nodes in the circular linked list
// Constructor to initialize an empty circular linked list
public CircularLinkedList() {
tail = null;
size = 0;
}
// Insert at Head: Adds a new node at the beginning
public void insertAtHead(int value) {
Node newNode = new Node(value); // Creates a new node
if (isEmpty()) { // If list is empty, sets tail to new node and links to itself
tail = newNode;
newNode.next = newNode;
} else {
newNode.next = tail.next; // Points new node to current head
tail.next = newNode; // Points tail to new node (new head)
}
size++; // Increments size
}
// Insert at Tail: Adds a new node at the end
public void insertAtTail(int value) {
Node newNode = new Node(value); // Creates a new node
if (isEmpty()) { // If list is empty, sets tail to new node and links to itself
tail = newNode;
newNode.next = newNode;
} else {
newNode.next = tail.next; // Points new node to current head
tail.next = newNode; // Points current tail to new node
tail = newNode; // Updates tail to new node
}
size++; // Increments size
}
// Delete at Head: Removes the first node
public void deleteAtHead() {
if (isEmpty()) { // Checks if the list is empty
throw new IllegalStateException("The circular linked list is empty.");
}
if (size == 1) { // If only one node, clears the list
tail = null;
} else {
tail.next = tail.next.next; // Points tail to the second node (new head)
}
size--; // Decrements size
}
// Delete by Value: Removes the first node with the given value
public void deleteByValue(int value) {
if (isEmpty()) { // Checks if the list is empty
throw new IllegalStateException("The circular linked list is empty.");
}
Node current = tail.next; // Starts at head
Node prev = tail; // Previous node for deletion
do {
if (current.value == value) { // If value is found
if (size == 1) { // If only one node, clears the list
tail = null;
} else if (current == tail.next) { // If head node
tail.next = current.next;
} else if (current == tail) { // If tail node
prev.next = tail.next;
tail = prev;
} else { // If middle node
prev.next = current.next;
}
size--; // Decrements size
return;
}
prev = current;
current = current.next;
} while (current != tail.next); // Loops until back to head
throw new IllegalArgumentException("Value not found in the circular linked list.");
}
// Search: Checks if a value exists in the circular linked list
public boolean search(int value) {
if (isEmpty()) { // Checks if the list is empty
return false;
}
Node current = tail.next; // Starts at head
do {
if (current.value == value) { // Returns true if value is found
return true;
}
current = current.next;
} while (current != tail.next); // Loops until back to head
return false; // Returns false if value is not found
}
// isEmpty: Checks if the circular linked list is empty
public boolean isEmpty() {
return size == 0;
}
// Size: Returns the number of nodes in the circular linked list
public int size() {
return size;
}
}
How It Works
- Initialization: The constructor initializes
tailas null andsizeas 0, indicating an empty circular linked list. - Insert at Head:
- The method creates a new node with the given value.
- If the list is empty, it sets
tailto the new node and links it to itself. - Otherwise, it sets the new node’s
nextto the current head (tail.next), updatestail.nextto the new node, and incrementssize.
- Insert at Tail:
- The method creates a new node.
- If the list is empty, it sets
tailto the new node and links it to itself. - Otherwise, it sets the new node’s
nextto the current head, updatestail.nextto the new node, setstailto the new node, and incrementssize.
- Delete at Head:
- The method checks if the list is empty. If not, it updates
tail.nextto the second node (new head) if multiple nodes exist, or setstailto null if only one node, then decrementssize. If empty, it throws an exception.
- The method checks if the list is empty. If not, it updates
- Delete by Value:
- The method traverses from the head (
tail.next) to find the value, keeping track of the previous node. - If found, it handles three cases: single node (clears the list), head node (updates
tail.next), tail node (updatestailandprev.next), or middle node (updatesprev.next). It decrementssize. - If the value is not found after a full loop, it throws an exception.
- The method traverses from the head (
- Search Operation: The method traverses from the head, returning true if the value is found, or false after a full loop to the head.
- isEmpty Operation: The method returns true if
sizeequals 0, indicating an empty list, and false otherwise. - Size Operation: The method returns
size, which tracks the number of nodes.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Insert at Head | O(1) | O(1) |
| Insert at Tail | O(1) | O(1) |
| Delete at Head | O(1) | O(1) |
| Delete by Value | O(n) | O(1) |
| Search | O(n) | O(1) |
| isEmpty | O(1) | O(1) |
| Size | O(1) | O(1) |
Note: n is the number of nodes in the circular linked list. O(1) operations assume a tail pointer is maintained.
Key Differences / Notes
- Circular vs. Singly Linked List:
- The implementation above is a singly circular linked list, where the last node points to the head, enabling continuous traversal. A singly linked list ends with a null pointer, requiring traversal to restart from the head.
- Circular linked lists are more complex to manage due to the loop, but they simplify cyclic operations.
- Circular vs. Doubly Circular Linked List:
- A doubly circular linked list includes previous pointers, allowing bidirectional traversal, but uses more memory.
- The singly circular linked list is simpler and more memory-efficient but only supports forward traversal.
- Thread Safety: The implementation is not thread-safe. For concurrent applications, use Java’s
ConcurrentLinkedDequeor synchronize access. - Java’s Built-in Support: Java’s
LinkedListcan be adapted for circular behavior, but no direct circular linked list class is provided. Libraries like Apache Commons Collections offer circular list implementations.
✅ Tip: Use a circular linked list when your application requires continuous cycling through elements, such as in scheduling or looping playlists. Maintaining a tail pointer simplifies head and tail operations to O(1).
⚠ Warning: Be cautious when traversing a circular linked list to avoid infinite loops. Always use a condition (e.g., checking for the head node) to terminate traversal.
Exercises
- Rotate the List: Write a Java program that rotates the circular linked list by k positions (e.g., move the head k nodes forward). Test with different values of k and list sizes.
- Split Circular List: Implement a method to split a circular linked list into two circular linked lists of roughly equal size. Test with even and odd-sized lists.
- Josephus Problem: Solve the Josephus problem using the circular linked list, where every k-th person in a circle is eliminated until one remains. Test with different k values.
- Insert After Value: Add a method to insert a new node after the first occurrence of a given value in the circular linked list. Test with cases where the value exists and does not exist.
- Round-Robin Scheduler: Create a program that simulates a round-robin scheduler using the circular linked list. Allow users to add tasks (nodes), cycle through them, and remove completed tasks.
Hashing Data Structure
Definition and Concepts
Hashing is a technique used to map data of arbitrary size to fixed-size values, typically for efficient storage and retrieval in a data structure called a hash table. A hash table uses a hash function to compute an index (or hash code) for each key, which determines where the key-value pair is stored in an array. The goal is to enable fast access to data using keys, such as strings or numbers. You can visualize a hash table as a set of labeled drawers, where each label (key) quickly directs you to the corresponding drawer (value). Collisions, where multiple keys map to the same index, are resolved using techniques like separate chaining (using linked lists) or open addressing (using probing). Hashing is fundamental for applications requiring quick lookups, insertions, and deletions.
Why Use It?
Hashing is used to achieve fast data retrieval, insertion, and deletion with an average time complexity of O(1). It is ideal for scenarios where you need to associate keys with values and access them efficiently without iterating through the entire dataset. Hash tables are versatile and widely used in databases, caches, and other systems requiring rapid data access.
Where to Use? (Real-Life Examples)
- Database Indexing: Databases use hashing to index records, allowing quick retrieval of data based on keys like user IDs.
- Caches: Web browsers and content delivery networks use hash tables to cache web pages, mapping URLs to cached content for fast access.
- Symbol Tables in Compilers: Compilers use hash tables to store variable names and their attributes during code compilation.
- Password Storage: Systems store hashed passwords to verify user credentials quickly without storing plain text.
Explain Operations
- Insert: This operation adds a key-value pair to the hash table. The hash function computes an index for the key, and the pair is stored at that index (or in a linked list if a collision occurs). It has an average time complexity of O(1).
- Lookup: This operation retrieves the value associated with a given key by computing its hash and checking the corresponding index. It has an average time complexity of O(1).
- Delete: This operation removes a key-value pair by finding the key’s index and removing it from the bucket (or linked list). It has an average time complexity of O(1).
- isEmpty: This operation checks whether the hash table is empty. It has a time complexity of O(1).
- Size: This operation returns the number of key-value pairs in the hash table. It has a time complexity of O(1).
Java Implementation
The following Java code implements a hash table using separate chaining (linked lists for collision resolution).
public class HashTable {
private static class Node {
String key; // Key for the hash table entry
int value; // Value associated with the key
Node next; // Reference to the next node in case of collision
Node(String key, int value) {
this.key = key;
this.value = value;
this.next = null;
}
}
private Node[] buckets; // Array of linked lists for storing key-value pairs
private int capacity; // Size of the buckets array
private int size; // Number of key-value pairs in the hash table
// Constructor to initialize the hash table with a given capacity
public HashTable(int capacity) {
this.capacity = capacity;
this.buckets = new Node[capacity];
this.size = 0;
}
// Hash function to compute index for a key
private int hash(String key) {
return Math.abs(key.hashCode() % capacity); // Ensures non-negative index
}
// Insert: Adds a key-value pair to the hash table
public void insert(String key, int value) {
int index = hash(key); // Computes the index for the key
if (buckets[index] == null) { // If bucket is empty, create new node
buckets[index] = new Node(key, value);
size++;
return;
}
Node current = buckets[index];
while (current != null) { // Traverses the linked list
if (current.key.equals(key)) { // Updates value if key exists
current.value = value;
return;
}
if (current.next == null) break; // Reaches the end of the list
current = current.next;
}
current.next = new Node(key, value); // Adds new node at the end
size++;
}
// Lookup: Retrieves the value for a given key
public int lookup(String key) {
int index = hash(key); // Computes the index for the key
Node current = buckets[index];
while (current != null) { // Traverses the linked list
if (current.key.equals(key)) {
return current.value; // Returns value if key is found
}
current = current.next;
}
throw new IllegalArgumentException("Key not found in the hash table.");
}
// Delete: Removes a key-value pair from the hash table
public void delete(String key) {
int index = hash(key); // Computes the index for the key
Node current = buckets[index];
Node prev = null;
while (current != null) { // Traverses the linked list
if (current.key.equals(key)) {
if (prev == null) { // If key is at the head
buckets[index] = current.next;
} else {
prev.next = current.next; // Bypasses the node
}
size--;
return;
}
prev = current;
current = current.next;
}
throw new IllegalArgumentException("Key not found in the hash table.");
}
// isEmpty: Checks if the hash table is empty
public boolean isEmpty() {
return size == 0;
}
// Size: Returns the number of key-value pairs in the hash table
public int size() {
return size;
}
}
How It Works
- Initialization: The constructor initializes an array of
Nodeobjects (buckets) with a givencapacityand setssizeto 0, indicating an empty hash table. - Hash Function: The
hashmethod computes an index by applying Java’shashCodeto the key and using modulocapacityto fit within the array bounds, ensuring a non-negative index. - Insert Operation:
- The method computes the index for the key using the hash function.
- If the bucket at that index is empty, it creates a new
Nodewith the key-value pair. - If the bucket contains a linked list, it traverses to check if the key exists (updating the value if found) or appends a new node at the end.
- It increments
sizefor new entries.
- Lookup Operation:
- The method computes the index for the key and traverses the linked list at that index.
- If the key is found, it returns the associated value; otherwise, it throws an exception.
- Delete Operation:
- The method computes the index and traverses the linked list to find the key.
- If found, it removes the node by updating pointers (either setting the bucket to the next node or bypassing the node) and decrements
size. - If the key is not found, it throws an exception.
- isEmpty Operation: The method returns true if
sizeequals 0, indicating an empty hash table, and false otherwise. - Size Operation: The method returns
size, which represents the number of key-value pairs.
Complexity Analysis Table
| Operation | Average Time Complexity | Worst-Case Time Complexity | Space Complexity |
|---|---|---|---|
| Insert | O(1) | O(n) | O(1) |
| Lookup | O(1) | O(n) | O(1) |
| Delete | O(1) | O(n) | O(1) |
| isEmpty | O(1) | O(1) | O(1) |
| Size | O(1) | O(1) | O(1) |
Note: Worst-case time complexity occurs when many keys hash to the same index, causing long linked lists.
Key Differences / Notes
- Separate Chaining vs. Open Addressing:
- The implementation above uses separate chaining (linked lists) to handle collisions, which is simpler and handles high load factors well.
- Open addressing resolves collisions by probing for the next available slot, which can be more memory-efficient but requires careful handling of deletions.
- Load Factor: The load factor (size/capacity) affects performance. A high load factor increases collisions, slowing operations. Resizing the hash table (doubling capacity) can maintain efficiency.
- Thread Safety: The provided implementation is not thread-safe. For concurrent applications, use Java’s
ConcurrentHashMapor synchronize access. - Java’s Built-in Hash Table: Java provides
HashMapandHashtableinjava.util.HashMapis preferred for non-thread-safe applications, whileConcurrentHashMapis used for thread safety.
✅ Tip: Choose a good hash function to minimize collisions, as this directly impacts performance. Java’s
hashCodeis a reasonable default, but custom hash functions may be needed for specific key types.
⚠ Warning: A poorly designed hash function or a small table size can lead to frequent collisions, degrading performance to O(n) in the worst case. Monitor the load factor and resize the table if necessary.
Exercises
- Word Frequency Counter: Write a Java program that uses the hash table implementation above to count the frequency of words in a given text. Use words as keys and their counts as values, then print the results.
- Phone Book Application: Create a program that implements a phone book using the hash table. Allow users to insert, lookup, and delete contacts (name as key, phone number as value).
- Collision Analysis: Modify the hash table to track the number of collisions (multiple keys mapping to the same index). Test it with a set of keys and report the collision count.
- Two Sum Problem: Implement a solution to the Two Sum problem (find two numbers in an array that add up to a target sum) using the hash table. Return the indices of the two numbers.
- Custom Hash Function: Create a custom hash function for a specific key type (e.g., strings with a fixed format) and integrate it into the hash table implementation. Compare its performance with Java’s default
hashCode.
Tree Data Structure
Definition and Concepts
A tree is a hierarchical data structure consisting of nodes connected by edges, with a single root node at the top and no cycles. Each node can have zero or more child nodes, and every node except the root has exactly one parent. In a Binary Search Tree (BST), which is a common type of tree, each node has at most two children (left and right), and the left subtree contains values less than the node’s value, while the right subtree contains values greater. Trees are used to represent hierarchical relationships and enable efficient searching, insertion, and deletion. You can visualize a tree as a family tree, where each person (node) has descendants (children) and an ancestor (parent).
Why Use It?
Trees are used to organize data hierarchically, enabling efficient operations like searching, insertion, and deletion with an average time complexity of O(log n) in a balanced BST. They are ideal for applications requiring ordered data, dynamic updates, or hierarchical structures. Trees provide a natural way to represent relationships, such as organizational charts or file systems, and are foundational for advanced data structures like heaps and tries.
Where to Use? (Real-Life Examples)
- File Systems: Operating systems use trees to represent directory structures, where folders are nodes and subfolders or files are children.
- Database Indexing: Databases use trees (e.g., B-trees or BSTs) to index data, enabling fast searches and range queries.
- Expression Parsing: Compilers use expression trees to represent mathematical expressions, such as
(2 + 3) * 4, for evaluation. - Autocomplete Systems: Search engines use trie (a type of tree) to store words for efficient prefix-based suggestions.
Explain Operations
- Insert: This operation adds a new node with a given value to the tree while maintaining the BST property. It has an average time complexity of O(log n) in a balanced tree.
- Search: This operation finds a node with a given value by traversing left or right based on comparisons. It has an average time complexity of O(log n).
- Delete: This operation removes a node with a given value, adjusting the tree to maintain the BST property. It has an average time complexity of O(log n).
- Inorder Traversal: This operation visits nodes in sorted order (left, root, right). It has a time complexity of O(n).
- isEmpty: This operation checks whether the tree is empty. It has a time complexity of O(1).
Java Implementation
The following Java code implements a Binary Search Tree with basic operations.
public class BinarySearchTree {
private class Node {
int value; // Value stored in the node
Node left; // Reference to the left child
Node right; // Reference to the right child
Node(int value) {
this.value = value;
this.left = null;
this.right = null;
}
}
private Node root; // Root of the BST
private int size; // Number of nodes in the tree
// Constructor to initialize an empty BST
public BinarySearchTree() {
root = null;
size = 0;
}
// Insert: Adds a new value to the BST
public void insert(int value) {
root = insertRec(root, value); // Recursively inserts the value
size++; // Increments the size
}
private Node insertRec(Node root, int value) {
if (root == null) { // If the subtree is empty, create a new node
return new Node(value);
}
if (value < root.value) { // Inserts into left subtree if value is smaller
root.left = insertRec(root.left, value);
} else if (value > root.value) { // Inserts into right subtree if value is larger
root.right = insertRec(root.right, value);
}
return root; // Returns unchanged root if value already exists
}
// Search: Checks if a value exists in the BST
public boolean search(int value) {
return searchRec(root, value); // Recursively searches for the value
}
private boolean searchRec(Node root, int value) {
if (root == null || root.value == value) { // Returns true if found, false if null
return root != null;
}
if (value < root.value) { // Searches left subtree if value is smaller
return searchRec(root.left, value);
}
return searchRec(root.right, value); // Searches right subtree if value is larger
}
// Delete: Removes a value from the BST
public void delete(int value) {
root = deleteRec(root, value); // Recursively deletes the value
size--; // Decrements the size
}
private Node deleteRec(Node root, int value) {
if (root == null) { // If value not found, return null
return null;
}
if (value < root.value) { // Deletes from left subtree if value is smaller
root.left = deleteRec(root.left, value);
} else if (value > root.value) { // Deletes from right subtree if value is larger
root.right = deleteRec(root.right, value);
} else { // Node to delete found
if (root.left == null) { // Case 1: No left child, return right child
return root.right;
} else if (root.right == null) { // Case 2: No right child, return left child
return root.left;
}
// Case 3: Two children, replace with successor
root.value = findMin(root.right).value; // Replaces with minimum in right subtree
root.right = deleteRec(root.right, root.value); // Deletes the successor
}
return root;
}
// Helper method to find the minimum value node in a subtree
private Node findMin(Node root) {
while (root.left != null) {
root = root.left;
}
return root;
}
// Inorder Traversal: Prints nodes in sorted order
public void inorder() {
inorderRec(root); // Recursively traverses the tree
System.out.println(); // Adds newline after traversal
}
private void inorderRec(Node root) {
if (root != null) {
inorderRec(root.left); // Visits left subtree
System.out.print(root.value + " "); // Visits current node
inorderRec(root.right); // Visits right subtree
}
}
// isEmpty: Checks if the BST is empty
public boolean isEmpty() {
return size == 0;
}
// Size: Returns the number of nodes in the BST
public int size() {
return size;
}
}
How It Works
- Initialization: The constructor initializes the
rootas null andsizeas 0, indicating an empty Binary Search Tree. - Insert Operation:
- The method recursively traverses the tree based on the value: left if the value is smaller, right if larger.
- If an empty spot is found (null node), it creates a new node with the value.
- The
sizeis incremented for each new node.
- Search Operation:
- The method recursively traverses the tree, comparing the target value with the current node’s value.
- It returns true if the value is found or false if a null node is reached.
- Delete Operation:
- The method finds the node to delete by traversing left or right based on the value.
- It handles three cases: no children (return null), one child (return the child), or two children (replace with the minimum value from the right subtree and delete that node).
- The
sizeis decremented after deletion.
- Inorder Traversal: The method recursively visits the left subtree, the current node, and the right subtree, printing values in sorted order.
- isEmpty Operation: The method returns true if
sizeequals 0, indicating an empty tree, and false otherwise. - Size Operation: The method returns
size, which represents the number of nodes in the tree. - FindMin Helper: This method finds the smallest value in a subtree by traversing left until a null left child is reached.
Complexity Analysis Table
| Operation | Average Time Complexity | Worst-Case Time Complexity | Space Complexity |
|---|---|---|---|
| Insert | O(log n) | O(n) | O(log n) |
| Search | O(log n) | O(n) | O(log n) |
| Delete | O(log n) | O(n) | O(log n) |
| Inorder Traversal | O(n) | O(n) | O(log n) |
| isEmpty | O(1) | O(1) | O(1) |
| Size | O(1) | O(1) | O(1) |
Note: Worst-case time complexity occurs in an unbalanced tree (e.g., a skewed tree resembling a linked list).
Key Differences / Notes
- Binary Search Tree vs. Other Trees:
- The implementation above is a BST, where nodes are ordered for efficient searching. Other trees include binary trees (no ordering), AVL trees (self-balancing), and B-trees (for databases).
- AVL and Red-Black trees maintain balance to ensure O(log n) operations, unlike a BST, which can degrade to O(n) if unbalanced.
- Balancing: The provided BST is not self-balancing, which can lead to poor performance if insertions are not random. Use AVL or Red-Black trees for guaranteed balance.
- Thread Safety: The implementation is not thread-safe. For concurrent applications, use synchronized methods or Java’s
ConcurrentSkipListMapfor a tree-like structure. - Java’s Built-in Trees: Java provides
TreeMapandTreeSetinjava.util, which are implemented as Red-Black trees for balanced performance.
✅ Tip: Use a self-balancing tree like AVL or Red-Black for applications requiring consistent O(log n) performance, especially with frequent insertions or deletions.
⚠ Warning: An unbalanced BST can degrade to O(n) performance if insertions occur in sorted order, resembling a linked list. Always consider input patterns when using a BST.
Exercises
- BST Validator: Write a Java program that checks if a given tree is a valid Binary Search Tree by ensuring all nodes follow the BST property. Test it with at least three different trees.
- Height of BST: Extend the BST implementation to include a method that computes the height of the tree. Test it with balanced and unbalanced trees.
- Range Sum Query: Create a program that uses the BST to find the sum of all values within a given range (e.g., between 20 and 60). Use inorder traversal to collect values.
- Preorder and Postorder Traversals: Add methods to the BST implementation for preorder (root, left, right) and postorder (left, right, root) traversals. Print the results for a sample tree.
- Lowest Common Ancestor: Implement a method to find the lowest common ancestor of two nodes in the BST. Test it with different pairs of values.
Graph Data Structure
Definition and Concepts
A graph is a non-linear data structure consisting of nodes (also called vertices) connected by edges. Graphs can represent relationships between entities, where nodes represent entities and edges represent connections. Graphs can be directed (edges have direction, like one-way roads) or undirected (edges are bidirectional, like friendships). They can also be weighted (edges have values, like distances) or unweighted. A common representation is an adjacency list, where each node stores a list of its adjacent nodes. You can visualize a graph as a network of cities (nodes) connected by roads (edges). Graphs are versatile and used to model complex relationships in various applications.
Why Use It?
Graphs are used to model and analyze relationships between entities, enabling solutions to problems like finding shortest paths, detecting cycles, or determining connectivity. They provide a flexible framework for representing networks, making them essential for applications in computer science, social networks, and logistics. Graphs support efficient algorithms for traversal, pathfinding, and optimization, with time complexities depending on the representation and algorithm used.
Where to Use? (Real-Life Examples)
- Social Networks: Social media platforms use graphs to represent users (nodes) and friendships or follows (edges) to recommend connections or analyze communities.
- Navigation Systems: GPS applications use graphs to model road networks, with cities as nodes and roads as weighted edges, to compute shortest paths.
- Internet Routing: The internet uses graphs to represent routers and connections, enabling efficient data packet routing.
- Dependency Management: Build tools like Maven use graphs to manage dependencies between software modules, detecting cyclic dependencies.
Explain Operations
- Add Vertex: This operation adds a new node to the graph. It has a time complexity of O(1) in an adjacency list.
- Add Edge: This operation adds a connection between two nodes. It has a time complexity of O(1) for an undirected graph using an adjacency list.
- Remove Vertex: This operation removes a node and all its edges. It has a time complexity of O(V + E), where V is the number of vertices and E is the number of edges.
- Remove Edge: This operation removes a connection between two nodes. It has a time complexity of O(E) in the worst case for an adjacency list.
- Depth-First Search (DFS): This operation traverses the graph by exploring as far as possible along each branch before backtracking. It has a time complexity of O(V + E).
- Breadth-First Search (BFS): This operation traverses the graph level by level, visiting all neighbors of a node before moving to the next level. It has a time complexity of O(V + E).
Java Implementation
The following Java code implements an undirected graph using an adjacency list, with basic operations and DFS/BFS traversals.
import java.util.*;
public class Graph {
private Map<Integer, List<Integer>> adjList; // Adjacency list to store the graph
private int vertexCount; // Number of vertices in the graph
// Constructor to initialize an empty graph
public Graph() {
adjList = new HashMap<>();
vertexCount = 0;
}
// Add Vertex: Adds a new vertex to the graph
public void addVertex(int vertex) {
if (!adjList.containsKey(vertex)) { // Checks if vertex doesn't already exist
adjList.put(vertex, new ArrayList<>());
vertexCount++;
}
}
// Add Edge: Adds an undirected edge between two vertices
public void addEdge(int vertex1, int vertex2) {
if (!adjList.containsKey(vertex1) || !adjList.containsKey(vertex2)) {
throw new IllegalArgumentException("Both vertices must exist in the graph.");
}
adjList.get(vertex1).add(vertex2); // Adds vertex2 to vertex1's list
adjList.get(vertex2).add(vertex1); // Adds vertex1 to vertex2's list (undirected)
}
// Remove Vertex: Removes a vertex and all its edges
public void removeVertex(int vertex) {
if (!adjList.containsKey(vertex)) {
throw new IllegalArgumentException("Vertex not found in the graph.");
}
List<Integer> neighbors = adjList.get(vertex);
for (int neighbor : neighbors) { // Removes vertex from neighbors' lists
adjList.get(neighbor).remove(Integer.valueOf(vertex));
}
adjList.remove(vertex); // Removes the vertex
vertexCount--;
}
// Remove Edge: Removes an undirected edge between two vertices
public void removeEdge(int vertex1, int vertex2) {
if (!adjList.containsKey(vertex1) || !adjList.containsKey(vertex2)) {
throw new IllegalArgumentException("Both vertices must exist in the graph.");
}
adjList.get(vertex1).remove(Integer.valueOf(vertex2)); // Removes vertex2 from vertex1's list
adjList.get(vertex2).remove(Integer.valueOf(vertex1)); // Removes vertex1 from vertex2's list
}
// Depth-First Search: Traverses the graph starting from a vertex
public List<Integer> dfs(int startVertex) {
if (!adjList.containsKey(startVertex)) {
throw new IllegalArgumentException("Start vertex not found in the graph.");
}
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
dfsRec(startVertex, visited, result);
return result;
}
private void dfsRec(int vertex, Set<Integer> visited, List<Integer> result) {
visited.add(vertex); // Marks the vertex as visited
result.add(vertex); // Adds vertex to result
for (int neighbor : adjList.get(vertex)) { // Visits unvisited neighbors
if (!visited.contains(neighbor)) {
dfsRec(neighbor, visited, result);
}
}
}
// Breadth-First Search: Traverses the graph starting from a vertex
public List<Integer> bfs(int startVertex) {
if (!adjList.containsKey(startVertex)) {
throw new IllegalArgumentException("Start vertex not found in the graph.");
}
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
Queue<Integer> queue = new LinkedList<>();
visited.add(startVertex);
queue.offer(startVertex);
while (!queue.isEmpty()) {
int vertex = queue.poll(); // Dequeues a vertex
result.add(vertex); // Adds vertex to result
for (int neighbor : adjList.get(vertex)) { // Enqueues unvisited neighbors
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
return result;
}
// isEmpty: Checks if the graph is empty
public boolean isEmpty() {
return vertexCount == 0;
}
// Size: Returns the number of vertices in the graph
public int size() {
return vertexCount;
}
}
How It Works
- Initialization: The constructor initializes an empty
HashMapfor the adjacency list and setsvertexCountto 0, indicating an empty graph. - Add Vertex: The method adds a new vertex to the
adjListwith an empty list of neighbors if it doesn’t already exist, incrementingvertexCount. - Add Edge: The method adds an undirected edge by appending each vertex to the other’s neighbor list in the
adjList, ensuring both vertices exist. - Remove Vertex: The method removes the vertex’s neighbor list and removes the vertex from all its neighbors’ lists, then removes the vertex from
adjListand decrementsvertexCount. - Remove Edge: The method removes each vertex from the other’s neighbor list in the
adjList, ensuring both vertices exist. - Depth-First Search (DFS): The method uses recursion to explore as far as possible along each branch, marking vertices as visited and adding them to the result list.
- Breadth-First Search (BFS): The method uses a queue to explore nodes level by level, marking vertices as visited, adding them to the result, and enqueuing unvisited neighbors.
- isEmpty Operation: The method returns true if
vertexCountequals 0, indicating an empty graph, and false otherwise. - Size Operation: The method returns
vertexCount, which represents the number of vertices in the graph.
Complexity Analysis Table
| Operation | Time Complexity (Adjacency List) | Space Complexity |
|---|---|---|
| Add Vertex | O(1) | O(1) |
| Add Edge | O(1) | O(1) |
| Remove Vertex | O(V + E) | O(1) |
| Remove Edge | O(E) | O(1) |
| DFS | O(V + E) | O(V) |
| BFS | O(V + E) | O(V) |
| isEmpty | O(1) | O(1) |
| Size | O(1) | O(1) |
Note: V is the number of vertices, and E is the number of edges. Time complexities assume an adjacency list representation.
Key Differences / Notes
- Adjacency List vs. Adjacency Matrix:
- The implementation above uses an adjacency list, which is space-efficient (O(V + E)) and suitable for sparse graphs.
- An adjacency matrix uses O(V²) space and is better for dense graphs or when checking edge existence is frequent.
- Directed vs. Undirected Graphs:
- The implementation is for an undirected graph. For a directed graph, only add the edge to the source vertex’s neighbor list.
- Weighted Graphs: The implementation is unweighted. For weighted graphs, store edge weights in the adjacency list (e.g., as a
Map<Integer, Integer>for weights). - Thread Safety: The implementation is not thread-safe. For concurrent applications, use Java’s
ConcurrentHashMapfor the adjacency list or synchronize access. - Java’s Built-in Support: Java does not provide a direct graph class, but libraries like JGraphT or Guava’s Graph can be used for advanced graph operations.
✅ Tip: Use an adjacency list for sparse graphs to save memory, and an adjacency matrix for dense graphs to optimize edge lookups.
⚠ Warning: Ensure the graph is properly initialized with all vertices before adding edges, as missing vertices can cause errors in adjacency list operations.
Exercises
- Graph Connectivity: Write a Java program that uses the graph implementation to check if a graph is connected (all vertices reachable from a starting vertex) using DFS or BFS.
- Cycle Detection: Extend the graph implementation to detect if the graph contains a cycle using DFS. Test it with cyclic and acyclic graphs.
- Shortest Path (Unweighted): Implement a method to find the shortest path between two vertices in an unweighted graph using BFS. Return the path as a list of vertices.
- Weighted Graph Extension: Modify the graph implementation to support weighted edges (store weights in the adjacency list). Test it by adding and retrieving edge weights.
- Social Network Simulation: Create a program that simulates a social network using the graph. Allow users to add users (vertices), friendships (edges), and perform DFS to find mutual friends.
Sorting Algorithms
What are Sorting Algorithms?
Sorting algorithms are methods for arranging elements of a list in a specific order, typically ascending or descending, to make data easier to search, process, or display. They are the building blocks of efficient data processing, used in everything from simple list sorting to complex database operations.
Why Learn Sorting Algorithms?
- Efficiency: Choosing the right sorting algorithm can significantly improve program performance.
- Problem Solving: Many real-world problems, like ranking or organizing data, rely on sorting.
- Foundation for Advanced Algorithms: Understanding sorting is essential for mastering more complex algorithms.
- Interview Readiness: Sorting algorithms are a common topic in technical interviews.
📖 What You Will Learn in This Chapter
This chapter introduces you to the most commonly used sorting algorithms, their uses, advantages, disadvantages, and Java implementations.
1. Bubble Sort
- What is Bubble Sort and how it works.
- Comparing and swapping adjacent elements.
- Limitations for large datasets.
- Real-life example: Sorting a small list of exam scores.
2. Selection Sort
- How Selection Sort selects the smallest element.
- Building a sorted portion incrementally.
- Minimizing swaps for efficiency.
- Real-life example: Arranging books by height on a shelf.
3. Insertion Sort
- How Insertion Sort builds a sorted portion by inserting elements.
- Efficiency for nearly sorted data.
- Common operations like shifting elements.
- Real-life example: Sorting a hand of playing cards.
4. Merge Sort
- How Merge Sort divides and conquers.
- Merging sorted subarrays for efficiency.
- Handling large datasets and stability.
- Real-life example: Sorting large customer records in a database.
🛠 How We Will Learn
For each sorting algorithm, we will cover:
- Definition – What it is and how it works.
- Why – Advantages and limitations of the algorithm.
- Where to Use – Real-life scenarios where the algorithm shines.
- Java Implementation – With step-by-step explanations.
- Complexity Analysis – Understanding performance trade-offs.
Bubble Sort
Definition and Concepts
Bubble Sort is a simple sorting algorithm that repeatedly iterates through a list, compares adjacent elements, and swaps them if they are in the wrong order, until the list is fully sorted. The algorithm gets its name because larger elements "bubble up" to the end of the list in each iteration, similar to bubbles rising in water. In Java, Bubble Sort is typically implemented on arrays due to their direct index-based access, which makes comparisons and swaps efficient. You can visualize Bubble Sort as a process where, in each pass, the largest unsorted element moves to its correct position at the end of the array. Bubble Sort is a stable sorting algorithm, meaning it preserves the relative order of equal elements, and it is in-place, requiring minimal extra memory.
Key Points:
- Bubble Sort compares adjacent elements and swaps them if they are out of order (e.g., for ascending order, if the first element is larger than the second).
- Each pass through the array places the next largest element in its final position.
- The algorithm terminates early if no swaps are needed in a pass, indicating the list is sorted.
- Bubble Sort is easy to implement but inefficient for large datasets due to its quadratic time complexity.
Why Use It?
Bubble Sort is used primarily for educational purposes because its logic is straightforward and easy for beginners to understand, making it an excellent introduction to sorting algorithms. It is suitable for small datasets or lists that are already nearly sorted, where its simplicity and minimal memory usage are advantageous. Bubble Sort requires only a constant amount of extra space, making it appropriate for environments with memory constraints. However, for large or complex datasets, more efficient algorithms like Quick Sort or Merge Sort are recommended.
Where to Use? (Real-Life Examples)
- Educational Tools: Bubble Sort is used in teaching environments to demonstrate sorting concepts due to its simple comparison and swap mechanism.
- Small Data Sorting: Embedded systems with limited resources use Bubble Sort to sort small lists, such as a few sensor readings, where simplicity outweighs performance concerns.
- Nearly Sorted Data: Applications like real-time data feeds use Bubble Sort for nearly sorted lists, as it performs efficiently with few swaps in such cases.
- Prototyping: Developers use Bubble Sort in early-stage prototypes to quickly sort small datasets for testing, leveraging its ease of implementation.
Explain Operations
- Comparison: This operation compares two adjacent elements to determine if they are in the correct order (e.g., for ascending order, the first element should be less than or equal to the second). It has a time complexity of O(1).
- Swap: This operation exchanges two adjacent elements if they are out of order. It has a time complexity of O(1).
- Pass: This operation involves one iteration through the unsorted portion of the array, performing comparisons and swaps as needed. It has a time complexity of O(n) for n elements in the worst case.
- Optimization Check: This operation uses a flag to track whether any swaps occurred in a pass. If no swaps are needed, the array is sorted, and the algorithm terminates early. It has a time complexity of O(1) per pass.
Java Implementation
The following Java code implements Bubble Sort for an array of integers, including an optimization to terminate early if the array is already sorted.
public class BubbleSort {
// Bubble Sort: Sorts an array in ascending order
public void bubbleSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return; // No sorting needed for null or single-element arrays
}
int n = arr.length;
for (int i = 0; i < n - 1; i++) { // Iterate n-1 times
boolean swapped = false; // Tracks if swaps occurred in this pass
for (int j = 0; j < n - 1 - i; j++) { // Compare adjacent elements
if (arr[j] > arr[j + 1]) { // If out of order, swap
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true;
}
}
if (!swapped) { // If no swaps, array is sorted
break;
}
}
}
}
How It Works
- Check Input:
- The
bubbleSortmethod checks if the input array is null or has one or fewer elements. If so, it returns immediately, as no sorting is needed.
- The
- Outer Loop (Passes):
- The outer loop iterates
n-1times, wherenis the array length, as each pass places one element in its final position. - For example, in an array of 5 elements, 4 passes are sufficient to sort the array.
- The outer loop iterates
- Inner Loop (Comparisons and Swaps):
- The inner loop iterates through the unsorted portion of the array (from index 0 to
n-1-i), comparing adjacent elements (arr[j]andarr[j+1]). - If
arr[j] > arr[j+1], the elements are swapped using a temporary variable. - For example, in the array
[5, 3, 8, 1, 9], the first pass compares and swaps 5 and 3, then 8 and 1, resulting in[3, 5, 1, 8, 9].
- The inner loop iterates through the unsorted portion of the array (from index 0 to
- Optimization Check:
- A
swappedflag tracks whether any swaps occurred in a pass. If no swaps are needed, the array is sorted, and the algorithm terminates early. - For example, if the array is already sorted like
[1, 3, 5, 8, 9], the algorithm stops after one pass with no swaps.
- A
- Result:
- After all passes, the array is sorted in ascending order. For example,
[5, 3, 8, 1, 9]becomes[1, 3, 5, 8, 9].
- After all passes, the array is sorted in ascending order. For example,
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Comparison | O(1) | O(1) |
| Swap | O(1) | O(1) |
| Pass | O(n) | O(1) |
| Full Algorithm | O(n) (best) | O(1) |
| Full Algorithm | O(n²) (average/worst) | O(1) |
Note:
- n is the number of elements in the array.
- Best case (O(n)) occurs when the array is already sorted, requiring one pass with no swaps.
- Average and worst cases (O(n²)) involve up to n passes, each with up to n comparisons/swaps.
- Space complexity is O(1) as Bubble Sort is in-place, using only a few variables.
Key Differences / Notes
- Bubble Sort vs. Other Sorting Algorithms:
- Bubble Sort is simpler but less efficient than Quick Sort (O(n log n) average) or Merge Sort (O(n log n)), which are preferred for large datasets.
- It is stable, preserving the relative order of equal elements, unlike Quick Sort.
- It is in-place, unlike Merge Sort, which requires O(n) extra space.
- Optimization:
- The
swappedflag allows early termination for nearly sorted arrays, reducing the number of passes.
- The
- Suitability for Data Structures:
- Bubble Sort is most efficient for arrays due to O(1) access and swap operations.
- It is less practical for linked lists or other structures due to inefficient access patterns, which is why this implementation focuses on arrays.
- Limitations:
- Bubble Sort is rarely used in production code due to its O(n²) time complexity, but it is valuable for teaching and small-scale applications.
✅ Tip: Use Bubble Sort for small arrays or nearly sorted data to take advantage of its simplicity and early termination optimization. Test your implementation with edge cases like empty, single-element, or already sorted arrays to ensure robustness.
⚠ Warning: Avoid using Bubble Sort for large datasets due to its O(n²) time complexity, which performs poorly compared to faster algorithms like Quick Sort or Merge Sort. Always validate array bounds to prevent
ArrayIndexOutOfBoundsException.
Exercises
- Basic Bubble Sort: Implement Bubble Sort for an array of integers and test it with various inputs (e.g., unsorted, sorted, reversed, duplicates). Count the number of swaps performed.
- Descending Order: Modify the Bubble Sort implementation to sort an array in descending order. Test with different array sizes and contents.
- Performance Analysis: Write a program that measures the execution time of Bubble Sort for arrays of increasing sizes (e.g., 10, 100, 1000 elements). Compare best, average, and worst cases.
- Flag Optimization: Implement Bubble Sort without the
swappedflag and compare its performance with the optimized version for nearly sorted arrays. - Edge Case Handling: Enhance the Bubble Sort implementation to handle arrays with negative numbers and floating-point numbers. Test with diverse inputs.
Selection Sort
Definition and Concepts
Selection Sort is a simple, in-place sorting algorithm that divides an array into a sorted and an unsorted portion. In each iteration, the algorithm selects the smallest (or largest, for descending order) element from the unsorted portion and places it at the beginning of the sorted portion. You can visualize Selection Sort as repeatedly finding the smallest element in the remaining unsorted section of the array and swapping it with the first element of that section. In Java, Selection Sort is typically implemented on arrays due to their direct index-based access, which facilitates efficient comparisons and swaps. Selection Sort is not stable, meaning it may change the relative order of equal elements, but it is in-place, requiring minimal extra memory.
Key Points:
- Selection Sort selects the minimum element from the unsorted portion and places it at the start of the sorted portion.
- Each iteration reduces the size of the unsorted portion by one, growing the sorted portion.
- The algorithm is simple to implement but inefficient for large datasets due to its quadratic time complexity.
- It performs a fixed number of swaps, which can be advantageous in scenarios where swaps are costly.
Why Use It?
Selection Sort is used primarily for educational purposes because its logic is intuitive and easy for beginners to grasp, making it a good introduction to sorting algorithms alongside Bubble Sort. It is suitable for small datasets where simplicity is prioritized over performance. Selection Sort minimizes the number of swaps compared to other quadratic algorithms like Bubble Sort, making it useful when write operations are expensive (e.g., in certain memory systems). However, for large datasets, more efficient algorithms like Quick Sort or Merge Sort are preferred.
Where to Use? (Real-Life Examples)
- Teaching Sorting: Selection Sort is used in classrooms to teach sorting concepts due to its straightforward mechanism of selecting the minimum element.
- Small Data Sorting: Embedded systems with limited resources use Selection Sort to sort small lists, such as configuration settings, where simplicity is key.
- Write-Constrained Environments: Applications with costly write operations, such as flash memory, use Selection Sort to minimize swaps.
- Prototyping: Developers use Selection Sort in prototypes to quickly sort small datasets for testing, leveraging its ease of implementation.
Explain Operations
- Find Minimum: This operation scans the unsorted portion of the array to find the index of the smallest element. It has a time complexity of O(n) for n elements in the unsorted portion.
- Swap: This operation exchanges the minimum element with the first element of the unsorted portion. It has a time complexity of O(1).
- Iteration: This operation represents one pass through the array, finding the minimum and performing one swap. It has a time complexity of O(n) due to the minimum search.
- Full Algorithm: This operation runs n-1 iterations to sort the entire array, where n is the array length. It has a time complexity of O(n²).
Java Implementation
The following Java code implements Selection Sort for an array of integers, designed to be clear and beginner-friendly.
public class SelectionSort {
// Selection Sort: Sorts an array in ascending order
public void selectionSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return; // No sorting needed for null or single-element arrays
}
int n = arr.length;
for (int i = 0; i < n - 1; i++) { // Iterate over unsorted portion
int minIndex = i; // Assume first element is minimum
for (int j = i + 1; j < n; j++) { // Search for minimum in unsorted portion
if (arr[j] < arr[minIndex]) {
minIndex = j; // Update minimum index
}
}
if (minIndex != i) { // Swap if a smaller element was found
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
}
How It Works
- Check Input:
- The
selectionSortmethod checks if the input array is null or has one or fewer elements. If so, it returns immediately, as no sorting is needed.
- The
- Outer Loop (Iterations):
- The outer loop iterates
n-1times, wherenis the array length, as each iteration places one element in its final position, growing the sorted portion. - For example, in an array of 5 elements, 4 iterations are sufficient to sort the array.
- The outer loop iterates
- Inner Loop (Find Minimum):
- The inner loop scans the unsorted portion (from index
i+1ton-1) to find the index of the smallest element, updatingminIndexwhen a smaller element is found. - For example, in the array
[5, 3, 8, 1, 9], the first iteration finds the minimum 1 at index 3.
- The inner loop scans the unsorted portion (from index
- Swap:
- If the minimum element is not already at index
i, the algorithm swaps the element atminIndexwith the element at indexiusing a temporary variable. - For example, swapping 5 (at index 0) with 1 (at index 3) results in
[1, 3, 8, 5, 9].
- If the minimum element is not already at index
- Result:
- After all iterations, the array is sorted in ascending order. For example,
[5, 3, 8, 1, 9]becomes[1, 3, 5, 8, 9].
- After all iterations, the array is sorted in ascending order. For example,
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Find Minimum | O(n) | O(1) |
| Swap | O(1) | O(1) |
| Iteration | O(n) | O(1) |
| Full Algorithm | O(n²) (best/average/worst) | O(1) |
Note:
- n is the number of elements in the array.
- The time complexity is O(n²) for all cases (best, average, worst) because the algorithm always performs n-1 iterations, each scanning up to n elements to find the minimum.
- Space complexity is O(1) as Selection Sort is in-place, using only a few variables for indexing and swapping.
Key Differences / Notes
- Selection Sort vs. Bubble Sort:
- Selection Sort minimizes swaps by performing one swap per iteration, while Bubble Sort may perform multiple swaps per pass.
- Both have O(n²) time complexity, but Selection Sort is less adaptive to nearly sorted data, as it always scans the entire unsorted portion.
- Selection Sort vs. Other Sorting Algorithms:
- Selection Sort is less efficient than Quick Sort (O(n log n) average) or Merge Sort (O(n log n)), which are preferred for large datasets.
- It is not stable, unlike Bubble Sort or Merge Sort, as it may swap equal elements out of order.
- Suitability for Data Structures:
- Selection Sort is most efficient for arrays due to O(1) access and swap operations.
- It is less practical for linked lists or other structures due to inefficient access patterns, which is why this implementation focuses on arrays.
- Fixed Swaps:
- Selection Sort performs at most n-1 swaps, making it advantageous when swaps are more costly than comparisons.
✅ Tip: Use Selection Sort for small arrays or when minimizing swaps is important, such as in systems where write operations are expensive. Test the implementation with edge cases like empty, single-element, or duplicate-filled arrays to ensure correctness.
⚠ Warning: Avoid using Selection Sort for large datasets due to its O(n²) time complexity, which is inefficient compared to algorithms like Quick Sort or Merge Sort. Always validate array bounds to prevent
ArrayIndexOutOfBoundsException.
Exercises
- Basic Selection Sort: Implement Selection Sort for an array of integers and test it with various inputs (e.g., unsorted, sorted, reversed, duplicates). Count the number of swaps performed.
- Descending Order: Modify the Selection Sort implementation to sort an array in descending order by selecting the maximum element instead. Test with different array sizes.
- Performance Analysis: Write a program that measures the execution time of Selection Sort for arrays of increasing sizes (e.g., 10, 100, 1000 elements). Compare performance across different input cases.
- String Array Sorting: Extend the Selection Sort implementation to sort an array of strings lexicographically. Test with strings of varying lengths and cases.
- Minimum Swap Count: Enhance the Selection Sort implementation to track and return the number of swaps performed. Test with nearly sorted and fully unsorted arrays.
Insertion Sort
Definition and Concepts
Insertion Sort is a simple sorting algorithm that builds a sorted array one element at a time by taking each element from the unsorted portion and inserting it into its correct position in the sorted portion. The algorithm maintains a sorted subarray at the beginning, which grows with each iteration as elements are inserted in their proper order. You can visualize Insertion Sort as sorting a hand of playing cards, where you pick one card at a time and place it in the correct position among the already-sorted cards. In Java, Insertion Sort is typically implemented on arrays due to their direct index-based access, which facilitates shifting elements during insertion. Insertion Sort is a stable algorithm, preserving the relative order of equal elements, and it is in-place, requiring minimal extra memory.
Key Points:
- Insertion Sort divides the array into a sorted and an unsorted portion.
- Each iteration takes an element from the unsorted portion and inserts it into the sorted portion.
- The algorithm is efficient for small or nearly sorted datasets due to its adaptive nature.
- It is easy to implement and understand, making it ideal for beginners.
Why Use It?
Insertion Sort is used for its simplicity and efficiency in sorting small datasets or nearly sorted arrays, where it performs well due to its adaptive nature. The algorithm requires minimal extra memory, as it sorts in-place, making it suitable for memory-constrained environments. Insertion Sort is particularly effective when the data is already partially sorted, as it can terminate early for each element once its position is found. It is also valuable for educational purposes, helping students understand sorting through its intuitive insertion process. For large datasets, however, more efficient algorithms like Quick Sort or Merge Sort are recommended.
Where to Use? (Real-Life Examples)
- Teaching Sorting Algorithms: Insertion Sort is used in classrooms to teach sorting concepts due to its intuitive approach of building a sorted portion incrementally.
- Small Data Sorting: Embedded systems with limited resources use Insertion Sort to sort small lists, such as sensor data, where simplicity and low memory usage are critical.
- Nearly Sorted Data: Applications like online leaderboards use Insertion Sort to maintain nearly sorted lists, as it efficiently handles small updates with few shifts.
- Incremental Data Processing: Real-time systems processing incoming data streams use Insertion Sort to insert new elements into a sorted list, such as in event logging.
Explain Operations
- Selection: This operation selects the next element from the unsorted portion to be inserted into the sorted portion. It has a time complexity of O(1).
- Comparison and Shift: This operation compares the selected element with elements in the sorted portion, shifting larger elements to the right until the correct position is found. It has a time complexity of O(n) in the worst case for n elements in the sorted portion.
- Insertion: This operation places the selected element in its correct position after shifting. It has a time complexity of O(1).
- Full Algorithm: This operation involves iterating through the array, performing selection, comparison, and insertion for each element. It has a time complexity of O(n²) in the worst and average cases.
Java Implementation
The following Java code implements Insertion Sort for an array of integers.
public class InsertionSort {
// Insertion Sort: Sorts an array in ascending order
public void insertionSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return; // No sorting needed for null or single-element arrays
}
int n = arr.length;
for (int i = 1; i < n; i++) { // Start from second element
int key = arr[i]; // Select element to insert
int j = i - 1; // Last index of sorted portion
while (j >= 0 && arr[j] > key) { // Shift larger elements
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key; // Insert key in correct position
}
}
}
How It Works
- Check Input:
- The
insertionSortmethod checks if the input array is null or has one or fewer elements. If so, it returns immediately, as no sorting is needed.
- The
- Outer Loop (Selection):
- The outer loop iterates from the second element (index 1) to the last element (index n-1), selecting each element (
key) from the unsorted portion. - For example, in the array
[5, 3, 8, 1, 9], the first iteration selectskey = 3.
- The outer loop iterates from the second element (index 1) to the last element (index n-1), selecting each element (
- Inner Loop (Comparison and Shift):
- The inner loop compares the
keywith elements in the sorted portion (from indexi-1backward to 0), shifting larger elements one position to the right. - For example, when inserting
3, the element5is shifted to index 1, resulting in[5, 5, 8, 1, 9].
- The inner loop compares the
- Insertion:
- After the inner loop finds the correct position (when
arr[j] <= keyorj < 0), thekeyis placed at indexj+1. - For example,
3is inserted at index 0, resulting in[3, 5, 8, 1, 9].
- After the inner loop finds the correct position (when
- Result:
- After all iterations, the array is sorted in ascending order. For example,
[5, 3, 8, 1, 9]becomes[1, 3, 5, 8, 9].
- After all iterations, the array is sorted in ascending order. For example,
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Selection | O(1) | O(1) |
| Comparison and Shift | O(n) | O(1) |
| Insertion | O(1) | O(1) |
| Full Algorithm | O(n) (best) | O(1) |
| Full Algorithm | O(n²) (average/worst) | O(1) |
Note:
- n is the number of elements in the array.
- Best case (O(n)) occurs when the array is already sorted, requiring minimal comparisons and no shifts.
- Average and worst cases (O(n²)) involve up to n iterations, each with up to n comparisons and shifts.
- Space complexity is O(1), as Insertion Sort is in-place, using only a few variables.
Key Differences / Notes
- Insertion Sort vs. Bubble Sort:
- Insertion Sort is more efficient for nearly sorted arrays, as it requires fewer shifts than Bubble Sort’s multiple swaps per pass.
- Both have O(n²) worst-case time complexity, but Insertion Sort is adaptive, performing better on partially sorted data.
- Insertion Sort vs. Selection Sort:
- Insertion Sort is stable, preserving the order of equal elements, while Selection Sort is not.
- Insertion Sort performs better for nearly sorted arrays, while Selection Sort performs a fixed number of comparisons regardless of input order.
- Insertion Sort vs. Other Sorting Algorithms:
- Insertion Sort is less efficient than Quick Sort (O(n log n) average) or Merge Sort (O(n log n)) for large datasets but excels in small or nearly sorted cases.
- It is in-place, unlike Merge Sort, which requires O(n) extra space.
- Suitability for Data Structures:
- Insertion Sort is most efficient for arrays due to O(1) access and shift operations. It is less practical for linked lists (though feasible) due to traversal costs.
✅ Tip: Use Insertion Sort for small arrays or nearly sorted data to leverage its adaptive nature and minimal memory usage. Test with edge cases like empty, single-element, or already sorted arrays to ensure robustness.
⚠ Warning: Avoid using Insertion Sort for large datasets due to its O(n²) time complexity in average and worst cases, which is inefficient compared to Quick Sort or Merge Sort. Always validate array bounds to prevent
ArrayIndexOutOfBoundsException.
Exercises
- Basic Insertion Sort: Implement Insertion Sort for an array of integers and test it with various inputs (e.g., unsorted, sorted, reversed, duplicates). Count the number of shifts performed.
- Descending Order: Modify the Insertion Sort implementation to sort an array in descending order by adjusting the comparison logic. Test with different array sizes.
- Performance Analysis: Write a program that measures the execution time of Insertion Sort for arrays of increasing sizes (e.g., 10, 100, 1000 elements). Compare with Bubble Sort and Selection Sort.
- Object Sorting: Extend Insertion Sort to sort an array of objects (e.g.,
Studentobjects with agradefield) based on a custom comparator. Test with a sample dataset. - Nearly Sorted Arrays: Implement Insertion Sort and test its performance on nearly sorted arrays (e.g., one element out of place). Analyze the number of comparisons and shifts compared to unsorted inputs.
Merge Sort
Definition and Concepts
Merge Sort is a divide-and-conquer sorting algorithm that recursively divides an array into two halves, sorts each half, and then merges the sorted halves to produce a fully sorted array. The algorithm relies on the principle that it is easier to sort smaller subarrays and combine them efficiently. You can visualize Merge Sort as splitting a deck of cards into smaller stacks, sorting each stack, and then combining them in order. In Java, Merge Sort is typically implemented on arrays, using recursion to divide the array and an auxiliary array to merge the sorted subarrays. Merge Sort is a stable algorithm, preserving the relative order of equal elements, but it is not in-place, as it requires additional memory for merging.
Key Points:
- Merge Sort divides the array into two equal halves until each subarray has one element (which is inherently sorted).
- It merges sorted subarrays by comparing elements and placing them in the correct order.
- The algorithm is efficient for large datasets due to its consistent O(n log n) time complexity.
- It requires extra space for merging, unlike in-place algorithms like Bubble Sort or Insertion Sort.
Why Use It?
Merge Sort is used for its consistent and efficient O(n log n) time complexity, making it suitable for sorting large datasets where performance is critical. The algorithm is stable, ensuring that equal elements maintain their relative order, which is important in applications like database sorting. Merge Sort is particularly effective for linked lists, as it avoids random access, and for external sorting (sorting data too large to fit in memory). Its divide-and-conquer approach makes it easy to parallelize, improving performance on multi-core systems. However, its need for extra space makes it less suitable for memory-constrained environments.
Where to Use? (Real-Life Examples)
- Database Sorting: Database systems use Merge Sort to sort large datasets, such as query results, due to its stability and efficiency.
- External Sorting: File systems use Merge Sort for sorting large files on disk, as it can handle data in chunks, minimizing memory usage.
- Linked List Sorting: Applications with linked list data structures use Merge Sort to sort elements efficiently without requiring random access.
- Parallel Processing: Parallel computing frameworks use Merge Sort in distributed systems, as its divide-and-conquer nature allows tasks to be split across processors.
Explain Operations
- Divide: This operation splits the array into two halves recursively until each subarray has one element. It has a time complexity of O(1) per division.
- Merge: This operation combines two sorted subarrays into a single sorted array by comparing elements and placing them in order. It has a time complexity of O(n) for n elements in the subarrays.
- Recursive Sort: This operation recursively sorts the two halves of the array. It contributes to the overall O(n log n) time complexity due to log n levels of recursion.
- Full Algorithm: This operation combines division, recursive sorting, and merging to sort the entire array. It has a time complexity of O(n log n) in all cases.
Java Implementation
The following Java code implements Merge Sort for an array of integers.
public class MergeSort {
// Merge Sort: Sorts an array in ascending order
public void mergeSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return; // No sorting needed for null or single-element arrays
}
int[] temp = new int[arr.length]; // Auxiliary array for merging
mergeSortHelper(arr, 0, arr.length - 1, temp);
}
// Helper method: Recursively divides and sorts the array
private void mergeSortHelper(int[] arr, int left, int right, int[] temp) {
if (left < right) { // Base case: subarray has more than one element
int mid = left + (right - left) / 2; // Find midpoint
mergeSortHelper(arr, left, mid, temp); // Sort left half
mergeSortHelper(arr, mid + 1, right, temp); // Sort right half
merge(arr, left, mid, right, temp); // Merge sorted halves
}
}
// Merge: Combines two sorted subarrays into a single sorted array
private void merge(int[] arr, int left, int mid, int right, int[] temp) {
// Copy elements to temporary array
for (int i = left; i <= right; i++) {
temp[i] = arr[i];
}
int i = left; // Index for left subarray
int j = mid + 1; // Index for right subarray
int k = left; // Index for merged array
while (i <= mid && j <= right) { // Compare and merge
if (temp[i] <= temp[j]) { // Use <= for stability
arr[k++] = temp[i++];
} else {
arr[k++] = temp[j++];
}
}
while (i <= mid) { // Copy remaining left subarray elements
arr[k++] = temp[i++];
}
// No need to copy right subarray, as it’s already processed
}
}
How It Works
- Check Input:
- The
mergeSortmethod checks if the input array is null or has one or fewer elements. If so, it returns immediately, as no sorting is needed.
- The
- Initialize Temporary Array:
- An auxiliary array
tempis created to assist with merging, with the same size as the input array.
- An auxiliary array
- Recursive Division (mergeSortHelper):
- The
mergeSortHelpermethod recursively divides the array into two halves by calculating the midpoint (mid = left + (right - left) / 2). - It calls itself on the left half (
lefttomid) and right half (mid + 1toright). - For example, for
[5, 3, 8, 1, 9], it divides into[5, 3]and[8, 1, 9], then further into[5],[3],[8],[1, 9].
- The
- Merge:
- The
mergemethod copies the subarray fromlefttorightintotemp, then merges the two sorted halves back into the original array. - It compares elements from the left subarray (
temp[i]) and right subarray (temp[j]), placing the smaller (or equal, for stability) element intoarr[k]. - For example, merging
[3, 5]and[1, 8, 9]results in[1, 3, 5, 8, 9].
- The
- Result:
- After all recursive calls and merges, the array is sorted in ascending order. For example,
[5, 3, 8, 1, 9]becomes[1, 3, 5, 8, 9].
- After all recursive calls and merges, the array is sorted in ascending order. For example,
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Divide | O(1) | O(1) |
| Merge | O(n) | O(n) |
| Recursive Sort | O(n log n) | O(n) |
| Full Algorithm | O(n log n) (all cases) | O(n) |
Note:
- n is the number of elements in the array.
- Time complexity is O(n log n) in best, average, and worst cases, as the array is divided log n times, and each merge takes O(n) time.
- Space complexity is O(n) due to the auxiliary array used for merging.
Key Differences / Notes
- Merge Sort vs. Bubble/Selection/Insertion Sort:
- Merge Sort has O(n log n) time complexity, making it more efficient than Bubble Sort, Selection Sort, and Insertion Sort (all O(n²)) for large datasets.
- It is stable, like Insertion Sort, but unlike Selection Sort.
- It requires O(n) extra space, unlike the in-place Bubble, Selection, and Insertion Sorts.
- Merge Sort vs. Quick Sort:
- Merge Sort is stable and has consistent O(n log n) time complexity, while Quick Sort is not stable and has O(n²) worst-case but O(n log n) average.
- Quick Sort is in-place, while Merge Sort requires extra space.
- Suitability for Data Structures:
- Merge Sort is efficient for arrays and linked lists, as it avoids random access in the merge step, unlike Selection Sort.
- It is particularly suited for external sorting and parallel processing due to its divide-and-conquer nature.
- Stability:
- The use of
<=in the merge step ensures stability, preserving the relative order of equal elements.
- The use of
✅ Tip: Use Merge Sort for large datasets or when stability is required, as its O(n log n) time complexity ensures consistent performance. For linked lists, Merge Sort is especially efficient, as it avoids random access.
⚠ Warning: Be mindful of Merge Sort’s O(n) space complexity, which may be a limitation in memory-constrained environments. Always validate array bounds and ensure the auxiliary array is properly initialized to prevent errors.
Exercises
- Basic Merge Sort: Implement Merge Sort for an array of integers and test it with various inputs (e.g., unsorted, sorted, reversed, duplicates). Verify the sorted output.
- Descending Order: Modify the Merge Sort implementation to sort an array in descending order by adjusting the merge comparison logic. Test with different array sizes.
- Performance Analysis: Write a program that measures the execution time of Merge Sort for arrays of increasing sizes (e.g., 10, 100, 1000 elements). Compare with Insertion Sort and Selection Sort.
- Object Sorting: Extend Merge Sort to sort an array of objects (e.g.,
Studentobjects with agradefield) based on a custom comparator. Test with a sample dataset. - Space Optimization: Implement an in-place Merge Sort variant (if feasible) and compare its performance and space usage with the standard implementation. Test with various inputs.
Searching Algorithms
What are Searching Algorithms?
Searching algorithms are methods for finding a specific element or its position within a data structure, such as an array or list. They are the building blocks of efficient data retrieval, used in applications from simple lookups to complex database queries. The algorithms covered in this chapter are Linear Search and Binary Search, each with distinct approaches to locating elements.
Why Learn Searching Algorithms?
- Efficiency: Choosing the right searching algorithm can drastically reduce the time needed to find data.
- Problem Solving: Many real-world tasks, like finding a contact or a record, rely on searching techniques.
- Foundation for Advanced Algorithms: Understanding searching is crucial for mastering more complex algorithms.
- Interview Readiness: Searching algorithms are a common topic in technical interviews.
📖 What You Will Learn in This Chapter
This chapter introduces you to the most commonly used searching algorithms, their uses, advantages, disadvantages, and Java implementations.
1. Linear Search
- What is Linear Search and how it works.
- Checking elements one by one in sequence.
- Applicability to unsorted data.
- Real-life example: Finding a name in an unsorted list.
2. Binary Search
- How Binary Search divides the search space.
- Requiring sorted data for efficiency.
- Iterative and recursive approaches.
- Real-life example: Looking up a word in a dictionary.
🛠 How We Will Learn
For each searching algorithm, we will cover:
- Definition – What it is and how it works.
- Why – Advantages and limitations of the algorithm.
- Where to Use – Real-life scenarios where the algorithm is effective.
- Java Implementation – With step-by-step explanations.
- Complexity Analysis – Understanding performance trade-offs.
Linear Search
Definition and Concepts
Linear Search, also known as sequential search, is a simple searching algorithm that checks each element in a list sequentially until the target element is found or the list is exhausted. The algorithm does not require the data to be sorted, making it versatile but inefficient for large datasets. You can visualize Linear Search as looking through a stack of papers one by one to find a specific document. In Java, Linear Search is typically implemented on arrays due to their direct index-based access, returning the index of the target or -1 if not found.
Key Points:
- Linear Search examines elements one at a time in order.
- It works on both sorted and unsorted data.
- The algorithm is simple to implement but has a linear time complexity.
- It returns the first occurrence of the target element.
Why Use It?
Linear Search is used for its simplicity and applicability to small or unsorted datasets, where the overhead of sorting for other algorithms is unnecessary. It is ideal for beginners to understand searching concepts and is useful when the dataset is small or when searching for the first occurrence of a value. However, for large datasets, more efficient algorithms like Binary Search are preferred.
Where to Use? (Real-Life Examples)
- Small List Search: Linear Search is used in applications like finding a contact in a short, unsorted phone list due to its simplicity.
- Unsorted Data: Inventory systems use Linear Search to locate items in unsorted lists, such as stock entries.
- Teaching Tool: Linear Search is used in classrooms to introduce searching concepts to beginners.
- First Occurrence: Applications like log analysis use Linear Search to find the first instance of an error in a sequence.
Explain Operations
- Comparison: Compares the current element with the target to check for a match, with a time complexity of O(1).
- Traversal: Moves to the next element in the array, incrementing the index, with a time complexity of O(n) for n elements in the worst case.
- Full Search: Executes the complete search, checking all elements until the target is found or the end is reached, with a time complexity of O(n).
Java Implementation
The following Java code implements Linear Search for an array of integers, designed to be clear and beginner-friendly.
public class LinearSearch {
// Linear Search: Finds the target by checking each element
public int linearSearch(int[] arr, int target) {
if (arr == null) {
return -1; // Return -1 for null array
}
for (int i = 0; i < arr.length; i++) { // Check each element
if (arr[i] == target) {
return i; // Return index of target
}
}
return -1; // Target not found
}
}
How It Works
- Check Input:
- The
linearSearchmethod checks if the input array is null. If so, it returns -1, as no search is possible.
- The
- Traversal and Comparison:
- The method iterates through the array from index 0 to the end, comparing each element with the target.
- If a match is found, the method returns the index of the target.
- For example, in
[1, 3, 5, 8, 9]with target 5, it checks indices 0, 1, and 2, returning 2.
- Result:
- If the target is not found after checking all elements, the method returns -1.
- For example, searching for 7 in
[1, 3, 5, 8, 9]returns -1.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Comparison | O(1) | O(1) |
| Traversal | O(n) | O(1) |
| Full Search | O(1) (best) | O(1) |
| Full Search | O(n) (average/worst) | O(1) |
Note:
- n is the number of elements.
- Best case (O(1)) occurs when the target is at the first index.
- Average and worst cases (O(n)) involve checking up to n elements.
- Space complexity is O(1), as no extra space is used.
Key Differences / Notes
- Linear Search vs. Binary Search:
- Linear Search works on unsorted data, while Binary Search requires sorted data.
- Linear Search has O(n) time complexity, while Binary Search is O(log n), making it faster for large datasets.
- Simplicity:
- Linear Search is easier to implement and understand than Binary Search, ideal for beginners.
- Use Cases:
- Linear Search is best for small or unsorted datasets or when finding the first occurrence is needed.
- Limitations:
- Linear Search is inefficient for large datasets due to its linear time complexity.
✅ Tip: Use Linear Search for small or unsorted datasets due to its simplicity and lack of preprocessing requirements. Test with edge cases like empty arrays or missing targets to ensure robustness.
⚠ Warning: Avoid Linear Search for large datasets, as its O(n) time complexity can be slow compared to Binary Search’s O(log n). Always check for null arrays to prevent
NullPointerException.
Exercises
- Basic Linear Search: Implement Linear Search and test it with arrays of different sizes and targets (e.g., present, absent, first element). Count the number of comparisons.
- Last Occurrence: Modify Linear Search to find the last occurrence of a target in an array with duplicates. Test with
[1, 3, 3, 5, 8]and target 3. - Performance Analysis: Measure the execution time of Linear Search for arrays of increasing sizes (e.g., 10, 100, 1000 elements). Analyze best and worst cases.
- Object Search: Extend Linear Search to find an object in an array (e.g.,
Studentwithid). Test with a sample dataset. - Multiple Targets: Implement Linear Search to return all indices where a target appears in an array. Test with duplicates like
[1, 3, 3, 5, 3].
Binary Search
Definition and Concepts
Binary Search is an efficient searching algorithm that finds a target element in a sorted array by repeatedly dividing the search space in half. The algorithm compares the target with the middle element and eliminates half of the remaining elements based on the comparison, continuing until the target is found or the search space is empty. You can visualize Binary Search as looking up a word in a dictionary by opening to the middle and narrowing the search to one half. In Java, Binary Search is typically implemented on sorted arrays, returning the index of the target or -1 if not found.
Key Points:
- Binary Search requires the input array to be sorted.
- It divides the search space in half with each step, making it highly efficient.
- The algorithm can be implemented iteratively or recursively.
- It returns the index of the first occurrence of the target in case of duplicates.
Why Use It?
Binary Search is used for its efficiency, with an O(log n) time complexity, making it ideal for large, sorted datasets. It is suitable for applications where data is pre-sorted, such as database indexes or lookup tables, and is a key concept for students learning divide-and-conquer strategies. However, it requires sorted data, which may necessitate preprocessing, and is less versatile than Linear Search for unsorted data.
Where to Use? (Real-Life Examples)
- Database Indexes: Binary Search is used in databases to quickly locate records in sorted indexes, like customer IDs.
- Dictionary Lookups: Dictionary applications use Binary Search to find words in a sorted list efficiently.
- Sorted Data Search: Search engines use Binary Search to locate items in sorted datasets, such as ranked results.
- Algorithm Design: Binary Search is used in algorithms like finding the square root of a number by narrowing ranges.
Explain Operations
- Comparison: Compares the middle element with the target to determine the search direction, with a time complexity of O(1).
- Division: Updates the search boundaries (left or right) to half the current range, with a time complexity of O(1).
- Full Search: Executes the algorithm, halving the search space until the target is found or the space is empty, with a time complexity of O(log n).
Java Implementation
The following Java code implements Binary Search (iterative version) for an array of integers.
public class BinarySearch {
// Binary Search: Finds the target in a sorted array
public int binarySearch(int[] arr, int target) {
if (arr == null) {
return -1; // Return -1 for null array
}
int left = 0, right = arr.length - 1; // Initialize boundaries
while (left <= right) { // Continue until search space is valid
int mid = left + (right - left) / 2; // Calculate midpoint
if (arr[mid] == target) {
return mid; // Return index of target
} else if (arr[mid] < target) {
left = mid + 1; // Search right half
} else {
right = mid - 1; // Search left half
}
}
return -1; // Target not found
}
}
How It Works
- Check Input:
- The
binarySearchmethod checks if the input array is null. If so, it returns -1, as no search is possible.
- The
- Initialize Boundaries:
- Sets
leftto 0 andrightto the last index of the array, defining the initial search space.
- Sets
- Division and Comparison:
- Calculates the midpoint (
mid) and compares the element atmidwith the target. - If equal, returns the index. If the target is greater, searches the right half (
left = mid + 1). If smaller, searches the left half (right = mid - 1). - For example, in
[1, 3, 5, 8, 9]with target 5, it checks index 2 (midpoint) and returns 2.
- Calculates the midpoint (
- Result:
- If the search space is empty (
left > right), returns -1, indicating the target is not found. - For example, searching for 7 returns -1.
- If the search space is empty (
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Comparison | O(1) | O(1) |
| Division | O(1) | O(1) |
| Full Search | O(1) (best) | O(1) |
| Full Search | O(log n) (average/worst) | O(1) |
Note:
- n is the number of elements.
- Best case (O(1)) occurs when the target is at the midpoint of the first check.
- Average and worst cases (O(log n)) involve log n divisions.
- Space complexity is O(1) for the iterative version, as no extra space is used.
Key Differences / Notes
- Binary Search vs. Linear Search:
- Binary Search is O(log n), much faster than Linear Search’s O(n) for large datasets, but requires sorted data.
- Binary Search is more complex to implement due to its divide-and-conquer logic.
- Iterative vs. Recursive:
- The iterative version shown is more space-efficient (O(1)) than the recursive version (O(log n) due to call stack).
- Use Cases:
- Binary Search is ideal for large, sorted datasets, while Linear Search is better for unsorted or small data.
- Limitations:
- Binary Search fails on unsorted data, requiring preprocessing to sort the array.
✅ Tip: Use Binary Search for large, sorted datasets to leverage its O(log n) efficiency. Test with sorted arrays and edge cases to ensure correctness.
⚠ Warning: Binary Search requires sorted data; applying it to unsorted data produces incorrect results. Use
left + (right - left) / 2for midpoint calculation to avoid integer overflow.
Exercises
- Basic Binary Search: Implement Binary Search and test it with sorted arrays of different sizes and targets (e.g., present, absent, middle element).
- Recursive Implementation: Implement a recursive version of Binary Search and compare its performance with the iterative version.
- First and Last Occurrence: Modify Binary Search to find the first and last occurrences of a target in an array with duplicates (e.g.,
[1, 3, 3, 3, 5]for target 3). - Performance Analysis: Measure the execution time of Binary Search vs. Linear Search for large sorted arrays (e.g., 1000, 10000 elements).
- Object Search: Extend Binary Search to find an object in a sorted array (e.g.,
Studentwithid). Test with a sample dataset.
Recursion Problem Solving with DSA
🛠 What you will learn
This section outlines the format used for each DSA problem statement, designed to guide students through solving exercise problems in a clear, structured learning process. Each component serves a specific purpose to enhance understanding and practical application:
- Problem Statement: Clearly defines the task, including inputs, outputs, constraints, and an example. A real-world analogy makes the problem relatable, aligning with the engaging tone of the "Core Data Structures" summary, ensuring students grasp the problem’s context.
- Pseudocode: Provides a high-level, language-agnostic outline of the solution using standardized syntax (e.g.,
FUNCTION,IF,SET,RETURN). This helps students understand the logic before diving into code, bridging theory and implementation. - Algorithm Steps: Breaks down the pseudocode into detailed, actionable steps, connecting theoretical logic to practical coding. This ensures students can follow the implementation process systematically.
- Java Implementation: Offers a complete, commented Java code solution that students can study and run. It includes a
mainmethod with at least four test cases (e.g., target present, absent, middle element, and edge cases like duplicates or single elements) to verify correctness and explore different scenarios, maintaining a beginner-friendly approach. - Output: Shows the expected results from the test cases, including indices, comparisons, and execution times (where applicable), helping students verify their code’s correctness and understand the algorithm’s behavior.
- How It Works: Traces the algorithm’s execution with a detailed example, showing step-by-step how the search narrows down the range. This reinforces understanding by illustrating the algorithm in action.
- Complexity Analysis Table: Summarizes time and space complexity for best, average, and worst cases, teaching students to evaluate efficiency and compare trade-offs across implementations.
- Tip or Warning Box: Provides practical advice (e.g., when to use DSA) and highlights pitfalls (e.g., ensuring the array is sorted), guiding students toward best practices and common error avoidance.
Each exercise includes test cases to verify correctness and performance, aligning with a learning platform featuring embedded compilers for hands-on practice and visualizations to illustrate the halving process of DSA.
Binary Search Recursion
Problem Statement
Write a Java program that implements a recursive binary search algorithm to find the index of a target element in a sorted array of integers. The program should return the index of the target if found, or -1 if the target does not exist in the array. Test the program with arrays containing both existing and non-existing elements. You can visualize this as searching for a word in a dictionary by repeatedly opening to the middle page and narrowing the search to one half of the book.
Input: A sorted array of integers and a target integer (e.g., arr = [1, 3, 5, 8, 9], target = 5).
Output: An integer representing the index of the target in the array, or -1 if the target is not found (e.g., 2 for target 5).
Constraints:
- The array length is between 0 and 10^5.
- Elements are integers between -10^9 and 10^9.
- The array is sorted in ascending order.
- The target may or may not exist in the array. Example:
- Input:
arr = [1, 3, 5, 8, 9], target = 5 - Output: 2
- Explanation: The target 5 is found at index 2 in the sorted array.
- Input:
arr = [1, 3, 5, 8, 9], target = 7 - Output: -1
- Explanation: The target 7 is not present in the array.
Pseudocode
FUNCTION binarySearch(arr, target, left, right)
IF arr is null OR left > right THEN
RETURN -1
ENDIF
SET mid to (left + right) / 2
IF arr[mid] equals target THEN
RETURN mid
ELSE IF arr[mid] < target THEN
RETURN binarySearch(arr, target, mid + 1, right)
ELSE
RETURN binarySearch(arr, target, left, mid - 1)
ENDIF
ENDFUNCTION
FUNCTION mainBinarySearch(arr, target)
RETURN binarySearch(arr, target, 0, length of arr - 1)
ENDFUNCTION
Algorithm Steps
- Check if the input array is null or if the left index exceeds the right index, indicating an empty search space. If so, return -1.
- Define a recursive helper function that takes the array, target, and left and right indices as parameters.
- In the recursive function, calculate the middle index of the current search space.
- Compare the element at the middle index with the target. If they are equal, return the middle index.
- If the middle element is less than the target, recursively search the right half of the array (from mid + 1 to right).
- If the middle element is greater than the target, recursively search the left half of the array (from left to mid - 1).
- In the main function, initiate the recursion by calling the helper function with the initial left index as 0 and the right index as the array length minus 1.
- Return the result from the recursive function, which is either the target’s index or -1 if not found.
Java Implementation
public class BinarySearchRecursion {
// Main method to initiate recursive binary search
public int binarySearch(int[] arr, int target) {
// Check for null array
if (arr == null) {
return -1;
}
// Call recursive helper with initial boundaries
return binarySearchHelper(arr, target, 0, arr.length - 1);
}
// Helper method for recursive binary search
private int binarySearchHelper(int[] arr, int target, int left, int right) {
// Base case: empty search space or invalid indices
if (left > right) {
return -1;
}
// Calculate middle index
int mid = left + (right - left) / 2;
// If target found at mid, return index
if (arr[mid] == target) {
return mid;
}
// If target is greater, search right half
else if (arr[mid] < target) {
return binarySearchHelper(arr, target, mid + 1, right);
}
// If target is smaller, search left half
else {
return binarySearchHelper(arr, target, left, mid - 1);
}
}
}
Output
For the input arr = [1, 3, 5, 8, 9], target = 5, the program outputs:
2
Explanation: The target 5 is found at index 2 in the sorted array.
For the input arr = [1, 3, 5, 8, 9], target = 7, the program outputs:
-1
Explanation: The target 7 is not present in the array.
For an empty array arr = [], the program outputs:
-1
Explanation: An empty array contains no elements, so the target cannot be found.
How It Works
- Step 1: The
binarySearchmethod checks if the array is null. For[1, 3, 5, 8, 9], it is valid, so it calls the helper function withleft = 0andright = 4. - Step 2: In
binarySearchHelper:- First call:
left = 0,right = 4,mid = 2,arr[2] = 5. Since 5 equals the target, return 2. - For target 7: First call:
mid = 2,arr[2] = 5 < 7, recurse onleft = 3,right = 4. - Second call:
mid = 3,arr[3] = 8 > 7, recurse onleft = 3,right = 2. - Third call:
left > right, return -1.
- First call:
- Example Trace: For
[1, 3, 5, 8, 9]with target 5:- Check
mid = 2,arr[2] = 5, matches target, return 2. - For target 7: Check
mid = 2,arr[2] = 5 < 7, recurse on right half[8, 9]. Checkmid = 3,arr[3] = 8 > 7, recurse on left half (empty). Return -1.
- Check
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Comparison | O(1) | O(1) |
| Recursive Call | O(log n) | O(log n) |
| Full Algorithm | O(log n) | O(log n) |
Note:
- n is the size of the input array.
- Time complexity: O(log n), as the algorithm halves the search space with each recursive call.
- Space complexity: O(log n) due to the recursive call stack, which grows logarithmically with the array size.
- Best case: O(1) if the target is at the midpoint of the first call.
- Average and worst cases: O(log n) for up to log n recursive calls.
✅ Tip: Use recursive binary search for sorted arrays to leverage its O(log n) efficiency, and test with both existing and non-existing targets to ensure correctness. Use the formula
left + (right - left) / 2for midpoint calculation to avoid integer overflow.
⚠ Warning: Binary search requires a sorted array; applying it to an unsorted array will produce incorrect results. Ensure proper bounds checking to avoid infinite recursion or
ArrayIndexOutOfBoundsException.
Factorial with Memoization
Problem Statement
Write a Java program that implements a recursive factorial function using memoization with a HashMap to cache previously computed results. The program should compute the factorial of a non-negative integer and compare its performance with a standard recursive factorial implementation for large inputs. Test the program with various inputs, including edge cases like 0 and 1. You can visualize this as calculating the number of ways to arrange books on a shelf, where memoization saves time by reusing previously calculated arrangements.
Input: A non-negative integer n (e.g., n = 5).
Output: A long integer representing the factorial of n (e.g., 120 for n = 5).
Constraints:
- 0 ≤ n ≤ 20 (to avoid overflow with
long). - The input is a non-negative integer. Example:
- Input:
n = 5 - Output: 120
- Explanation: The factorial of 5 is 5 * 4 * 3 * 2 * 1 = 120.
- Input:
n = 0 - Output: 1
- Explanation: The factorial of 0 is defined as 1.
Pseudocode
FUNCTION factorialWithMemo(n, memo)
IF n < 0 THEN
RETURN -1
ENDIF
IF n equals 0 OR n equals 1 THEN
RETURN 1
ENDIF
IF n exists in memo THEN
RETURN memo[n]
ENDIF
SET result to n * factorialWithMemo(n - 1, memo)
SET memo[n] to result
RETURN result
ENDFUNCTION
FUNCTION mainFactorial(n)
CREATE empty HashMap memo
RETURN factorialWithMemo(n, memo)
ENDFUNCTION
Algorithm Steps
- Check if the input
nis negative. If so, return -1, as factorial is undefined for negative numbers. - Define a recursive helper function that takes the input
nand a HashMap for memoization as parameters. - In the recursive function, implement the base cases: if
nis 0 or 1, return 1, as the factorial of 0 and 1 is 1. - Check if the factorial of
nis already in the HashMap. If so, return the cached result. - For the recursive case, compute the factorial by multiplying
nwith the result of the recursive call forn - 1. - Store the computed result in the HashMap with
nas the key. - In the main function, create an empty HashMap and call the helper function with
nand the HashMap. - Return the final factorial result.
Java Implementation
import java.util.HashMap;
public class FactorialWithMemoization {
// Computes factorial of n using memoization with HashMap
public long factorial(int n) {
// Check for invalid input
if (n < 0) {
return -1;
}
// Create HashMap for memoization
HashMap<Integer, Long> memo = new HashMap<>();
// Call recursive helper function
return factorialHelper(n, memo);
}
// Helper function for recursive factorial with memoization
private long factorialHelper(int n, HashMap<Integer, Long> memo) {
// Base case: factorial of 0 or 1 is 1
if (n == 0 || n == 1) {
return 1;
}
// Check if result is in memo
if (memo.containsKey(n)) {
return memo.get(n);
}
// Recursive case: n * factorial(n-1)
long result = n * factorialHelper(n - 1, memo);
// Cache result in memo
memo.put(n, result);
return result;
}
// Standard recursive factorial for comparison
public long standardFactorial(int n) {
// Check for invalid input
if (n < 0) {
return -1;
}
// Base case: factorial of 0 or 1 is 1
if (n == 0 || n == 1) {
return 1;
}
// Recursive case: n * factorial(n-1)
return n * standardFactorial(n - 1);
}
}
Output
For the input n = 5, the factorial method outputs:
120
Explanation: The factorial of 5 is computed as 5 * 4 * 3 * 2 * 1 = 120.
For the input n = 0, the factorial method outputs:
1
Explanation: The factorial of 0 is defined as 1.
For the input n = 15, the factorial method outputs:
1307674368000
Explanation: The factorial of 15 is computed as 15 * 14 * ... * 1 = 1307674368000.
How It Works
- Step 1: The
factorialmethod checks ifnis negative. Forn = 5, it is valid, so it creates a HashMap and callsfactorialHelper. - Step 2: In
factorialHelper:- For
n = 5: Not in memo, compute 5 *factorialHelper(4). - For
n = 4: Not in memo, compute 4 *factorialHelper(3). - For
n = 3: Not in memo, compute 3 *factorialHelper(2). - For
n = 2: Not in memo, compute 2 *factorialHelper(1). - For
n = 1: Base case, return 1. - Unwind: Store 2! = 2 in memo, 3! = 6 in memo, 4! = 24 in memo, 5! = 120 in memo, return 120.
- For
- Step 3: For subsequent calls with cached values (e.g., in a performance test), the memoized function retrieves results directly from the HashMap, reducing computation.
- Example Trace: For
n = 5, the algorithm computes: 5 * (4 * (3 * (2 * (1)))) = 5 * 24 = 120, caching each intermediate result. Forn = 0, it returns 1 immediately. - Performance Comparison: The memoized version is faster for repeated calls or large
nwithin the same HashMap, as it avoids recomputing factorials. For example, computing 15! takes O(n) time without memoization but O(1) for cached values with memoization.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Comparison | O(1) | O(1) |
| HashMap Operations | O(1) (average) | O(n) |
| Full Algorithm (Memoized) | O(n) (first call) | O(n) |
| Full Algorithm (Memoized) | O(1) (cached) | O(n) |
| Full Algorithm (Standard) | O(n) | O(n) |
Note:
- n is the input integer.
- Time complexity (memoized): O(n) for the first call to compute and cache results up to n; O(1) for subsequent calls if cached.
- Time complexity (standard): O(n) for all calls, as it recomputes each step.
- Space complexity: O(n) for both, due to the recursive call stack and HashMap (for memoized version).
- Best case (memoized): O(1) if the result is cached; worst case is O(n) for initial computation.
✅ Tip: Use memoization to optimize recursive functions like factorial for repeated calls or large inputs, and test with edge cases like 0, 1, and large values (within constraints) to verify correctness.
⚠ Warning: Be cautious with inputs larger than 20, as factorial results may exceed the
longdata type’s capacity, causing overflow. Ensure the HashMap is properly initialized to avoidNullPointerException.
Recursive Sum of Array
Problem Statement
Write a Java program that uses recursion to compute the sum of all elements in an array of integers. The program should take an array as input and return the total sum of its elements. Test the program with arrays of different sizes, including empty arrays and single-element arrays. You can visualize this as calculating the total score of a student’s test results stored in a list, where you add each score one by one recursively.
Input: An array of integers (e.g., arr = [1, 2, 3, 4, 5]).
Output: An integer representing the sum of all elements in the array (e.g., 15 for [1, 2, 3, 4, 5]).
Constraints:
- The array length is between 0 and 10^5.
- Elements are integers between -10^9 and 10^9.
- The array may be empty. Example:
- Input:
arr = [1, 2, 3, 4, 5] - Output: 15
- Explanation: The sum is calculated as 1 + 2 + 3 + 4 + 5 = 15.
Pseudocode
1. If the array is null or empty, return 0.
2. Define a recursive function sumArray(arr, index):
a. Base case: If index equals array length, return 0.
b. Recursive case: Return arr[index] + sumArray(arr, index + 1).
3. Call sumArray(arr, 0) to start recursion from the first element.
4. Return the result.
Algorithm Steps
- Check if the input array is null or empty. If so, return 0, as the sum of no elements is zero.
- Define a recursive helper function that takes the array and the current index as parameters.
- In the recursive function, implement the base case: if the index equals the array length, return 0, as no more elements remain to sum.
- For the recursive case, add the element at the current index to the result of the recursive call for the next index.
- Initiate the recursion by calling the helper function with the initial index set to 0.
- Return the final sum computed by the recursive function.
Java Implementation
public class RecursiveArraySum {
// Computes the sum of array elements using recursion
public int sumArray(int[] arr) {
// Check for null or empty array
if (arr == null || arr.length == 0) {
return 0;
}
// Call recursive helper function starting at index 0
return sumArrayHelper(arr, 0);
}
// Helper function for recursive sum
private int sumArrayHelper(int[] arr, int index) {
// Base case: if index reaches array length, return 0
if (index == arr.length) {
return 0;
}
// Recursive case: add current element to sum of remaining elements
return arr[index] + sumArrayHelper(arr, index + 1);
}
}
Output
For the input array [1, 2, 3, 4, 5], the program outputs:
15
Explanation: The sum is computed as 1 + 2 + 3 + 4 + 5 = 15.
For an empty array [], the program outputs:
0
Explanation: The sum of an empty array is 0.
How It Works
- Step 1: The
sumArraymethod checks if the input array is null or empty. For[1, 2, 3, 4, 5], it is valid, so it calls the helper function with index 0. - Step 2: The
sumArrayHelpermethod implements recursion:- At index 0:
arr[0] = 1, recursive call for index 1. - At index 1:
arr[1] = 2, recursive call for index 2. - At index 2:
arr[2] = 3, recursive call for index 3. - At index 3:
arr[3] = 4, recursive call for index 4. - At index 4:
arr[4] = 5, recursive call for index 5. - At index 5: Base case reached (index equals length), return 0.
- At index 0:
- Step 3: The recursion unwinds: 5 + (4 + (3 + (2 + (1 + 0)))) = 15.
- Example Trace: For
[1, 2, 3, 4, 5], the algorithm recursively adds:1 + sum([2, 3, 4, 5]) = 1 + (2 + sum([3, 4, 5])) = 1 + 2 + (3 + sum([4, 5])) = 1 + 2 + 3 + (4 + sum([5])) = 1 + 2 + 3 + 4 + (5 + sum([])) = 1 + 2 + 3 + 4 + 5 + 0 = 15.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Comparison | O(1) | O(1) |
| Recursive Call | O(n) | O(n) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the size of the input array.
- Time complexity: O(n), as the algorithm makes one recursive call per element.
- Space complexity: O(n) due to the recursive call stack, which grows linearly with the array size.
- Best, average, and worst cases are all O(n), as every element is processed exactly once.
✅ Tip: Use recursion for problems like array summation when the logic is naturally recursive, and test with edge cases like empty arrays or single-element arrays to ensure correctness.
⚠ Warning: Be cautious with large arrays, as the O(n) space complexity from the recursive call stack can lead to stack overflow errors. Consider iterative solutions for very large inputs.
Reverse a String Recursively
Problem Statement
Write a Java program that uses recursion to reverse a string by breaking it into smaller substrings. The program should take a string as input and return the reversed string. Test the program with various strings, including empty strings and single-character strings. You can visualize this as rearranging the letters of a word written on a whiteboard, starting from the last letter and moving backward to the first, using smaller pieces of the word.
Input: A string (e.g., str = "hello").
Output: A string representing the reversed input (e.g., "olleh").
Constraints:
- The string length is between 0 and 10^5.
- The string contains printable ASCII characters.
- The string may be empty. Example:
- Input:
str = "hello" - Output:
"olleh" - Explanation: The string
"hello"is reversed to"olleh"by rearranging its characters starting from the last one.
Pseudocode
FUNCTION reverseString(str)
IF str is null OR length of str <= 1 THEN
RETURN str
ENDIF
SET lastChar to character at index (length of str - 1)
SET substring to str from index 0 to (length of str - 1)
RETURN lastChar + reverseString(substring)
ENDFUNCTION
Algorithm Steps
- Check if the input string is null, empty, or has one character. If so, return the string as is, since no reversal is needed.
- Define a recursive function that takes the input string as a parameter.
- In the recursive function, implement the base case: if the string is empty or has one character, return the string itself.
- For the recursive case, extract the last character of the string and recursively call the function on the substring excluding the last character.
- Combine the last character with the result of the recursive call to build the reversed string.
- Return the final reversed string from the initial function call.
Java Implementation
public class ReverseStringRecursively {
// Reverses a string using recursion
public String reverseString(String str) {
// Check for null, empty, or single-character string
if (str == null || str.length() <= 1) {
return str;
}
// Recursive case: last character + reverse of remaining string
return str.charAt(str.length() - 1) + reverseString(str.substring(0, str.length() - 1));
}
}
Output
For the input string "hello", the program outputs:
olleh
Explanation: The string "hello" is reversed to "olleh" by recursively rearranging characters.
For an empty string "", the program outputs:
""
Explanation: An empty string remains empty after reversal.
For a single-character string "a", the program outputs:
a
Explanation: A single-character string is already reversed.
How It Works
- Step 1: The
reverseStringmethod checks if the input string is null, empty, or has one character. For"hello", it has length 5, so it proceeds to the recursive case. - Step 2: In the recursive case:
- For
"hello": Returns'o'+reverseString("hell"). - For
"hell": Returns'l'+reverseString("hel"). - For
"hel": Returns'l'+reverseString("he"). - For
"he": Returns'e'+reverseString("h"). - For
"h": Base case reached (length 1), returns"h".
- For
- Step 3: The recursion unwinds:
'h'+'e'+'l'+'l'+'o'="olleh". - Example Trace: For
"hello", the algorithm recursively processes:'o'+reverse("hell")='o'+ ('l'+reverse("hel")) ='o'+'l'+ ('l'+reverse("he")) ='o'+'l'+'l'+ ('e'+reverse("h")) ='o'+'l'+'l'+'e'+'h'="olleh".
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Comparison | O(1) | O(1) |
| Recursive Call | O(n) | O(n) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the length of the input string.
- Time complexity: O(n), as the algorithm makes one recursive call per character.
- Space complexity: O(n) due to the recursive call stack and the creation of substrings in Java, which involves new string objects.
- Best, average, and worst cases are all O(n), as every character is processed exactly once.
✅ Tip: Use recursion for string reversal when the problem naturally breaks into smaller subproblems, and test with edge cases like empty strings, single characters, or long strings to ensure robustness.
⚠ Warning: Be cautious with very long strings, as the O(n) space complexity from the recursive call stack and substring creation can lead to stack overflow or memory issues. Consider iterative solutions for large inputs.
Tower of Hanoi
Problem Statement
Write a Java program that uses recursion to solve the Tower of Hanoi problem for n disks, printing the sequence of moves required to transfer all disks from a source rod to a destination rod using an auxiliary rod. The rules are: only one disk can be moved at a time, a larger disk cannot be placed on a smaller disk, and disks must be moved between the rods (source, auxiliary, destination). Test the program with different values of n, including edge cases like n = 1. You can visualize this as moving a stack of differently sized books from one table to another, using a third table as temporary storage, ensuring larger books are always below smaller ones.
Input: A non-negative integer n representing the number of disks, and three rod names (e.g., source = 'A', auxiliary = 'B', destination = 'C').
Output: A sequence of moves printed in the format "Move disk [number] from [source] to [destination]" (e.g., "Move disk 1 from A to C"), with each move on a new line.
Constraints:
- 0 ≤ n ≤ 10 (to keep output manageable and avoid excessive recursion).
- Rod names are single characters (e.g., 'A', 'B', 'C'). Example:
- Input:
n = 2, source = 'A', auxiliary = 'B', destination = 'C' - Output:
Move disk 1 from A to B Move disk 2 from A to C Move disk 1 from B to C - Explanation: The two disks are moved from rod A to rod C using rod B as auxiliary, following the rules of the Tower of Hanoi.
Pseudocode
FUNCTION towerOfHanoi(n, source, auxiliary, destination)
IF n equals 0 THEN
RETURN
ENDIF
IF n equals 1 THEN
PRINT "Move disk 1 from source to destination"
RETURN
ENDIF
CALL towerOfHanoi(n - 1, source, destination, auxiliary)
PRINT "Move disk n from source to destination"
CALL towerOfHanoi(n - 1, auxiliary, source, destination)
ENDFUNCTION
Algorithm Steps
- Check if the number of disks
nis 0. If so, return immediately, as no moves are needed. - Define a recursive function that takes the number of disks
nand the names of the source, auxiliary, and destination rods as parameters. - In the recursive function, implement the base case: if
nequals 1, print a move to transfer the single disk from the source to the destination rod and return. - For the recursive case, perform three steps:
a. Recursively move
n - 1disks from the source rod to the auxiliary rod, using the destination rod as the auxiliary. b. Print a move to transfer the nth disk from the source rod to the destination rod. c. Recursively moven - 1disks from the auxiliary rod to the destination rod, using the source rod as the auxiliary. - In the main function, call the recursive function with the input
nand rod names (e.g., 'A', 'B', 'C'). - The function prints the sequence of moves to solve the Tower of Hanoi problem.
Java Implementation
public class TowerOfHanoi {
// Solves Tower of Hanoi for n disks and prints moves
public void towerOfHanoi(int n, char source, char auxiliary, char destination) {
// Base case: no disks to move
if (n == 0) {
return;
}
// Base case: single disk, move directly
if (n == 1) {
System.out.println("Move disk 1 from " + source + " to " + destination);
return;
}
// Recursive case: move n-1 disks to auxiliary rod
towerOfHanoi(n - 1, source, destination, auxiliary);
// Move nth disk to destination rod
System.out.println("Move disk " + n + " from " + source + " to " + destination);
// Move n-1 disks from auxiliary to destination rod
towerOfHanoi(n - 1, auxiliary, source, destination);
}
}
Output
For the input n = 2, source = 'A', auxiliary = 'B', destination = 'C', the program outputs:
Move disk 1 from A to B
Move disk 2 from A to C
Move disk 1 from B to C
Explanation: The two disks are moved from rod A to rod C using rod B as auxiliary, following the Tower of Hanoi rules.
For the input n = 1, source = 'A', auxiliary = 'B', destination = 'C', the program outputs:
Move disk 1 from A to C
Explanation: A single disk is moved directly from rod A to rod C.
How It Works
- Step 1: The
towerOfHanoimethod checks ifnis 0. Forn = 2, it proceeds to the recursive case. - Step 2: For
n = 2:- First recursive call:
towerOfHanoi(1, A, C, B):- Base case:
n = 1, prints "Move disk 1 from A to B".
- Base case:
- Print: "Move disk 2 from A to C".
- Second recursive call:
towerOfHanoi(1, B, A, C):- Base case:
n = 1, prints "Move disk 1 from B to C".
- Base case:
- First recursive call:
- Step 3: The recursion generates the sequence of moves to transfer all disks from source to destination while respecting the rules.
- Example Trace: For
n = 2:- Move disk 1 from A to B (move top disk to auxiliary).
- Move disk 2 from A to C (move largest disk to destination).
- Move disk 1 from B to C (move top disk to destination).
- Testing with Different n: For
n = 1, it prints one move. Forn = 3, it generates 7 moves (2^3 - 1), following the same recursive pattern.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Recursive Call | O(2^n) | O(n) |
| Print Operation | O(1) | O(1) |
| Full Algorithm | O(2^n) | O(n) |
Note:
- n is the number of disks.
- Time complexity: O(2^n), as the algorithm makes 2^n - 1 moves, with each move involving a constant-time print operation.
- Space complexity: O(n) due to the recursive call stack, which grows linearly with the number of disks.
- Best, average, and worst cases are all O(2^n), as the number of moves is fixed for a given
n.
✅ Tip: Use the Tower of Hanoi to understand recursive problem-solving, and test with small values of
n(e.g., 1, 2, 3) to visualize the move sequence. Count the number of moves to confirm it matches 2^n - 1.
⚠ Warning: Avoid large values of
n(e.g., >10), as the O(2^n) time complexity leads to an exponential number of moves, causing significant delays. Ensure rod names are distinct to avoid confusion in output.
Fibonacci Optimization
Problem Statement
Write a Java program that computes the nth Fibonacci number using a recursive approach with memoization (using a HashMap to cache results) and an iterative approach. The program should return the nth Fibonacci number and compare the performance of both methods for large inputs. Test the program with various inputs, including edge cases like (n = 0) and (n = 1). You can visualize this as calculating the number of rabbits in a population after (n) months, where each month’s population depends on the previous two, with memoization or iteration to optimize the process.
Input: A non-negative integer (n) (e.g., (n = 6)). Output: A long integer representing the nth Fibonacci number (e.g., 8 for (n = 6)). Constraints:
- (0 \leq n \leq 50) (to avoid overflow with
long). - The input is a non-negative integer. Example:
- Input: (n = 6)
- Output: 8
- Explanation: The Fibonacci sequence is 0, 1, 1, 2, 3, 5, 8, ..., so the 6th Fibonacci number is 8.
- Input: (n = 0)
- Output: 0
- Explanation: The 0th Fibonacci number is defined as 0.
Pseudocode
FUNCTION fibonacciMemo(n, memo)
IF n < 0 THEN
RETURN -1
ENDIF
IF n equals 0 THEN
RETURN 0
ENDIF
IF n equals 1 THEN
RETURN 1
ENDIF
IF n exists in memo THEN
RETURN memo[n]
ENDIF
SET result to fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo)
SET memo[n] to result
RETURN result
ENDFUNCTION
FUNCTION fibonacciIterative(n)
IF n < 0 THEN
RETURN -1
ENDIF
IF n equals 0 THEN
RETURN 0
ENDIF
IF n equals 1 THEN
RETURN 1
ENDIF
SET prev to 0
SET current to 1
FOR i from 2 to n
SET next to prev + current
SET prev to current
SET current to next
ENDFOR
RETURN current
ENDFUNCTION
FUNCTION mainFibonacci(n)
CREATE empty HashMap memo
CALL fibonacciMemo(n, memo)
CALL fibonacciIterative(n)
RETURN results
ENDFUNCTION
Algorithm Steps
- For the recursive approach with memoization: a. Check if (n) is negative. If so, return -1, as Fibonacci is undefined for negative numbers. b. Define a recursive helper function that takes (n) and a HashMap for memoization as parameters. c. Implement base cases: if (n = 0), return 0; if (n = 1), return 1. d. Check if the Fibonacci number for (n) is in the HashMap. If so, return the cached result. e. Compute the Fibonacci number as the sum of the results for (n - 1) and (n - 2) using recursive calls. f. Store the result in the HashMap and return it.
- For the iterative approach:
a. Check if (n) is negative. If so, return -1.
b. Implement base cases: if (n = 0), return 0; if (n = 1), return 1.
c. Initialize variables
prev(F(0) = 0) andcurrent(F(1) = 1). d. Iterate from (i = 2) to (n), computing each Fibonacci number asprev + current, updatingprevandcurrentaccordingly. e. Return the final Fibonacci number. - In the main function, create a HashMap for memoization, call both methods, and compare their performance for large inputs.
Java Implementation
import java.util.HashMap;
public class FibonacciOptimization {
// Computes nth Fibonacci number using recursion with memoization
public long fibonacciMemo(int n) {
// Check for invalid input
if (n < 0) {
return -1;
}
// Create HashMap for memoization
HashMap<Integer, Long> memo = new HashMap<>();
// Call recursive helper function
return fibonacciMemoHelper(n, memo);
}
// Helper function for recursive Fibonacci with memoization
private long fibonacciMemoHelper(int n, HashMap<Integer, Long> memo) {
// Base case: F(0) = 0
if (n == 0) {
return 0;
}
// Base case: F(1) = 1
if (n == 1) {
return 1;
}
// Check if result is in memo
if (memo.containsKey(n)) {
return memo.get(n);
}
// Recursive case: F(n) = F(n-1) + F(n-2)
long result = fibonacciMemoHelper(n - 1, memo) + fibonacciMemoHelper(n - 2, memo);
// Cache result in memo
memo.put(n, result);
return result;
}
// Computes nth Fibonacci number using iteration
public long fibonacciIterative(int n) {
// Check for invalid input
if (n < 0) {
return -1;
}
// Base case: F(0) = 0
if (n == 0) {
return 0;
}
// Base case: F(1) = 1
if (n == 1) {
return 1;
}
// Initialize variables for F(0) and F(1)
long prev = 0;
long current = 1;
// Iterate to compute F(n)
for (int i = 2; i <= n; i++) {
long next = prev + current;
prev = current;
current = next;
}
// Return nth Fibonacci number
return current;
}
}
Output
For the input (n = 6), both methods output:
8
Explanation: The Fibonacci sequence is 0, 1, 1, 2, 3, 5, 8, so the 6th Fibonacci number is 8.
For the input (n = 0), both methods output:
0
Explanation: The 0th Fibonacci number is defined as 0.
For the input (n = 10), both methods output:
55
Explanation: The 10th Fibonacci number is 55.
How It Works
- Recursive Approach with Memoization:
- Step 1: The
fibonacciMemomethod checks if (n < 0). For (n = 6), it creates a HashMap and callsfibonacciMemoHelper. - Step 2: In
fibonacciMemoHelper:- For (n = 6): Not in memo, compute F(5) + F(4).
- For (n = 5): Not in memo, compute F(4) + F(3).
- For (n = 4): Not in memo, compute F(3) + F(2).
- For (n = 3): Not in memo, compute F(2) + F(1).
- For (n = 2): Not in memo, compute F(1) + F(0) = 1 + 0 = 1.
- For (n = 1): Base case, return 1.
- For (n = 0): Base case, return 0.
- Unwind: Cache F(2) = 1, F(3) = 2, F(4) = 3, F(5) = 5, F(6) = 8.
- Example Trace: For (n = 6), computes F(6) = F(5) + F(4) = 5 + 3 = 8, caching each result.
- Step 1: The
- Iterative Approach:
- Step 1: The
fibonacciIterativemethod checks if (n < 0). For (n = 6), initializesprev = 0,current = 1. - Step 2: Iterates from (i = 2) to 6:
- (i = 2):
next = 0 + 1 = 1,prev = 1,current = 1. - (i = 3):
next = 1 + 1 = 2,prev = 1,current = 2. - (i = 4):
next = 1 + 2 = 3,prev = 2,current = 3. - (i = 5):
next = 2 + 3 = 5,prev = 3,current = 5. - (i = 6):
next = 3 + 5 = 8,prev = 5,current = 8.
- (i = 2):
- Example Trace: For (n = 6), computes 0, 1, 1, 2, 3, 5, 8, returning 8.
- Step 1: The
- Performance Comparison: Memoized recursive is O(n) time, much faster than O(2^n) for naive recursion, and comparable to iterative O(n) time. Iterative uses O(1) space, while memoized uses O(n) space for the HashMap and call stack, making iterative more efficient for very large (n).
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Recursive Call (Memoized) | O(n) | O(n) |
| Iteration | O(n) | O(1) |
| Full Algorithm (Memoized) | O(n) | O(n) |
| Full Algorithm (Iterative) | O(n) | O(1) |
Note:
- n is the input integer.
- Time complexity: O(n) for memoized recursive, as each Fibonacci number is computed once and cached; O(n) for iterative, as it performs n iterations.
- Space complexity: O(n) for memoized recursive due to the HashMap and call stack; O(1) for iterative, using only a few variables.
- Best, average, and worst cases are O(n) for both.
✅ Tip: Use the iterative approach for large (n) to minimize space usage, or memoization for recursive solutions to avoid exponential time complexity. Test with large inputs (e.g., (n = 50)) to compare performance and verify results.
⚠ Warning: Without memoization, recursive Fibonacci has O(2^n) time complexity, making it impractical for large (n). Ensure inputs are within constraints ((n \leq 50)) to avoid overflow with
long.
Linked List Traversal
Problem Statement
Write a Java program that implements both recursive and iterative methods to traverse and print the elements of a singly linked list. The program should take a singly linked list as input and print each element in order, from the head to the tail. Test both approaches with lists of varying sizes, including empty lists and single-node lists, and discuss their trade-offs in terms of readability, performance, and memory usage. You can visualize this as walking through a chain of boxes, each containing a number, and reading the numbers either by stepping through each box (iterative) or by recursively passing to the next box (recursive).
Input: A singly linked list with integer nodes (e.g., 1 -> 2 -> 3 -> null).
Output: The elements of the list printed in order, each on a new line (e.g., 1, 2, 3).
Constraints:
- The list contains 0 to 10^5 nodes.
- Node values are integers between -10^9 and 10^9.
- The list may be empty. Example:
- Input: Linked list
1 -> 2 -> 3 -> null - Output:
1 2 3 - Explanation: The elements 1, 2, and 3 are printed in order.
- Input: Empty linked list
null - Output: (no output)
- Explanation: An empty list has no elements to print.
Pseudocode
FUNCTION recursivePrint(node)
IF node is null THEN
RETURN
ENDIF
PRINT node.value
CALL recursivePrint(node.next)
ENDFUNCTION
FUNCTION iterativePrint(node)
SET current to node
WHILE current is not null
PRINT current.value
SET current to current.next
ENDWHILE
ENDFUNCTION
FUNCTION mainPrint(head)
CALL recursivePrint(head)
CALL iterativePrint(head)
ENDFUNCTION
Algorithm Steps
- For the recursive approach: a. Check if the current node is null. If so, return, as there are no more elements to print. b. Define a recursive function that takes the current node as a parameter. c. Print the value of the current node. d. Recursively call the function on the next node in the list.
- For the iterative approach:
a. Initialize a pointer
currentto the head of the list. b. Whilecurrentis not null, print the value ofcurrentand movecurrentto the next node. - In the main function, call both the recursive and iterative methods to print the list’s elements.
- Discuss trade-offs: recursive is more concise but uses more memory due to the call stack; iterative is less readable but more space-efficient.
Java Implementation
public class LinkedListTraversal {
// Node class for singly linked list
class Node {
int data;
Node next;
Node(int data) {
this.data = data;
this.next = null;
}
}
// Prints linked list using recursion
public void recursivePrint(Node head) {
// Base case: if node is null, stop
if (head == null) {
return;
}
// Print current node's data
System.out.println(head.data);
// Recursive call for next node
recursivePrint(head.next);
}
// Prints linked list using iteration
public void iterativePrint(Node head) {
// Start at head
Node current = head;
// Iterate until null
while (current != null) {
// Print current node's data
System.out.println(current.data);
// Move to next node
current = current.next;
}
}
}
Output
For the input linked list 1 -> 2 -> 3 -> null, both methods output:
1
2
3
Explanation: The elements 1, 2, and 3 are printed in order from head to tail.
For an empty linked list null, both methods output:
(nothing printed)
Explanation: An empty list has no elements to print.
For a single-node linked list 4 -> null, both methods output:
4
Explanation: The single node’s value 4 is printed.
How It Works
- Recursive Approach:
- Step 1: The
recursivePrintmethod checks if the head is null. For1 -> 2 -> 3 -> null, it proceeds. - Step 2: Recursion:
- For node
1: Print 1, recurse on node2. - For node
2: Print 2, recurse on node3. - For node
3: Print 3, recurse onnull. - For
null: Base case, return.
- For node
- Example Trace: For
1 -> 2 -> 3 -> null, prints 1, then 2, then 3.
- Step 1: The
- Iterative Approach:
- Step 1: The
iterativePrintmethod initializescurrentto the head (1). - Step 2: Iterates:
current = 1: Print 1, move to2.current = 2: Print 2, move to3.current = 3: Print 3, move tonull.current = null: Stop.
- Example Trace: For
1 -> 2 -> 3 -> null, prints 1, 2, 3 in sequence.
- Step 1: The
- Trade-offs:
- Readability: Recursive is more concise and elegant, mirroring the list’s structure, but may be harder for beginners to grasp.
- Performance: Both have O(n) time, but recursive uses O(n) space due to the call stack, while iterative uses O(1) space, making it more efficient for large lists.
- Memory: Recursive risks stack overflow for very large lists; iterative is safer.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Recursive Call | O(n) | O(n) |
| Iteration | O(n) | O(1) |
| Full Algorithm (Recursive) | O(n) | O(n) |
| Full Algorithm (Iterative) | O(n) | O(1) |
Note:
- n is the number of nodes in the linked list.
- Time complexity: O(n) for both, as each node is visited exactly once.
- Space complexity: O(n) for recursive due to the call stack; O(1) for iterative, using only a single pointer.
- Best, average, and worst cases are O(n) for both.
✅ Tip: Use the iterative approach for large linked lists to avoid stack overflow and minimize memory usage. Test with empty lists and long lists to verify correctness and compare performance.
⚠ Warning: The recursive approach may cause stack overflow for very large lists (e.g., 10^5 nodes) due to O(n) space complexity. Ensure the list is properly constructed to avoid null pointer exceptions.
Power Function
Problem Statement
Write a Java program that implements both recursive and iterative methods to compute (x^n), where (x) is a double and (n) is a non-negative integer representing the power. The program should return the result of (x) raised to the power (n) and test the methods with different inputs, including edge cases like (n = 0) and (n = 1). Analyze the space complexity of both approaches. You can visualize this as calculating the total area of a square piece of land ((x)) that is scaled by itself (n) times, either by repeatedly multiplying (iterative) or by breaking it into smaller multiplications (recursive).
Input: A double (x) and a non-negative integer (n) (e.g., (x = 2.0, n = 3)). Output: A double representing (x^n) (e.g., 8.0 for (2.0^3)). Constraints:
- (-100.0 \leq x \leq 100.0).
- (0 \leq n \leq 10^3).
- The result should fit within a double’s precision. Example:
- Input: (x = 2.0, n = 3)
- Output: 8.0
- Explanation: (2.0^3 = 2.0 * 2.0 * 2.0 = 8.0).
- Input: (x = 5.0, n = 0)
- Output: 1.0
- Explanation: Any non-zero number raised to the power 0 is 1.
Pseudocode
FUNCTION recursivePower(x, n)
IF n equals 0 THEN
RETURN 1.0
ENDIF
IF n equals 1 THEN
RETURN x
ENDIF
RETURN x * recursivePower(x, n - 1)
ENDFUNCTION
FUNCTION iterativePower(x, n)
IF n equals 0 THEN
RETURN 1.0
ENDIF
SET result to 1.0
FOR i from 1 to n
SET result to result * x
ENDFOR
RETURN result
ENDFUNCTION
Algorithm Steps
- For the recursive approach: a. Check if (n) is 0. If so, return 1.0, as any non-zero number raised to the power 0 is 1. b. Check if (n) is 1. If so, return (x), as (x^1 = x). c. Define a recursive function that multiplies (x) by the result of the recursive call for (n - 1). d. Return the computed result.
- For the iterative approach:
a. Check if (n) is 0. If so, return 1.0.
b. Initialize a variable
resultto 1.0. c. Iterate (n) times, multiplyingresultby (x) in each iteration. d. Return the finalresult. - Test both methods with various inputs (e.g., (n = 0, 1, 5)) and analyze space complexity.
Java Implementation
public class PowerFunction {
// Computes x^n using recursion
public double recursivePower(double x, int n) {
// Base case: x^0 = 1
if (n == 0) {
return 1.0;
}
// Base case: x^1 = x
if (n == 1) {
return x;
}
// Recursive case: x * x^(n-1)
return x * recursivePower(x, n - 1);
}
// Computes x^n using iteration
public double iterativePower(double x, int n) {
// Base case: x^0 = 1
if (n == 0) {
return 1.0;
}
// Initialize result
double result = 1.0;
// Multiply x by itself n times
for (int i = 1; i <= n; i++) {
result *= x;
}
// Return final result
return result;
}
}
Output
For the input (x = 2.0, n = 3), both methods output:
8.0
Explanation: (2.0^3 = 2.0 * 2.0 * 2.0 = 8.0).
For the input (x = 5.0, n = 0), both methods output:
1.0
Explanation: Any non-zero number raised to the power 0 is 1.
For the input (x = 3.0, n = 2), both methods output:
9.0
Explanation: (3.0^2 = 3.0 * 3.0 = 9.0).
How It Works
- Recursive Approach:
- Step 1: The
recursivePowermethod checks if (n = 0). For (x = 2.0, n = 3), it proceeds to recursion. - Step 2: Recursion:
- For (n = 3): Returns (2.0 * recursivePower(2.0, 2)).
- For (n = 2): Returns (2.0 * recursivePower(2.0, 1)).
- For (n = 1): Base case, returns 2.0.
- Unwind: (2.0 * (2.0 * 2.0) = 2.0 * 4.0 = 8.0).
- Example Trace: For (x = 2.0, n = 3), computes (2.0 * (2.0 * (2.0 * 1)) = 8.0).
- Step 1: The
- Iterative Approach:
- Step 1: The
iterativePowermethod checks if (n = 0). For (x = 2.0, n = 3), it initializesresult = 1.0. - Step 2: Iterates 3 times:
- Iteration 1:
result = 1.0 * 2.0 = 2.0. - Iteration 2:
result = 2.0 * 2.0 = 4.0. - Iteration 3:
result = 4.0 * 2.0 = 8.0.
- Iteration 1:
- Example Trace: For (x = 2.0, n = 3), multiplies (2.0 * 2.0 * 2.0 = 8.0).
- Step 1: The
- Space Complexity Analysis:
- Recursive: O(n) due to the call stack, which grows linearly with (n).
- Iterative: O(1), as it uses only a single variable for the result.
- The iterative approach is more space-efficient, especially for large (n), while both have similar time performance for this basic implementation.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Recursive Call | O(n) | O(n) |
| Iteration | O(n) | O(1) |
| Full Algorithm (Recursive) | O(n) | O(n) |
| Full Algorithm (Iterative) | O(n) | O(1) |
Note:
- n is the exponent.
- Time complexity: O(n) for both, as recursive makes n calls, and iterative performs n multiplications.
- Space complexity: O(n) for recursive due to the call stack; O(1) for iterative, using only a single variable.
- Best, average, and worst cases are O(n) for both.
✅ Tip: Use the iterative approach for large (n) to avoid stack overflow and minimize space usage. Test with edge cases like (n = 0), (n = 1), and large (n) (e.g., 1000) to verify correctness and compare space efficiency.
⚠ Warning: The recursive approach may cause stack overflow for very large (n) due to O(n) space complexity. Be cautious with floating-point precision for large (x) or (n), as results may exceed double’s capacity.
Recursive vs. Iterative Sum
Problem Statement
Write a Java program that computes the sum of all elements in an array of integers using both recursive and iterative approaches. The program should return the total sum and allow performance comparison between the two methods for large arrays. Test the program with arrays of different sizes, including empty arrays and single-element arrays. You can visualize this as calculating the total cost of items in a shopping list, either by adding items one by one (iterative) or by breaking the list into smaller parts and combining their sums (recursive).
Input: An array of integers (e.g., arr = [1, 2, 3, 4, 5]).
Output: An integer representing the sum of all elements in the array (e.g., 15 for [1, 2, 3, 4, 5]).
Constraints:
- The array length is between 0 and 10^5.
- Elements are integers between -10^9 and 10^9.
- The array may be empty. Example:
- Input:
arr = [1, 2, 3, 4, 5] - Output: 15
- Explanation: The sum is calculated as 1 + 2 + 3 + 4 + 5 = 15.
- Input:
arr = [] - Output: 0
- Explanation: The sum of an empty array is 0.
Pseudocode
FUNCTION recursiveSum(arr, index)
IF arr is null OR index equals length of arr THEN
RETURN 0
ENDIF
RETURN arr[index] + recursiveSum(arr, index + 1)
ENDFUNCTION
FUNCTION iterativeSum(arr)
IF arr is null OR length of arr equals 0 THEN
RETURN 0
ENDIF
SET sum to 0
FOR each index i from 0 to length of arr - 1
SET sum to sum + arr[i]
ENDFOR
RETURN sum
ENDFUNCTION
FUNCTION mainSum(arr)
CALL recursiveSum(arr, 0)
CALL iterativeSum(arr)
RETURN results
ENDFUNCTION
Algorithm Steps
- For the recursive approach: a. Check if the array is null or the current index equals the array length. If so, return 0. b. Define a recursive helper function that takes the array and the current index as parameters. c. In the recursive function, add the element at the current index to the result of the recursive call for the next index. d. Call the recursive function with the initial index set to 0.
- For the iterative approach: a. Check if the array is null or empty. If so, return 0. b. Initialize a variable to store the sum. c. Iterate through the array from index 0 to the last index, adding each element to the sum. d. Return the final sum.
- In the main function, call both the recursive and iterative methods to compute the sum.
- Return the results for comparison, ideally measuring execution time for large arrays.
Java Implementation
public class RecursiveVsIterativeSum {
// Computes sum of array elements using recursion
public long recursiveSum(int[] arr) {
// Check for null array
if (arr == null) {
return 0;
}
// Call recursive helper with initial index 0
return recursiveSumHelper(arr, 0);
}
// Helper function for recursive sum
private long recursiveSumHelper(int[] arr, int index) {
// Base case: if index reaches array length, return 0
if (index == arr.length) {
return 0;
}
// Recursive case: add current element to sum of remaining elements
return arr[index] + recursiveSumHelper(arr, index + 1);
}
// Computes sum of array elements using iteration
public long iterativeSum(int[] arr) {
// Check for null or empty array
if (arr == null || arr.length == 0) {
return 0;
}
// Initialize sum
long sum = 0;
// Iterate through array and add each element
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
// Return final sum
return sum;
}
}
Output
For the input array [1, 2, 3, 4, 5], both methods output:
15
Explanation: The sum is computed as 1 + 2 + 3 + 4 + 5 = 15.
For an empty array [], both methods output:
0
Explanation: The sum of an empty array is 0.
For a single-element array [7], both methods output:
7
Explanation: The sum of a single-element array is the element itself.
How It Works
- Recursive Approach:
- Step 1: The
recursiveSummethod checks if the array is null. For[1, 2, 3, 4, 5], it callsrecursiveSumHelperwith index 0. - Step 2: In
recursiveSumHelper:- Index 0:
arr[0] = 1, recurse with index 1. - Index 1:
arr[1] = 2, recurse with index 2. - Index 2:
arr[2] = 3, recurse with index 3. - Index 3:
arr[3] = 4, recurse with index 4. - Index 4:
arr[4] = 5, recurse with index 5. - Index 5: Base case, return 0.
- Unwind: 5 + (4 + (3 + (2 + (1 + 0)))) = 15.
- Index 0:
- Example Trace: For
[1, 2, 3, 4, 5], computes 1 + (2 + (3 + (4 + (5 + 0)))) = 15.
- Step 1: The
- Iterative Approach:
- Step 1: The
iterativeSummethod checks if the array is null or empty. For[1, 2, 3, 4, 5], it initializessum = 0. - Step 2: Iterates: adds 1, 2, 3, 4, 5, resulting in
sum = 15. - Example Trace: For
[1, 2, 3, 4, 5], adds 1 + 2 + 3 + 4 + 5 = 15.
- Step 1: The
- Performance Comparison: For large arrays (e.g., 10^5 elements), the iterative approach is generally faster due to O(1) space complexity, while the recursive approach uses O(n) stack space, potentially causing stack overflow for very large arrays.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Recursive Call | O(n) | O(n) |
| Iteration | O(n) | O(1) |
| Full Algorithm (Recursive) | O(n) | O(n) |
| Full Algorithm (Iterative) | O(n) | O(1) |
Note:
- n is the size of the input array.
- Time complexity: O(n) for both approaches, as each element is processed exactly once.
- Space complexity: O(n) for recursive due to the call stack; O(1) for iterative, using only a single sum variable.
- Best, average, and worst cases are O(n) for both, as all elements are summed.
✅ Tip: Use the iterative approach for large arrays to avoid stack overflow and reduce memory usage. Test both methods with large arrays (e.g., 10^4, 10^5 elements) to compare execution times and verify identical results.
⚠ Warning: The recursive approach may cause stack overflow for very large arrays due to O(n) space complexity. Ensure inputs are within reasonable bounds (e.g., n ≤ 10^5) to prevent performance issues.
Reverse String Comparison
Problem Statement
Write a Java program that implements string reversal using both recursive and iterative approaches. The program should take a string as input and return the reversed string. Test the program with various strings, including empty strings, single-character strings, and long strings, and compare the code readability and execution time of both approaches. You can visualize this as rearranging the letters of a word on a signboard, either by recursively breaking it into smaller parts or by iteratively swapping characters from the ends toward the center.
Input: A string (e.g., str = "hello").
Output: A string representing the reversed input (e.g., "olleh").
Constraints:
- The string length is between 0 and 10^5.
- The string contains printable ASCII characters.
- The string may be empty. Example:
- Input:
str = "hello" - Output:
"olleh" - Explanation: The string
"hello"is reversed to"olleh"by rearranging its characters. - Input:
str = "" - Output:
"" - Explanation: An empty string remains empty after reversal.
Pseudocode
FUNCTION recursiveReverse(str)
IF str is null OR length of str <= 1 THEN
RETURN str
ENDIF
SET lastChar to character at index (length of str - 1)
SET substring to str from index 0 to (length of str - 1)
RETURN lastChar + recursiveReverse(substring)
ENDFUNCTION
FUNCTION iterativeReverse(str)
IF str is null OR length of str <= 1 THEN
RETURN str
ENDIF
CONVERT str to array of characters
SET left to 0
SET right to length of array - 1
WHILE left < right
SWAP characters at left and right
INCREMENT left
DECREMENT right
ENDWHILE
RETURN array converted to string
ENDFUNCTION
Algorithm Steps
- For the recursive approach: a. Check if the input string is null, empty, or has one character. If so, return the string as is. b. Define a recursive function that takes the input string as a parameter. c. Extract the last character of the string and recursively call the function on the substring excluding the last character. d. Combine the last character with the result of the recursive call to build the reversed string. e. Return the reversed string.
- For the iterative approach:
a. Check if the input string is null, empty, or has one character. If so, return the string as is.
b. Convert the string to a character array for in-place manipulation.
c. Initialize two pointers:
leftat the start (index 0) andrightat the end (index length - 1). d. Whileleftis less thanright, swap the characters atleftandright, incrementleft, and decrementright. e. Convert the character array back to a string and return it. - Test both methods with various inputs and measure execution time for large strings to compare performance.
Java Implementation
public class ReverseStringComparison {
// Reverses a string using recursion
public String recursiveReverse(String str) {
// Check for null, empty, or single-character string
if (str == null || str.length() <= 1) {
return str;
}
// Recursive case: last character + reverse of remaining string
return str.charAt(str.length() - 1) + recursiveReverse(str.substring(0, str.length() - 1));
}
// Reverses a string using iteration
public String iterativeReverse(String str) {
// Check for null, empty, or single-character string
if (str == null || str.length() <= 1) {
return str;
}
// Convert string to char array
char[] charArray = str.toCharArray();
int left = 0;
int right = charArray.length - 1;
// Swap characters from ends toward center
while (left < right) {
char temp = charArray[left];
charArray[left] = charArray[right];
charArray[right] = temp;
left++;
right--;
}
// Convert char array back to string
return new String(charArray);
}
}
Output
For the input string "hello", both methods output:
olleh
Explanation: The string "hello" is reversed to "olleh" by rearranging its characters.
For an empty string "", both methods output:
""
Explanation: An empty string remains empty after reversal.
For a single-character string "a", both methods output:
a
Explanation: A single-character string is already reversed.
How It Works
- Recursive Approach:
- Step 1: The
recursiveReversemethod checks if the string is null, empty, or has one character. For"hello", it proceeds to recursion. - Step 2: Recursion:
- For
"hello": Returns'o'+recursiveReverse("hell"). - For
"hell": Returns'l'+recursiveReverse("hel"). - For
"hel": Returns'l'+recursiveReverse("he"). - For
"he": Returns'e'+recursiveReverse("h"). - For
"h": Base case, returns"h". - Unwind:
'h'+'e'+'l'+'l'+'o'="olleh".
- For
- Example Trace: For
"hello", computes'o'+ ('l'+ ('l'+ ('e'+'h'))) ="olleh".
- Step 1: The
- Iterative Approach:
- Step 1: The
iterativeReversemethod checks if the string is null, empty, or has one character. For"hello", it converts to['h', 'e', 'l', 'l', 'o']. - Step 2: Initializes
left = 0,right = 4. Swaps:- Swap
hando:['o', 'e', 'l', 'l', 'h'],left = 1,right = 3. - Swap
eandl:['o', 'l', 'l', 'e', 'h'],left = 2,right = 2. - Stop as
left >= right.
- Swap
- Step 3: Converts array to
"olleh". - Example Trace: For
"hello", swaps characters from ends:h↔o,e↔l, resulting in"olleh".
- Step 1: The
- Comparison:
- Readability: Iterative is more straightforward, using simple swaps, while recursive is more abstract but elegant for small inputs.
- Execution Time: Iterative is faster for large strings due to O(1) space complexity, while recursive uses O(n) space and creates substrings, increasing overhead.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Recursive Call | O(n) | O(n) |
| Iteration | O(n) | O(n) |
| Full Algorithm (Recursive) | O(n) | O(n) |
| Full Algorithm (Iterative) | O(n) | O(n) |
Note:
- n is the length of the input string.
- Time complexity: O(n) for both, as recursive processes each character once, and iterative performs n/2 swaps.
- Space complexity: O(n) for recursive due to call stack and substring creation; O(n) for iterative due to the character array (though in-place swapping reduces overhead in practice).
- Best, average, and worst cases are O(n) for both.
✅ Tip: Use the iterative approach for large strings to avoid stack overflow and reduce memory usage due to substring creation in recursion. Test both methods with long strings (e.g., 10^4 characters) to compare execution times and assess readability.
⚠ Warning: The recursive approach may cause stack overflow for very large strings due to O(n) space complexity from the call stack and substring operations. Ensure inputs are within reasonable bounds to prevent performance issues.
Factorial Conversion
Problem Statement
Write a Java program that implements a tail-recursive method to compute the factorial of a non-negative integer (n) using an accumulator to track the running product, and convert this tail-recursive implementation to an iterative version. The program should return the factorial of (n) and test both methods with various inputs, including large values, while measuring stack usage by catching StackOverflowError for recursive calls. You can visualize this as calculating the number of ways to arrange (n) books on a shelf, either by recursively multiplying and passing the product forward or by iteratively accumulating the product.
Input: A non-negative integer (n) (e.g., (n = 5)). Output: A long integer representing the factorial of (n) (e.g., 120 for (n = 5)). Constraints:
- (0 \leq n \leq 20) (to avoid overflow with
long). - The input is a non-negative integer. Example:
- Input: (n = 5)
- Output: 120
- Explanation: The factorial of 5 is (5 * 4 * 3 * 2 * 1 = 120).
- Input: (n = 0)
- Output: 1
- Explanation: The factorial of 0 is defined as 1.
Pseudocode
FUNCTION tailRecursiveFactorial(n, accumulator)
IF n < 0 THEN
RETURN -1
ENDIF
IF n equals 0 THEN
RETURN accumulator
ENDIF
SET newAccumulator to accumulator * n
RETURN tailRecursiveFactorial(n - 1, newAccumulator)
ENDFUNCTION
FUNCTION iterativeFactorial(n)
IF n < 0 THEN
RETURN -1
ENDIF
IF n equals 0 THEN
RETURN 1
ENDIF
SET result to 1
FOR i from 1 to n
SET result to result * i
ENDFOR
RETURN result
ENDFUNCTION
FUNCTION mainFactorial(n)
TRY
CALL tailRecursiveFactorial(n, 1)
CATCH StackOverflowError
PRINT "Stack overflow occurred"
ENDTRY
CALL iterativeFactorial(n)
RETURN results
ENDFUNCTION
Algorithm Steps
- For the tail-recursive approach: a. Check if (n) is negative. If so, return -1, as factorial is undefined for negative numbers. b. Define a tail-recursive helper function that takes (n) and an accumulator as parameters. c. Implement the base case: if (n = 0), return the accumulator. d. For the recursive case, multiply the accumulator by (n) and recursively call the function with (n - 1) and the updated accumulator. e. Call the helper function with the initial accumulator set to 1.
- For the iterative approach:
a. Check if (n) is negative. If so, return -1.
b. Check if (n = 0). If so, return 1.
c. Initialize a variable
resultto 1. d. Iterate from 1 to (n), multiplyingresultby each integer. e. Return the finalresult. - Test both methods with various inputs, including large values (e.g., (n = 20)), and wrap the tail-recursive call in a try-catch block to detect
StackOverflowError. - Measure stack usage by observing whether
StackOverflowErroroccurs for large inputs in the recursive method.
Java Implementation
public class FactorialConversion {
// Computes factorial using tail recursion
public long tailRecursiveFactorial(int n) {
// Check for invalid input
if (n < 0) {
return -1;
}
// Call tail-recursive helper with initial accumulator
try {
return tailRecursiveFactorialHelper(n, 1);
} catch (StackOverflowError e) {
System.out.println("Stack overflow occurred for n = " + n);
return -1;
}
}
// Helper function for tail-recursive factorial
private long tailRecursiveFactorialHelper(int n, long accumulator) {
// Base case: n = 0 returns accumulator
if (n == 0) {
return accumulator;
}
// Recursive case: multiply accumulator by n and recurse
return tailRecursiveFactorialHelper(n - 1, accumulator * n);
}
// Computes factorial using iteration
public long iterativeFactorial(int n) {
// Check for invalid input
if (n < 0) {
return -1;
}
// Base case: 0! = 1
if (n == 0) {
return 1;
}
// Initialize result
long result = 1;
// Multiply numbers from 1 to n
for (int i = 1; i <= n; i++) {
result *= i;
}
// Return final result
return result;
}
}
Output
For the input (n = 5), both methods output:
120
Explanation: The factorial of 5 is (5 * 4 * 3 * 2 * 1 = 120).
For the input (n = 0), both methods output:
1
Explanation: The factorial of 0 is defined as 1.
For a large input (n = 20), both methods output:
2432902008176640000
Explanation: The factorial of 20 is computed correctly within the long range.
For a very large input (e.g., (n = 10000)), the tail-recursive method may output:
Stack overflow occurred for n = 10000
-1
Explanation: Java’s lack of tail-call optimization causes a stack overflow, while the iterative method may overflow the long data type but avoids stack issues.
How It Works
- Tail-Recursive Approach:
- Step 1: The
tailRecursiveFactorialmethod checks if (n < 0). For (n = 5), it calls the helper withaccumulator = 1. - Step 2: In
tailRecursiveFactorialHelper:- For (n = 5):
accumulator = 1 * 5 = 5, recurse with (n = 4, accumulator = 5). - For (n = 4):
accumulator = 5 * 4 = 20, recurse with (n = 3, accumulator = 20). - For (n = 3):
accumulator = 20 * 3 = 60, recurse with (n = 2, accumulator = 60). - For (n = 2):
accumulator = 60 * 2 = 120, recurse with (n = 1, accumulator = 120). - For (n = 1):
accumulator = 120 * 1 = 120, recurse with (n = 0, accumulator = 120). - For (n = 0): Base case, return
accumulator = 120.
- For (n = 5):
- Example Trace: For (n = 5), the accumulator builds: (1 \rightarrow 5 \rightarrow 20 \rightarrow 60 \rightarrow 120), returning 120.
- Stack Usage: The function is tail-recursive, but Java does not optimize tail recursion, so each call adds a stack frame, leading to O(n) stack usage and potential
StackOverflowErrorfor large (n).
- Step 1: The
- Iterative Approach:
- Step 1: The
iterativeFactorialmethod checks if (n < 0) or (n = 0). For (n = 5), it initializesresult = 1. - Step 2: Iterates from 1 to 5:
- (i = 1):
result = 1 * 1 = 1. - (i = 2):
result = 1 * 2 = 2. - (i = 3):
result = 2 * 3 = 6. - (i = 4):
result = 6 * 4 = 24. - (i = 5):
result = 24 * 5 = 120.
- (i = 1):
- Example Trace: For (n = 5), multiplies (1 * 2 * 3 * 4 * 5 = 120).
- Stack Usage: The iterative approach uses O(1) stack space, as it avoids recursive calls, making it immune to
StackOverflowError.
- Step 1: The
- Comparison: Both methods are O(n) time, but iterative is more space-efficient (O(1) vs. O(n)). For large inputs, the tail-recursive method may throw
StackOverflowError, while the iterative method is limited only bylongoverflow.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Recursive Call (Tail) | O(n) | O(n) |
| Iteration | O(n) | O(1) |
| Full Algorithm (Tail-Recursive) | O(n) | O(n) |
| Full Algorithm (Iterative) | O(n) | O(1) |
Note:
- n is the input integer.
- Time complexity: O(n) for both, as tail-recursive makes n calls, and iterative performs n multiplications.
- Space complexity: O(n) for tail-recursive due to the call stack (Java does not optimize tail recursion); O(1) for iterative, using only a single variable.
- Best, average, and worst cases are O(n) for both.
✅ Tip: Convert tail-recursive functions to iterative versions to eliminate stack overflow risks in Java. Test with large inputs (e.g., (n = 1000)) to observe stack overflow in the recursive version and verify correctness in the iterative version.
⚠ Warning: Java does not optimize tail recursion, so the O(n) space complexity may cause
StackOverflowErrorfor large inputs. Be cautious with inputs larger than 20, as factorial results may exceed thelongdata type’s capacity, causing overflow.
Reverse String Tail Recursion
Problem Statement
Write a Java program that implements a tail-recursive method to reverse a string using an accumulator to build the result. The program should take a string as input and return the reversed string. Test the method with various strings, including empty strings, single-character strings, and long strings, and discuss Java’s stack usage for the tail-recursive approach. You can visualize this as rearranging the letters of a word on a signboard, building the reversed word step-by-step by passing the accumulated result to the next recursive call.
Input: A string (e.g., str = "hello").
Output: A string representing the reversed input (e.g., "olleh").
Constraints:
- The string length is between 0 and 10^5.
- The string contains printable ASCII characters.
- The string may be empty. Example:
- Input:
str = "hello" - Output:
"olleh" - Explanation: The string
"hello"is reversed to"olleh"by rearranging its characters. - Input:
str = "" - Output:
"" - Explanation: An empty string remains empty after reversal.
Pseudocode
FUNCTION tailRecursiveReverse(str, index, accumulator)
IF str is null OR index < 0 THEN
RETURN accumulator
ENDIF
SET newAccumulator to character at index in str + accumulator
RETURN tailRecursiveReverse(str, index - 1, newAccumulator)
ENDFUNCTION
FUNCTION mainReverse(str)
IF str is null OR length of str equals 0 THEN
RETURN str
ENDIF
RETURN tailRecursiveReverse(str, length of str - 1, "")
ENDFUNCTION
Algorithm Steps
- Check if the input string is null or empty. If so, return the string as is.
- Define a tail-recursive helper function that takes the string, the current index, and an accumulator as parameters.
- In the helper function, implement the base case: if the index is less than 0, return the accumulator.
- For the recursive case, prepend the character at the current index to the accumulator and recursively call the function with the previous index and the updated accumulator.
- In the main function, call the tail-recursive function with the initial index set to the last index of the string (length - 1) and an empty string as the accumulator.
- Return the reversed string computed by the tail-recursive function.
Java Implementation
public class ReverseStringTailRecursion {
// Reverses a string using tail recursion
public String reverse(String str) {
// Check for null or empty string
if (str == null || str.length() == 0) {
return str;
}
// Call tail-recursive helper with initial index and accumulator
return tailRecursiveReverse(str, str.length() - 1, "");
}
// Helper function for tail-recursive string reversal
private String tailRecursiveReverse(String str, int index, String accumulator) {
// Base case: if index < 0, return accumulator
if (index < 0) {
return accumulator;
}
// Recursive case: prepend current character to accumulator and recurse
return tailRecursiveReverse(str, index - 1, str.charAt(index) + accumulator);
}
}
Output
For the input string "hello", the program outputs:
olleh
Explanation: The string "hello" is reversed to "olleh" by building the result in the accumulator.
For an empty string "", the program outputs:
""
Explanation: An empty string remains empty after reversal.
For a single-character string "a", the program outputs:
a
Explanation: A single-character string is already reversed.
How It Works
- Step 1: The
reversemethod checks if the string is null or empty. For"hello", it callstailRecursiveReversewithindex = 4andaccumulator = "". - Step 2: In
tailRecursiveReverse:- Index 4:
accumulator = 'o' + "" = "o", recurse withindex = 3,accumulator = "o". - Index 3:
accumulator = 'l' + "o" = "lo", recurse withindex = 2,accumulator = "lo". - Index 2:
accumulator = 'l' + "lo" = "llo", recurse withindex = 1,accumulator = "llo". - Index 1:
accumulator = 'e' + "llo" = "ello", recurse withindex = 0,accumulator = "ello". - Index 0:
accumulator = 'h' + "ello" = "hello", recurse withindex = -1,accumulator = "hello". - Index -1: Base case, return
accumulator = "hello".
- Index 4:
- Example Trace: For
"hello", the accumulator builds:"" → "o" → "lo" → "llo" → "ello" → "hello", returning"olleh". - Java Stack Usage: The function is tail-recursive because the recursive call is the last operation. However, Java does not optimize tail recursion, so each recursive call adds a frame to the call stack, leading to O(n) space complexity. This can cause stack overflow for very large strings (e.g., 10^5 characters). An iterative approach would use O(1) space (excluding the output string).
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Recursive Call (Tail) | O(n) | O(n) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the length of the input string.
- Time complexity: O(n), as the function processes each character exactly once.
- Space complexity: O(n) due to the recursive call stack (Java does not optimize tail recursion) and the accumulator string, which grows with each call.
- Best, average, and worst cases are O(n).
✅ Tip: Use tail recursion to write elegant recursive solutions for problems like string reversal, but consider iterative methods for better space efficiency in Java. Test with long strings (e.g., 10^4 characters) and edge cases like empty or single-character strings to verify correctness.
⚠ Warning: Java does not optimize tail recursion, so the O(n) space complexity may cause stack overflow for very large strings. Additionally, string concatenation in the accumulator can be inefficient; consider using
StringBuilderin an iterative approach for better performance.
Tail-Recursive List Length
Problem Statement
Write a Java program that computes the length of a singly linked list using a tail-recursive method with an accumulator to track the count of nodes. The program should also implement an iterative method to compute the length for comparison. Test both approaches with lists of varying sizes, including empty lists and single-node lists, and compare their readability and performance. You can visualize this as counting the number of links in a chain, either by recursively passing the count to the next link (tail-recursive) or by iteratively stepping through each link (iterative).
Input: A singly linked list with integer nodes (e.g., 1 -> 2 -> 3 -> null).
Output: An integer representing the number of nodes in the list (e.g., 3 for 1 -> 2 -> 3 -> null).
Constraints:
- The list contains 0 to 10^5 nodes.
- Node values are integers between -10^9 and 10^9.
- The list may be empty. Example:
- Input: Linked list
1 -> 2 -> 3 -> null - Output: 3
- Explanation: The list has 3 nodes.
- Input: Empty linked list
null - Output: 0
- Explanation: An empty list has no nodes.
Pseudocode
FUNCTION tailRecursiveLength(node, accumulator)
IF node is null THEN
RETURN accumulator
ENDIF
SET newAccumulator to accumulator + 1
RETURN tailRecursiveLength(node.next, newAccumulator)
ENDFUNCTION
FUNCTION iterativeLength(node)
SET count to 0
SET current to node
WHILE current is not null
INCREMENT count
SET current to current.next
ENDWHILE
RETURN count
ENDFUNCTION
FUNCTION mainLength(head)
CALL tailRecursiveLength(head, 0)
CALL iterativeLength(head)
RETURN results
ENDFUNCTION
Algorithm Steps
- For the tail-recursive approach: a. Define a tail-recursive helper function that takes the current node and an accumulator as parameters. b. Implement the base case: if the node is null, return the accumulator. c. For the recursive case, increment the accumulator by 1 and recursively call the function with the next node and the updated accumulator. d. Call the helper function with the head node and an initial accumulator of 0.
- For the iterative approach:
a. Initialize a counter to 0 and a pointer
currentto the head of the list. b. Whilecurrentis not null, increment the counter and movecurrentto the next node. c. Return the counter as the list length. - In the main function, call both the tail-recursive and iterative methods to compute the list length.
- Compare readability (recursive is more concise but abstract; iterative is more explicit) and performance (iterative is more space-efficient).
Java Implementation
public class TailRecursiveListLength {
// Node class for singly linked list
class Node {
int data;
Node next;
Node(int data) {
this.data = data;
this.next = null;
}
}
// Computes list length using tail recursion
public int tailRecursiveLength(Node head) {
// Call tail-recursive helper with initial accumulator
return tailRecursiveLengthHelper(head, 0);
}
// Helper function for tail-recursive length
private int tailRecursiveLengthHelper(Node node, int accumulator) {
// Base case: if node is null, return accumulator
if (node == null) {
return accumulator;
}
// Recursive case: increment accumulator and recurse
return tailRecursiveLengthHelper(node.next, accumulator + 1);
}
// Computes list length using iteration
public int iterativeLength(Node head) {
// Initialize counter and current pointer
int count = 0;
Node current = head;
// Iterate until null
while (current != null) {
count++;
current = current.next;
}
// Return final count
return count;
}
}
Output
For the input linked list 1 -> 2 -> 3 -> null, both methods output:
3
Explanation: The list has 3 nodes.
For an empty linked list null, both methods output:
0
Explanation: An empty list has no nodes.
For a single-node linked list 4 -> null, both methods output:
1
Explanation: The list has 1 node.
How It Works
- Tail-Recursive Approach:
- Step 1: The
tailRecursiveLengthmethod calls the helper withheadandaccumulator = 0. - Step 2: In
tailRecursiveLengthHelper:- For node
1:accumulator = 0 + 1 = 1, recurse withnode = 2,accumulator = 1. - For node
2:accumulator = 1 + 1 = 2, recurse withnode = 3,accumulator = 2. - For node
3:accumulator = 2 + 1 = 3, recurse withnode = null,accumulator = 3. - For
null: Base case, returnaccumulator = 3.
- For node
- Example Trace: For
1 -> 2 -> 3 -> null, the accumulator builds: 0 → 1 → 2 → 3, returning 3. - Tail Recursion Note: The function is tail-recursive because the recursive call is the last operation, though Java does not optimize tail recursion.
- Step 1: The
- Iterative Approach:
- Step 1: The
iterativeLengthmethod initializescount = 0,current = head. - Step 2: Iterates:
current = 1:count = 1, move to2.current = 2:count = 2, move to3.current = 3:count = 3, move tonull.current = null: Returncount = 3.
- Example Trace: For
1 -> 2 -> 3 -> null, counts nodes: 1 → 2 → 3, returning 3.
- Step 1: The
- Comparison:
- Readability: Tail-recursive is concise and elegant but may be less intuitive for beginners; iterative is straightforward and explicit.
- Performance: Both are O(n) time, but iterative uses O(1) space, while tail-recursive uses O(n) space due to the call stack in Java.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Recursive Call (Tail) | O(n) | O(n) |
| Iteration | O(n) | O(1) |
| Full Algorithm (Tail-Recursive) | O(n) | O(n) |
| Full Algorithm (Iterative) | O(n) | O(1) |
Note:
- n is the number of nodes in the linked list.
- Time complexity: O(n) for both, as each node is visited exactly once.
- Space complexity: O(n) for tail-recursive due to the call stack (Java does not optimize tail recursion); O(1) for iterative, using only a counter and pointer.
- Best, average, and worst cases are O(n) for both.
✅ Tip: Use tail recursion for elegant solutions to problems like list length, but prefer the iterative approach for large lists to save memory. Test with empty lists and long lists (e.g., 10^4 nodes) to compare performance and readability.
⚠ Warning: Java does not optimize tail recursion, so the O(n) space complexity may cause stack overflow for very large lists. Ensure the list is properly constructed to avoid null pointer exceptions.
Tail-Recursive Power
Problem Statement
Write a Java program that implements a tail-recursive method to compute (x^n) (x raised to the power n) using an accumulator to track the running product. The program should also include an iterative method for comparison. Test both methods with various inputs, including edge cases like (n = 0) and (n = 1), and compare their performance and readability. You can visualize this as calculating the total growth of an investment ((x)) compounded (n) times, either by iteratively multiplying or by recursively passing the accumulated product to the next step.
Input: A double (x) and a non-negative integer (n) (e.g., (x = 2.0, n = 3)). Output: A double representing (x^n) (e.g., 8.0 for (2.0^3)). Constraints:
- (-100.0 \leq x \leq 100.0).
- (0 \leq n \leq 10^3).
- The result should fit within a double’s precision. Example:
- Input: (x = 2.0, n = 3)
- Output: 8.0
- Explanation: (2.0^3 = 2.0 * 2.0 * 2.0 = 8.0).
- Input: (x = 5.0, n = 0)
- Output: 1.0
- Explanation: Any non-zero number raised to the power 0 is 1.
Pseudocode
FUNCTION tailRecursivePower(x, n, accumulator)
IF n equals 0 THEN
RETURN accumulator
ENDIF
SET newAccumulator to accumulator * x
RETURN tailRecursivePower(x, n - 1, newAccumulator)
ENDFUNCTION
FUNCTION iterativePower(x, n)
IF n equals 0 THEN
RETURN 1.0
ENDIF
SET result to 1.0
FOR i from 1 to n
SET result to result * x
ENDFOR
RETURN result
ENDFUNCTION
FUNCTION mainPower(x, n)
CALL tailRecursivePower(x, n, 1.0)
CALL iterativePower(x, n)
RETURN results
ENDFUNCTION
Algorithm Steps
- For the tail-recursive approach: a. Define a tail-recursive helper function that takes (x), (n), and an accumulator as parameters. b. Implement the base case: if (n = 0), return the accumulator. c. For the recursive case, multiply the accumulator by (x) and recursively call the function with (n - 1) and the updated accumulator. d. Call the helper function with the initial accumulator set to 1.0.
- For the iterative approach:
a. Check if (n = 0). If so, return 1.0.
b. Initialize a variable
resultto 1.0. c. Iterate (n) times, multiplyingresultby (x) in each iteration. d. Return the finalresult. - Test both methods with various inputs (e.g., (n = 0, 1, 5)) and compare performance and readability.
Java Implementation
public class TailRecursivePower {
// Computes x^n using tail recursion
public double tailRecursivePower(double x, int n) {
// Call tail-recursive helper with initial accumulator
return tailRecursivePowerHelper(x, n, 1.0);
}
// Helper function for tail-recursive power
private double tailRecursivePowerHelper(double x, int n, double accumulator) {
// Base case: x^0 = accumulator
if (n == 0) {
return accumulator;
}
// Recursive case: multiply accumulator by x and recurse
return tailRecursivePowerHelper(x, n - 1, accumulator * x);
}
// Computes x^n using iteration
public double iterativePower(double x, int n) {
// Base case: x^0 = 1
if (n == 0) {
return 1.0;
}
// Initialize result
double result = 1.0;
// Multiply x by itself n times
for (int i = 1; i <= n; i++) {
result *= x;
}
// Return final result
return result;
}
}
Output
For the input (x = 2.0, n = 3), both methods output:
8.0
Explanation: (2.0^3 = 2.0 * 2.0 * 2.0 = 8.0).
For the input (x = 5.0, n = 0), both methods output:
1.0
Explanation: Any non-zero number raised to the power 0 is 1.
For the input (x = 3.0, n = 2), both methods output:
9.0
Explanation: (3.0^2 = 3.0 * 3.0 = 9.0).
How It Works
- Tail-Recursive Approach:
- Step 1: The
tailRecursivePowermethod calls the helper with (x = 2.0, n = 3, accumulator = 1.0). - Step 2: In
tailRecursivePowerHelper:- For (n = 3):
accumulator = 1.0 * 2.0 = 2.0, recurse with (n = 2, accumulator = 2.0). - For (n = 2):
accumulator = 2.0 * 2.0 = 4.0, recurse with (n = 1, accumulator = 4.0). - For (n = 1):
accumulator = 4.0 * 2.0 = 8.0, recurse with (n = 0, accumulator = 8.0). - For (n = 0): Base case, return
accumulator = 8.0.
- For (n = 3):
- Example Trace: For (x = 2.0, n = 3), the accumulator builds: (1.0 \rightarrow 2.0 \rightarrow 4.0 \rightarrow 8.0), returning 8.0.
- Tail Recursion Note: The function is tail-recursive because the recursive call is the last operation, though Java does not optimize tail recursion.
- Step 1: The
- Iterative Approach:
- Step 1: The
iterativePowermethod checks if (n = 0). For (x = 2.0, n = 3), it initializesresult = 1.0. - Step 2: Iterates 3 times:
- Iteration 1:
result = 1.0 * 2.0 = 2.0. - Iteration 2:
result = 2.0 * 2.0 = 4.0. - Iteration 3:
result = 4.0 * 2.0 = 8.0.
- Iteration 1:
- Example Trace: For (x = 2.0, n = 3), multiplies (2.0 * 2.0 * 2.0 = 8.0).
- Step 1: The
- Comparison:
- Readability: Tail-recursive is concise and intuitive for recursive thinking but abstract; iterative is straightforward and explicit.
- Performance: Both are O(n) time, but iterative uses O(1) space, while tail-recursive uses O(n) space due to the call stack in Java.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Recursive Call (Tail) | O(n) | O(n) |
| Iteration | O(n) | O(1) |
| Full Algorithm (Tail-Recursive) | O(n) | O(n) |
| Full Algorithm (Iterative) | O(n) | O(1) |
Note:
- n is the exponent.
- Time complexity: O(n) for both, as tail-recursive makes n calls, and iterative performs n multiplications.
- Space complexity: O(n) for tail-recursive due to the call stack (Java does not optimize tail recursion); O(1) for iterative, using only a single variable.
- Best, average, and worst cases are O(n) for both.
✅ Tip: Use tail recursion for elegant recursive solutions, but prefer the iterative approach for large (n) to minimize memory usage. Test with edge cases like (n = 0, 1) and large (n) (e.g., 1000) to verify correctness and compare performance.
⚠ Warning: Java does not optimize tail recursion, so the O(n) space complexity may cause stack overflow for very large (n). Be cautious with floating-point precision for large (x) or (n), as results may exceed double’s capacity.
Tail-Recursive Sum
Problem Statement
Write a Java program that computes the sum of all elements in an array of integers using a tail-recursive function with an accumulator to track the running sum. The program should return the total sum and test the function with arrays of different sizes, including empty arrays and single-element arrays. You can visualize this as tallying the total score of a series of games, where each game’s score is added to a running total, passing the updated total to the next step recursively until all scores are processed.
Input: An array of integers (e.g., arr = [1, 2, 3, 4, 5]).
Output: A long integer representing the sum of all elements in the array (e.g., 15 for [1, 2, 3, 4, 5]).
Constraints:
- The array length is between 0 and 10^5.
- Elements are integers between -10^9 and 10^9.
- The array may be empty. Example:
- Input:
arr = [1, 2, 3, 4, 5] - Output: 15
- Explanation: The sum is calculated as 1 + 2 + 3 + 4 + 5 = 15.
- Input:
arr = [] - Output: 0
- Explanation: The sum of an empty array is 0.
Pseudocode
FUNCTION tailRecursiveSum(arr, index, accumulator)
IF arr is null OR index equals length of arr THEN
RETURN accumulator
ENDIF
SET newAccumulator to accumulator + arr[index]
RETURN tailRecursiveSum(arr, index + 1, newAccumulator)
ENDFUNCTION
FUNCTION mainSum(arr)
IF arr is null THEN
RETURN 0
ENDIF
RETURN tailRecursiveSum(arr, 0, 0)
ENDFUNCTION
Algorithm Steps
- Check if the input array is null. If so, return 0.
- Define a tail-recursive helper function that takes the array, the current index, and an accumulator as parameters.
- In the helper function, implement the base case: if the index equals the array length, return the accumulator.
- For the recursive case, add the current element to the accumulator and recursively call the function with the next index and the updated accumulator.
- In the main function, call the tail-recursive function with the initial index set to 0 and the accumulator set to 0.
- Return the final sum computed by the tail-recursive function.
Java Implementation
public class TailRecursiveSum {
// Computes sum of array elements using tail recursion
public long sum(int[] arr) {
// Check for null array
if (arr == null) {
return 0;
}
// Call tail-recursive helper with initial index and accumulator
return tailRecursiveSum(arr, 0, 0);
}
// Helper function for tail-recursive sum
private long tailRecursiveSum(int[] arr, int index, long accumulator) {
// Base case: if index reaches array length, return accumulator
if (index == arr.length) {
return accumulator;
}
// Recursive case: update accumulator and recurse
return tailRecursiveSum(arr, index + 1, accumulator + arr[index]);
}
}
Output
For the input array [1, 2, 3, 4, 5], the program outputs:
15
Explanation: The sum is computed as 1 + 2 + 3 + 4 + 5 = 15.
For an empty array [], the program outputs:
0
Explanation: The sum of an empty array is 0.
For a single-element array [7], the program outputs:
7
Explanation: The sum of a single-element array is the element itself.
How It Works
- Step 1: The
summethod checks if the array is null. For[1, 2, 3, 4, 5], it callstailRecursiveSumwithindex = 0andaccumulator = 0. - Step 2: In
tailRecursiveSum:- Index 0:
accumulator = 0 + 1 = 1, recurse withindex = 1,accumulator = 1. - Index 1:
accumulator = 1 + 2 = 3, recurse withindex = 2,accumulator = 3. - Index 2:
accumulator = 3 + 3 = 6, recurse withindex = 3,accumulator = 6. - Index 3:
accumulator = 6 + 4 = 10, recurse withindex = 4,accumulator = 10. - Index 4:
accumulator = 10 + 5 = 15, recurse withindex = 5,accumulator = 15. - Index 5: Base case, return
accumulator = 15.
- Index 0:
- Example Trace: For
[1, 2, 3, 4, 5], the accumulator builds: 0 → 1 → 3 → 6 → 10 → 15, returning 15. - Tail Recursion Note: The function is tail-recursive because the recursive call is the last operation, allowing potential optimization in languages that support tail-call optimization (though Java does not natively optimize tail recursion).
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Recursive Call | O(n) | O(n) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the size of the input array.
- Time complexity: O(n), as the function processes each element exactly once.
- Space complexity: O(n) due to the recursive call stack, as Java does not optimize tail recursion.
- Best, average, and worst cases are O(n), as all elements are processed.
✅ Tip: Use tail recursion with an accumulator to make recursive functions more intuitive by tracking state explicitly. Test with large arrays (e.g., 10^4 elements) and edge cases like empty or single-element arrays to ensure correctness.
⚠ Warning: Java does not optimize tail recursion, so the O(n) space complexity may lead to stack overflow for very large arrays. Consider iterative solutions for better space efficiency in such cases.
Array Problem Solving with DSA
🛠 What you will learn
This section outlines the format used for each DSA problem statement, designed to guide students through solving exercise problems in a clear, structured learning process. Each component serves a specific purpose to enhance understanding and practical application:
- Problem Statement: Clearly defines the task, including inputs, outputs, constraints, and an example. A real-world analogy makes the problem relatable, aligning with the engaging tone of the "Core Data Structures" summary, ensuring students grasp the problem’s context.
- Pseudocode: Provides a high-level, language-agnostic outline of the solution using standardized syntax (e.g.,
FUNCTION,IF,SET,RETURN). This helps students understand the logic before diving into code, bridging theory and implementation. - Algorithm Steps: Breaks down the pseudocode into detailed, actionable steps, connecting theoretical logic to practical coding. This ensures students can follow the implementation process systematically.
- Java Implementation: Offers a complete, commented Java code solution that students can study and run. It includes a
mainmethod with at least four test cases (e.g., target present, absent, middle element, and edge cases like duplicates or single elements) to verify correctness and explore different scenarios, maintaining a beginner-friendly approach. - Output: Shows the expected results from the test cases, including indices, comparisons, and execution times (where applicable), helping students verify their code’s correctness and understand the algorithm’s behavior.
- How It Works: Traces the algorithm’s execution with a detailed example, showing step-by-step how the search narrows down the range. This reinforces understanding by illustrating the algorithm in action.
- Complexity Analysis Table: Summarizes time and space complexity for best, average, and worst cases, teaching students to evaluate efficiency and compare trade-offs across implementations.
- Tip or Warning Box: Provides practical advice (e.g., when to use DSA) and highlights pitfalls (e.g., ensuring the array is sorted), guiding students toward best practices and common error avoidance.
Each exercise includes test cases to verify correctness and performance, aligning with a learning platform featuring embedded compilers for hands-on practice and visualizations to illustrate the halving process of DSA.
Array Reversal
Problem Statement
Write a Java program that reverses an array of integers in-place, meaning without using additional array storage beyond a few variables. The program should modify the input array such that the elements are arranged in reverse order and test the implementation with arrays of different sizes, including empty arrays and single-element arrays. You can visualize this as rearranging a row of numbered cards on a table by swapping pairs from the ends toward the center, without needing extra space to store a new row.
Input: An array of integers (e.g., arr = [1, 2, 3, 4, 5]).
Output: The same array modified in-place to contain the elements in reverse order (e.g., [5, 4, 3, 2, 1]).
Constraints:
- The array length is between 0 and 10^5.
- Elements are integers between -10^9 and 10^9.
- The array may be empty. Example:
- Input:
arr = [1, 2, 3, 4, 5] - Output:
[5, 4, 3, 2, 1] - Explanation: The array is reversed in-place by swapping elements from the ends toward the center.
- Input:
arr = [] - Output:
[] - Explanation: An empty array remains unchanged.
Pseudocode
FUNCTION reverseArray(arr)
IF arr is null OR length of arr <= 1 THEN
RETURN
ENDIF
SET left to 0
SET right to length of arr - 1
WHILE left < right
SET temp to arr[left]
SET arr[left] to arr[right]
SET arr[right] to temp
INCREMENT left
DECREMENT right
ENDWHILE
ENDFUNCTION
Algorithm Steps
- Check if the input array is null or has 0 or 1 element. If so, return, as no reversal is needed.
- Initialize two pointers:
leftat the start of the array (index 0) andrightat the end of the array (index length - 1). - While
leftis less thanright: a. Swap the elements at indicesleftandrightusing a temporary variable. b. Incrementleftand decrementrightto move the pointers inward. - Continue until
leftmeets or exceedsright, at which point the array is fully reversed. - Test the method with arrays of different sizes to verify correctness.
Java Implementation
public class ArrayReversal {
// Reverses an array in-place
public void reverseArray(int[] arr) {
// Check for null or single-element/empty array
if (arr == null || arr.length <= 1) {
return;
}
// Initialize pointers
int left = 0;
int right = arr.length - 1;
// Swap elements from ends toward center
while (left < right) {
// Swap using temporary variable
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
// Move pointers
left++;
right--;
}
}
}
Output
For the input array [1, 2, 3, 4, 5], the program modifies it to:
[5, 4, 3, 2, 1]
Explanation: The array is reversed in-place by swapping elements: (1 ↔ 5), (2 ↔ 4), leaving the middle element 3 in place.
For an empty array [], the program outputs:
[]
Explanation: An empty array remains unchanged.
For a single-element array [7], the program outputs:
[7]
Explanation: A single-element array is already reversed.
How It Works
- Step 1: The
reverseArraymethod checks if the array is null or has 0 or 1 element. For[1, 2, 3, 4, 5], it proceeds. - Step 2: Initialize
left = 0,right = 4. - Step 3: Iterate while
left < right:- First iteration: Swap
arr[0] = 1andarr[4] = 5, resulting in[5, 2, 3, 4, 1]. Setleft = 1,right = 3. - Second iteration: Swap
arr[1] = 2andarr[3] = 4, resulting in[5, 4, 3, 2, 1]. Setleft = 2,right = 2. - Stop:
left = 2 >= right = 2, so the loop ends.
- First iteration: Swap
- Example Trace: For
[1, 2, 3, 4, 5], swaps (1 ↔ 5), (2 ↔ 4), resulting in[5, 4, 3, 2, 1]. - In-Place Property: The algorithm uses only a single temporary variable, ensuring O(1) extra space regardless of array size.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Swapping | O(n/2) = O(n) | O(1) |
| Full Algorithm | O(n) | O(1) |
Note:
- n is the size of the input array.
- Time complexity: O(n), as the algorithm performs n/2 swaps, each taking constant time.
- Space complexity: O(1), as only a single temporary variable is used for swapping.
- Best, average, and worst cases are O(n), as all elements are processed up to the midpoint.
✅ Tip: Use two pointers to reverse arrays in-place for optimal space efficiency. Test with large arrays (e.g., 10^4 elements) and edge cases like empty or single-element arrays to ensure robustness.
⚠ Warning: Ensure the input array is not null to avoid
NullPointerException. Be cautious with very large arrays, as accessing invalid indices could occur if pointers are mismanaged.
Array Rotation
Problem Statement
Write a Java program that rotates an array of integers by k positions to the left. The rotation should shift each element k positions left, with elements at the beginning wrapping around to the end of the array. The program should modify the array in-place and test the implementation with different values of k and array sizes, including edge cases like empty arrays, single-element arrays, and k larger than the array length. You can visualize this as rotating a circular table of numbered plates k positions counterclockwise, so the first k plates move to the end while the rest shift forward.
Input: An array of integers and a non-negative integer k (e.g., arr = [1, 2, 3, 4, 5], k = 2).
Output: The array modified in-place to reflect the left rotation by k positions (e.g., [3, 4, 5, 1, 2]).
Constraints:
- The array length is between 0 and 10^5.
- Elements are integers between -10^9 and 10^9.
- k is a non-negative integer (0 ≤ k ≤ 10^9).
- The array may be empty. Example:
- Input:
arr = [1, 2, 3, 4, 5], k = 2 - Output:
[3, 4, 5, 1, 2] - Explanation: Rotating [1, 2, 3, 4, 5] left by 2 positions moves 1 and 2 to the end.
- Input:
arr = [1, 2, 3], k = 4 - Output:
[3, 1, 2] - Explanation: Rotating by k = 4 is equivalent to k = 1 (since 4 % 3 = 1).
Pseudocode
FUNCTION rotateArray(arr, k)
IF arr is null OR length of arr <= 1 THEN
RETURN
ENDIF
SET n to length of arr
SET k to k modulo n
IF k equals 0 THEN
RETURN
ENDIF
CALL reverseArray(arr, 0, k - 1)
CALL reverseArray(arr, k, n - 1)
CALL reverseArray(arr, 0, n - 1)
ENDFUNCTION
FUNCTION reverseArray(arr, start, end)
WHILE start < end
SET temp to arr[start]
SET arr[start] to arr[end]
SET arr[end] to temp
INCREMENT start
DECREMENT end
ENDWHILE
ENDFUNCTION
FUNCTION main()
SET testCases to array of tuples (arr, k)
FOR each (arr, k) in testCases
CALL rotateArray(arr, k)
PRINT arr
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input array is null or has 0 or 1 element. If so, return, as no rotation is needed.
- Compute the effective rotation value:
k = k % n, wherenis the array length, to handle cases where k exceeds n. - If k equals 0 after modulo, return, as no rotation is needed.
- Use the reversal algorithm to rotate the array in-place: a. Reverse the first k elements (indices 0 to k-1). b. Reverse the remaining elements (indices k to n-1). c. Reverse the entire array (indices 0 to n-1).
- The
reverseArrayhelper function swaps elements fromstarttoendusing a temporary variable. - In the
mainmethod, create test cases with different array sizes and k values, callrotateArray, and print the results.
Java Implementation
public class ArrayRotation {
// Rotates array left by k positions in-place
public void rotateArray(int[] arr, int k) {
// Check for null or single-element/empty array
if (arr == null || arr.length <= 1) {
return;
}
// Normalize k to handle cases where k > array length
int n = arr.length;
k = k % n;
if (k == 0) {
return;
}
// Perform three reversals
reverseArray(arr, 0, k - 1);
reverseArray(arr, k, n - 1);
reverseArray(arr, 0, n - 1);
}
// Helper function to reverse array segment from start to end
private void reverseArray(int[] arr, int start, int end) {
while (start < end) {
int temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
start++;
end--;
}
}
// Main method to test rotateArray with various inputs
public static void main(String[] args) {
ArrayRotation rotator = new ArrayRotation();
// Test case 1: Normal array, k = 2
int[] arr1 = {1, 2, 3, 4, 5};
System.out.print("Test case 1 before: ");
printArray(arr1);
rotator.rotateArray(arr1, 2);
System.out.print("Test case 1 after: ");
printArray(arr1);
// Test case 2: k > array length
int[] arr2 = {1, 2, 3};
System.out.print("Test case 2 before: ");
printArray(arr2);
rotator.rotateArray(arr2, 4);
System.out.print("Test case 2 after: ");
printArray(arr2);
// Test case 3: Empty array
int[] arr3 = {};
System.out.print("Test case 3 before: ");
printArray(arr3);
rotator.rotateArray(arr3, 3);
System.out.print("Test case 3 after: ");
printArray(arr3);
// Test case 4: Single-element array
int[] arr4 = {7};
System.out.print("Test case 4 before: ");
printArray(arr4);
rotator.rotateArray(arr4, 2);
System.out.print("Test case 4 after: ");
printArray(arr4);
// Test case 5: k = 0
int[] arr5 = {1, 2, 3, 4};
System.out.print("Test case 5 before: ");
printArray(arr5);
rotator.rotateArray(arr5, 0);
System.out.print("Test case 5 after: ");
printArray(arr5);
}
// Helper method to print array
private static void printArray(int[] arr) {
if (arr == null || arr.length == 0) {
System.out.println("[]");
return;
}
System.out.print("[");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
if (i < arr.length - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
}
Output
Running the main method produces:
Test case 1 before: [1, 2, 3, 4, 5]
Test case 1 after: [3, 4, 5, 1, 2]
Test case 2 before: [1, 2, 3]
Test case 2 after: [3, 1, 2]
Test case 3 before: []
Test case 3 after: []
Test case 4 before: [7]
Test case 4 after: [7]
Test case 5 before: [1, 2, 3, 4]
Test case 5 after: [1, 2, 3, 4]
Explanation:
- Test case 1: Rotates
[1, 2, 3, 4, 5]left by 2, resulting in[3, 4, 5, 1, 2]. - Test case 2: Rotates
[1, 2, 3]by k = 4, equivalent to k = 1 (4 % 3), resulting in[3, 1, 2]. - Test case 3: Empty array remains
[]. - Test case 4: Single-element array
[7]remains unchanged. - Test case 5: k = 0 leaves
[1, 2, 3, 4]unchanged.
How It Works
- Step 1: The
rotateArraymethod checks if the array is null or has 0 or 1 element. For[1, 2, 3, 4, 5], k = 2, it proceeds. - Step 2: Normalize k:
n = 5,k = 2 % 5 = 2. - Step 3: Perform three reversals:
- Reverse indices 0 to 1:
[2, 1, 3, 4, 5]. - Reverse indices 2 to 4:
[2, 1, 5, 4, 3]. - Reverse entire array:
[3, 4, 5, 1, 2].
- Reverse indices 0 to 1:
- Example Trace: For
[1, 2, 3, 4, 5], k = 2, reverses [1, 2] to [2, 1], [3, 4, 5] to [5, 4, 3], then [2, 1, 5, 4, 3] to [3, 4, 5, 1, 2]. - Main Method: Tests the method with various cases: normal rotation, k > n, empty array, single-element array, and k = 0.
- In-Place Property: The algorithm uses O(1) extra space by performing swaps via the
reverseArrayhelper.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Reverse | O(n) | O(1) |
| Full Algorithm | O(n) | O(1) |
Note:
- n is the size of the input array.
- Time complexity: O(n), as the algorithm performs three reversals, each taking O(n/2), O(n-k), and O(n) respectively, totaling O(n).
- Space complexity: O(1), as only a few temporary variables are used for swapping.
- Best, average, and worst cases are O(n).
✅ Tip: Normalize k using modulo to handle large k values efficiently. Test with edge cases like k = 0, k > n, and empty arrays to ensure robustness.
⚠ Warning: Ensure the input array is not null to avoid
NullPointerException. Verify index bounds in the reverse function to preventArrayIndexOutOfBoundsException.
Array Sorting
Problem Statement
Write a Java program that implements the bubble sort algorithm to sort an array of integers in ascending order. The program should modify the input array in-place to arrange its elements from smallest to largest and test the implementation with various input arrays, including empty arrays, single-element arrays, and arrays with duplicate or negative numbers. You can visualize this as organizing a row of books on a shelf by repeatedly comparing and swapping adjacent books to ensure they are in order by size.
Input: An array of integers (e.g., arr = [64, 34, 25, 12, 22]).
Output: The array modified in-place to be sorted in ascending order (e.g., [12, 22, 25, 34, 64]).
Constraints:
- The array length is between 0 and 10^5.
- Elements are integers between -10^9 and 10^9.
- The array may be empty. Example:
- Input:
arr = [64, 34, 25, 12, 22] - Output:
[12, 22, 25, 34, 64] - Explanation: The array is sorted in ascending order using bubble sort.
- Input:
arr = [1, 1, 1] - Output:
[1, 1, 1] - Explanation: The array with duplicate elements remains sorted.
Pseudocode
FUNCTION bubbleSort(arr)
IF arr is null OR length of arr <= 1 THEN
RETURN
ENDIF
SET n to length of arr
FOR i from 0 to n - 1
FOR j from 0 to n - i - 2
IF arr[j] > arr[j + 1] THEN
SET temp to arr[j]
SET arr[j] to arr[j + 1]
SET arr[j + 1] to temp
ENDIF
ENDFOR
ENDFOR
ENDFUNCTION
FUNCTION main()
SET testArrays to arrays of integers
FOR each array in testArrays
CALL bubbleSort(array)
PRINT array
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input array is null or has 0 or 1 element. If so, return, as no sorting is needed.
- Initialize
nas the length of the array. - For each index
ifrom 0 ton - 1: a. Iterate through indicesjfrom 0 ton - i - 2. b. Ifarr[j] > arr[j + 1], swap the elements at indicesjandj + 1using a temporary variable. - Repeat until no more swaps are needed, indicating the array is sorted.
- In the
mainmethod, create test arrays with various cases (e.g., unsorted, sorted, duplicates, negative numbers) and callbubbleSortto verify correctness.
Java Implementation
public class ArraySorting {
// Sorts array in ascending order using bubble sort
public void bubbleSort(int[] arr) {
// Check for null or single-element/empty array
if (arr == null || arr.length <= 1) {
return;
}
// Get array length
int n = arr.length;
// Outer loop for passes
for (int i = 0; i < n; i++) {
// Inner loop for comparisons and swaps
for (int j = 0; j < n - i - 1; j++) {
// Compare adjacent elements
if (arr[j] > arr[j + 1]) {
// Swap elements
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// Main method to test bubbleSort with various arrays
public static void main(String[] args) {
ArraySorting sorter = new ArraySorting();
// Test case 1: Unsorted array
int[] arr1 = {64, 34, 25, 12, 22};
System.out.print("Test case 1 before: ");
printArray(arr1);
sorter.bubbleSort(arr1);
System.out.print("Test case 1 after: ");
printArray(arr1);
// Test case 2: Already sorted array
int[] arr2 = {1, 2, 3, 4, 5};
System.out.print("Test case 2 before: ");
printArray(arr2);
sorter.bubbleSort(arr2);
System.out.print("Test case 2 after: ");
printArray(arr2);
// Test case 3: Array with duplicates
int[] arr3 = {3, 1, 3, 2, 1};
System.out.print("Test case 3 before: ");
printArray(arr3);
sorter.bubbleSort(arr3);
System.out.print("Test case 3 after: ");
printArray(arr3);
// Test case 4: Array with negative numbers
int[] arr4 = {-5, 0, -2, 3, -1};
System.out.print("Test case 4 before: ");
printArray(arr4);
sorter.bubbleSort(arr4);
System.out.print("Test case 4 after: ");
printArray(arr4);
// Test case 5: Empty array
int[] arr5 = {};
System.out.print("Test case 5 before: ");
printArray(arr5);
sorter.bubbleSort(arr5);
System.out.print("Test case 5 after: ");
printArray(arr5);
// Test case 6: Single-element array
int[] arr6 = {42};
System.out.print("Test case 6 before: ");
printArray(arr6);
sorter.bubbleSort(arr6);
System.out.print("Test case 6 after: ");
printArray(arr6);
}
// Helper method to print array
private static void printArray(int[] arr) {
if (arr == null || arr.length == 0) {
System.out.println("[]");
return;
}
System.out.print("[");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
if (i < arr.length - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
}
Output
Running the main method produces:
Test case 1 before: [64, 34, 25, 12, 22]
Test case 1 after: [12, 22, 25, 34, 64]
Test case 2 before: [1, 2, 3, 4, 5]
Test case 2 after: [1, 2, 3, 4, 5]
Test case 3 before: [3, 1, 3, 2, 1]
Test case 3 after: [1, 1, 2, 3, 3]
Test case 4 before: [-5, 0, -2, 3, -1]
Test case 4 after: [-5, -2, -1, 0, 3]
Test case 5 before: []
Test case 5 after: []
Test case 6 before: [42]
Test case 6 after: [42]
Explanation:
- Test case 1: Sorts
[64, 34, 25, 12, 22]to[12, 22, 25, 34, 64]. - Test case 2: Already sorted
[1, 2, 3, 4, 5]remains unchanged. - Test case 3: Sorts
[3, 1, 3, 2, 1]to[1, 1, 2, 3, 3], handling duplicates. - Test case 4: Sorts
[-5, 0, -2, 3, -1]to[-5, -2, -1, 0, 3], handling negatives. - Test case 5: Empty array
[]remains unchanged. - Test case 6: Single-element array
[42]remains unchanged.
How It Works
- Step 1: The
bubbleSortmethod checks if the array is null or has 0 or 1 element. For[64, 34, 25, 12, 22], it proceeds. - Step 2: For
n = 5, perform 5 passes:- Pass 1: Compare and swap adjacent elements:
[64, 34] → [34, 64],[64, 25] → [25, 64],[64, 12] → [12, 64],[64, 22] → [22, 64]. Result:[34, 25, 12, 22, 64]. - Pass 2:
[34, 25] → [25, 34],[34, 12] → [12, 34],[34, 22] → [22, 34]. Result:[25, 12, 22, 34, 64]. - Pass 3:
[25, 12] → [12, 25],[25, 22] → [22, 25]. Result:[12, 22, 25, 34, 64]. - Pass 4: No swaps needed, array is sorted.
- Pass 5: No swaps needed, done.
- Pass 1: Compare and swap adjacent elements:
- Example Trace: For
[64, 34, 25, 12, 22], sorts to[12, 22, 25, 34, 64]by bubbling larger elements to the end. - Main Method: Tests the method with unsorted, sorted, duplicate, negative, empty, and single-element arrays, printing results.
- In-Place Property: Bubble sort modifies the array in-place, using only a temporary variable for swaps.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Sorting | O(n^2) | O(1) |
| Full Algorithm | O(n^2) | O(1) |
Note:
- n is the size of the input array.
- Time complexity: O(n^2) in the worst and average cases, as the algorithm performs up to n passes, each with up to n-1 comparisons. Best case is O(n) when the array is already sorted (no swaps).
- Space complexity: O(1), as only a temporary variable is used for swapping.
- Worst case: O(n^2) for reverse-sorted arrays. Best case: O(n) for sorted arrays.
✅ Tip: Bubble sort is simple but inefficient for large arrays. Consider adding a flag to detect if no swaps occur in a pass to optimize for nearly sorted arrays. Test with various cases to ensure correctness.
⚠ Warning: Ensure the input array is not null to avoid
NullPointerException. Be cautious with very large arrays, as O(n^2) time complexity can lead to slow performance.
Duplicate Finder
Problem Statement
Write a Java program that implements a method to check if an array of integers contains any duplicate elements. The method should return true if there are duplicates and false otherwise. Test the method with both sorted and unsorted arrays, including edge cases like empty arrays, single-element arrays, and arrays with multiple duplicates. You can visualize this as checking a list of student IDs to determine if any ID appears more than once, whether the list is sorted or unsorted.
Input: An array of integers (e.g., arr = [1, 2, 3, 1] or arr = [1, 1, 2, 3]).
Output: A boolean indicating whether the array contains duplicate elements (e.g., true for [1, 2, 3, 1], false for [1, 2, 3, 4]).
Constraints:
- The array length is between 0 and 10^5.
- Elements are integers between -10^9 and 10^9.
- The array may be empty. Example:
- Input:
arr = [1, 2, 3, 1] - Output: true
- Explanation: The element 1 appears twice, so the array contains duplicates.
- Input:
arr = [1, 2, 3, 4] - Output: false
- Explanation: All elements are unique.
Pseudocode
FUNCTION hasDuplicates(arr)
IF arr is null OR length of arr <= 1 THEN
RETURN false
ENDIF
CREATE empty HashSet seen
FOR each element in arr
IF element exists in seen THEN
RETURN true
ELSE
ADD element to seen
ENDIF
ENDFOR
RETURN false
ENDFUNCTION
FUNCTION main()
SET testArrays to arrays of integers
FOR each array in testArrays
CALL hasDuplicates(array)
PRINT result
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input array is null or has 0 or 1 element. If so, return false, as duplicates are not possible.
- Create an empty HashSet to store seen elements.
- Iterate through the array: a. For each element, check if it exists in the HashSet. b. If the element is in the HashSet, return true, as a duplicate has been found. c. Otherwise, add the element to the HashSet.
- If the loop completes without finding duplicates, return false.
- In the
mainmethod, create test arrays (sorted and unsorted) with various cases and call thehasDuplicatesmethod to verify correctness.
Java Implementation
import java.util.HashSet;
public class DuplicateFinder {
// Checks if array contains duplicate elements
public boolean hasDuplicates(int[] arr) {
// Check for null or single-element/empty array
if (arr == null || arr.length <= 1) {
return false;
}
// Use HashSet to track seen elements
HashSet<Integer> seen = new HashSet<>();
// Iterate through array
for (int num : arr) {
// If element is already in set, duplicate found
if (seen.contains(num)) {
return true;
}
// Add element to set
seen.add(num);
}
// No duplicates found
return false;
}
// Main method to test hasDuplicates with various arrays
public static void main(String[] args) {
DuplicateFinder finder = new DuplicateFinder();
// Test case 1: Unsorted array with duplicates
int[] arr1 = {1, 2, 3, 1};
System.out.println("Test case 1 (unsorted with duplicates): " + finder.hasDuplicates(arr1));
// Test case 2: Sorted array with duplicates
int[] arr2 = {1, 1, 2, 3};
System.out.println("Test case 2 (sorted with duplicates): " + finder.hasDuplicates(arr2));
// Test case 3: Unsorted array without duplicates
int[] arr3 = {1, 2, 3, 4};
System.out.println("Test case 3 (unsorted without duplicates): " + finder.hasDuplicates(arr3));
// Test case 4: Empty array
int[] arr4 = {};
System.out.println("Test case 4 (empty): " + finder.hasDuplicates(arr4));
// Test case 5: Single-element array
int[] arr5 = {5};
System.out.println("Test case 5 (single element): " + finder.hasDuplicates(arr5));
// Test case 6: Array with negative numbers and duplicates
int[] arr6 = {-1, 2, -1, 3};
System.out.println("Test case 6 (negative numbers with duplicates): " + finder.hasDuplicates(arr6));
}
}
Output
Running the main method produces:
Test case 1 (unsorted with duplicates): true
Test case 2 (sorted with duplicates): true
Test case 3 (unsorted without duplicates): false
Test case 4 (empty): false
Test case 5 (single element): false
Test case 6 (negative numbers with duplicates): true
Explanation:
- Test case 1:
[1, 2, 3, 1]returns true (duplicate 1). - Test case 2:
[1, 1, 2, 3]returns true (duplicate 1). - Test case 3:
[1, 2, 3, 4]returns false (no duplicates). - Test case 4:
[]returns false (empty array). - Test case 5:
[5]returns false (single element). - Test case 6:
[-1, 2, -1, 3]returns true (duplicate -1).
How It Works
- Step 1: The
hasDuplicatesmethod checks if the array is null or has 0 or 1 element. For[1, 2, 3, 1], it proceeds. - Step 2: Initialize an empty HashSet
seen. - Step 3: Iterate through
[1, 2, 3, 1]:- Element 1: Not in
seen, add 1 toseen. - Element 2: Not in
seen, add 2 toseen. - Element 3: Not in
seen, add 3 toseen. - Element 1: In
seen, return true.
- Element 1: Not in
- Example Trace: For
[1, 2, 3, 1],seengrows:{1} → {1, 2} → {1, 2, 3}, then detects duplicate 1, returning true. - Main Method: Tests the method with sorted and unsorted arrays, including duplicates, no duplicates, empty, single-element, and negative numbers.
- HashSet Efficiency: The HashSet provides O(1) average-time lookups and insertions, making the algorithm efficient for both sorted and unsorted arrays.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Iteration | O(n) | O(n) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the size of the input array.
- Time complexity: O(n), as the algorithm iterates through the array once, with O(1) average-time HashSet operations.
- Space complexity: O(n), as the HashSet may store up to n elements in the worst case.
- Best, average, and worst cases are O(n) for time; space depends on the number of unique elements.
✅ Tip: Use a HashSet for efficient duplicate detection in both sorted and unsorted arrays. Test with edge cases like empty arrays, single-element arrays, and arrays with negative numbers to ensure robustness.
⚠ Warning: Ensure the input array is not null to avoid
NullPointerException. Be mindful of memory usage for very large arrays, as the HashSet requires O(n) space.
Maximum Element
Problem Statement
Write a Java program that implements a method to find the maximum element in an array of integers. The program should return the largest element in the array and test the method with arrays containing positive and negative numbers, including edge cases like single-element arrays and arrays with duplicate maximum values. You can visualize this as searching through a list of exam scores to find the highest score, ensuring the method works whether the scores are positive, negative, or a mix of both.
Input: An array of integers (e.g., arr = [3, -1, 5, 2, -7]).
Output: An integer representing the maximum element in the array (e.g., 5 for [3, -1, 5, 2, -7]).
Constraints:
- The array length is between 1 and 10^5.
- Elements are integers between -10^9 and 10^9.
- The array is guaranteed to have at least one element. Example:
- Input:
arr = [3, -1, 5, 2, -7] - Output: 5
- Explanation: The maximum element in the array is 5.
- Input:
arr = [-2, -5, -1, -8] - Output: -1
- Explanation: The maximum element in the array is -1.
Pseudocode
FUNCTION findMax(arr)
IF arr is null OR length of arr equals 0 THEN
RETURN null
ENDIF
SET max to arr[0]
FOR i from 1 to length of arr - 1
IF arr[i] > max THEN
SET max to arr[i]
ENDIF
ENDFOR
RETURN max
ENDFUNCTION
FUNCTION main()
SET testArrays to arrays of integers
FOR each array in testArrays
CALL findMax(array)
PRINT result
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input array is null or empty. If so, return null (or throw an exception, depending on requirements).
- Initialize a variable
maxto the first element of the array. - Iterate through the array from index 1 to the last index.
- For each element, compare it with
max. If the element is greater thanmax, updatemaxto the element’s value. - After the loop, return
maxas the maximum element. - In the
mainmethod, create test arrays with positive and negative numbers and call thefindMaxmethod to verify correctness.
Java Implementation
public class MaximumElement {
// Finds the maximum element in an array
public Integer findMax(int[] arr) {
// Check for null or empty array
if (arr == null || arr.length == 0) {
return null;
}
// Initialize max to first element
int max = arr[0];
// Iterate through array to find maximum
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
// Main method to test findMax with various arrays
public static void main(String[] args) {
MaximumElement maxElement = new MaximumElement();
// Test case 1: Mixed positive and negative numbers
int[] arr1 = {3, -1, 5, 2, -7};
System.out.println("Maximum element in arr1: " + maxElement.findMax(arr1));
// Test case 2: All negative numbers
int[] arr2 = {-2, -5, -1, -8};
System.out.println("Maximum element in arr2: " + maxElement.findMax(arr2));
// Test case 3: Single element
int[] arr3 = {42};
System.out.println("Maximum element in arr3: " + maxElement.findMax(arr3));
// Test case 4: Duplicate maximum values
int[] arr4 = {4, 4, 3, 4, 2};
System.out.println("Maximum element in arr4: " + maxElement.findMax(arr4));
// Test case 5: Null array
int[] arr5 = null;
System.out.println("Maximum element in arr5: " + maxElement.findMax(arr5));
}
}
Output
Running the main method produces:
Maximum element in arr1: 5
Maximum element in arr2: -1
Maximum element in arr3: 42
Maximum element in arr4: 4
Maximum element in arr5: null
Explanation:
- For
arr1 = [3, -1, 5, 2, -7], the maximum is 5. - For
arr2 = [-2, -5, -1, -8], the maximum is -1 (largest among negatives). - For
arr3 = [42], the maximum is 42 (single element). - For
arr4 = [4, 4, 3, 4, 2], the maximum is 4 (handles duplicates). - For
arr5 = null, the method returns null.
How It Works
- Step 1: The
findMaxmethod checks if the array is null or empty. For[3, -1, 5, 2, -7], it proceeds. - Step 2: Initialize
max = arr[0] = 3. - Step 3: Iterate from index 1 to 4:
- Index 1:
arr[1] = -1,-1 < 3, somaxremains 3. - Index 2:
arr[2] = 5,5 > 3, somax = 5. - Index 3:
arr[3] = 2,2 < 5, somaxremains 5. - Index 4:
arr[4] = -7,-7 < 5, somaxremains 5.
- Index 1:
- Step 4: Return
max = 5. - Example Trace: For
[3, -1, 5, 2, -7],maxupdates:3 → 5, returning 5. - Main Method: The
mainmethod tests thefindMaxmethod with various arrays, including mixed numbers, all negatives, single-element, duplicates, and null, printing the results.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Iteration | O(n) | O(1) |
| Full Algorithm | O(n) | O(1) |
Note:
- n is the size of the input array.
- Time complexity: O(n), as the algorithm iterates through the array once, comparing each element.
- Space complexity: O(1), as only a single variable (
max) is used, regardless of array size. - Best, average, and worst cases are O(n), as all elements are processed.
✅ Tip: Initialize the maximum to the first element to handle arrays with negative numbers efficiently. Test with edge cases like single-element arrays, all-negative arrays, and arrays with duplicates to ensure robustness.
⚠ Warning: Ensure the input array is not null or empty to avoid
NullPointerExceptionor unexpected behavior. For very large arrays, ensure elements are within the integer range to avoid overflow issues in comparisons.
3D Array Summation
Problem Statement
Write a Java program that initializes a 3D array of integers and computes the sum of all elements using nested loops. The program should return the total sum and test the implementation with 3D arrays of different dimensions, including edge cases like single-element arrays and arrays with varying sizes for each dimension. You can visualize this as calculating the total value of items stored in a 3D grid, such as boxes stacked in a warehouse with layers, rows, and columns, by adding up every item’s value.
Input: A 3D array of integers with dimensions d × r × c (e.g., array = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}).
Output: A long integer representing the sum of all elements in the 3D array (e.g., 36 for the example above).
Constraints:
- 1 ≤ d, r, c ≤ 100 (where d is depth, r is rows, c is columns).
- Elements are integers between -10^4 and 10^4.
- The array is guaranteed to be non-empty and well-formed (all sub-arrays have consistent dimensions). Example:
- Input:
array = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}} - Output: 36
- Explanation: Sum = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 = 36.
- Input:
array = {{{1}}} - Output: 1
- Explanation: The single-element 3D array has sum 1.
Pseudocode
FUNCTION sum3DArray(array)
IF array is null OR array is empty OR array[0] is empty OR array[0][0] is empty THEN
RETURN 0
ENDIF
SET depth to number of layers in array
SET rows to number of rows in array[0]
SET cols to number of columns in array[0][0]
SET sum to 0
FOR i from 0 to depth - 1
FOR j from 0 to rows - 1
FOR k from 0 to cols - 1
SET sum to sum + array[i][j][k]
ENDFOR
ENDFOR
ENDFOR
RETURN sum
ENDFUNCTION
FUNCTION main()
SET testArrays to 3D arrays with different dimensions
FOR each array in testArrays
CALL sum3DArray(array)
PRINT result
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input 3D array is null, empty, or has empty sub-arrays. If so, return 0.
- Determine the dimensions:
depth(d),rows(r), andcolumns(c) of the 3D array. - Initialize a variable
sumto 0. - Use three nested loops to iterate through each element
array[i][j][k]: a. Add the element tosum. - Return the final
sum. - In the
mainmethod, create test 3D arrays with different dimensions and callsum3DArrayto verify correctness.
Java Implementation
public class ThreeDArraySummation {
// Computes the sum of all elements in a 3D array
public long sum3DArray(int[][][] array) {
// Check for null or empty array
if (array == null || array.length == 0 || array[0].length == 0 || array[0][0].length == 0) {
return 0;
}
// Get dimensions
int depth = array.length;
int rows = array[0].length;
int cols = array[0][0].length;
// Initialize sum
long sum = 0;
// Iterate through all elements
for (int i = 0; i < depth; i++) {
for (int j = 0; j < rows; j++) {
for (int k = 0; k < cols; k++) {
sum += array[i][j][k];
}
}
}
return sum;
}
// Main method to test sum3DArray with various inputs
public static void main(String[] args) {
ThreeDArraySummation summer = new ThreeDArraySummation();
// Test case 1: 2x2x2 3D array
int[][][] array1 = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}};
System.out.println("Test case 1:");
System.out.println("Array:");
print3DArray(array1);
System.out.println("Sum: " + summer.sum3DArray(array1));
// Test case 2: 1x1x1 single-element array
int[][][] array2 = {{{1}}};
System.out.println("Test case 2:");
System.out.println("Array:");
print3DArray(array2);
System.out.println("Sum: " + summer.sum3DArray(array2));
// Test case 3: 2x3x2 non-uniform dimensions
int[][][] array3 = {{{1, 2}, {3, 4}, {5, 6}}, {{7, 8}, {9, 10}, {11, 12}}};
System.out.println("Test case 3:");
System.out.println("Array:");
print3DArray(array3);
System.out.println("Sum: " + summer.sum3DArray(array3));
// Test case 4: 1x2x3 array with negative numbers
int[][][] array4 = {{{-1, -2, -3}, {-4, -5, -6}}};
System.out.println("Test case 4:");
System.out.println("Array:");
print3DArray(array4);
System.out.println("Sum: " + summer.sum3DArray(array4));
// Test case 5: Empty array
int[][][] array5 = {};
System.out.println("Test case 5:");
System.out.println("Array:");
print3DArray(array5);
System.out.println("Sum: " + summer.sum3DArray(array5));
}
// Helper method to print 3D array
private static void print3DArray(int[][][] array) {
if (array == null || array.length == 0) {
System.out.println("null");
return;
}
System.out.println("[");
for (int i = 0; i < array.length; i++) {
System.out.println(" [");
for (int j = 0; j < array[i].length; j++) {
System.out.print(" [");
for (int k = 0; k < array[i][j].length; k++) {
System.out.print(array[i][j][k]);
if (k < array[i][j].length - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
System.out.println(" ]");
}
System.out.println("]");
}
}
Output
Running the main method produces:
Test case 1:
Array:
[
[
[1, 2]
[3, 4]
]
[
[5, 6]
[7, 8]
]
]
Sum: 36
Test case 2:
Array:
[
[
[1]
]
]
Sum: 1
Test case 3:
Array:
[
[
[1, 2]
[3, 4]
[5, 6]
]
[
[7, 8]
[9, 10]
[11, 12]
]
]
Sum: 78
Test case 4:
Array:
[
[
[-1, -2, -3]
[-4, -5, -6]
]
]
Sum: -21
Test case 5:
Array:
null
Sum: 0
Explanation:
- Test case 1: Sums a 2×2×2 array: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 = 36.
- Test case 2: Sums a 1×1×1 array: 1 = 1.
- Test case 3: Sums a 2×3×2 array: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 = 78.
- Test case 4: Sums a 1×2×3 array with negatives: -1 + (-2) + (-3) + (-4) + (-5) + (-6) = -21.
- Test case 5: Returns 0 for an empty array.
How It Works
- Step 1: The
sum3DArraymethod checks for null or empty arrays. For{{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}, it proceeds. - Step 2: Get dimensions:
depth = 2,rows = 2,cols = 2. - Step 3: Initialize
sum = 0. - Step 4: Iterate through all elements:
- Layer 0:
[1, 2]→ sum = 0 + 1 + 2 = 3;[3, 4]→ sum = 3 + 3 + 4 = 10. - Layer 1:
[5, 6]→ sum = 10 + 5 + 6 = 21;[7, 8]→ sum = 21 + 7 + 8 = 36.
- Layer 0:
- Example Trace: For test case 1, accumulates: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 = 36.
- Main Method: Tests with different dimensions (2×2×2, 1×1×1, 2×3×2, 1×2×3) and an empty array, printing inputs and sums.
- Summation Property: Uses nested loops to access each element, ensuring all are included in the sum.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Summation | O(drc) | O(1) |
| Full Algorithm | O(drc) | O(1) |
Note:
- d is the depth, r is the number of rows, c is the number of columns.
- Time complexity: O(drc), as the algorithm iterates through each element once.
- Space complexity: O(1), as only a single
sumvariable is used (excluding input/output). - Best, average, and worst cases are O(drc).
✅ Tip: Use nested loops for straightforward 3D array traversal. Test with varying dimensions and negative numbers to ensure correctness, especially for edge cases like single-element arrays.
⚠ Warning: Ensure the 3D array is well-formed (consistent dimensions across layers, rows, and columns) to avoid
NullPointerExceptionorArrayIndexOutOfBoundsException. Use alongfor the sum to handle large arrays within the given constraints.
Diagonal Elements
Problem Statement
Write a Java program that implements a method to extract the main diagonal elements of a square 2D array (matrix) into a 1D array. The main diagonal consists of elements where the row index equals the column index (i.e., matrix[i][i]). The program should return a 1D array containing these elements and test the implementation with various square matrices, including edge cases like 1×1 matrices and matrices with negative numbers. You can visualize this as collecting the numbers along the top-left to bottom-right diagonal of a square grid, like picking out the main diagonal pieces on a chessboard.
Input: A square 2D array (matrix) of integers with dimensions n × n (e.g., matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]).
Output: A 1D array containing the main diagonal elements (e.g., [1, 5, 9]).
Constraints:
- 1 ≤ n ≤ 100 (where n is the number of rows and columns).
- Elements are integers between -10^9 and 10^9.
- The matrix is guaranteed to be square (number of rows equals number of columns) and non-empty. Example:
- Input:
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] - Output:
[1, 5, 9] - Explanation: The main diagonal elements are matrix[0][0] = 1, matrix[1][1] = 5, matrix[2][2] = 9.
- Input:
matrix = [[5]] - Output:
[5] - Explanation: The 1×1 matrix has a single diagonal element, 5.
Pseudocode
FUNCTION getMainDiagonal(matrix)
IF matrix is null OR matrix is empty OR matrix[0] is empty OR rows not equal to columns THEN
RETURN null
ENDIF
SET n to number of rows in matrix
CREATE result array of size n
FOR i from 0 to n - 1
SET result[i] to matrix[i][i]
ENDFOR
RETURN result
ENDFUNCTION
FUNCTION main()
SET testMatrices to square 2D arrays
FOR each matrix in testMatrices
CALL getMainDiagonal(matrix)
PRINT result
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input matrix is null, empty, or not square (rows ≠ columns). If so, return null.
- Get the size of the matrix:
n(number of rows, equal to columns since square). - Create a 1D result array of size n.
- Iterate through indices i from 0 to n-1:
a. Set
result[i]tomatrix[i][i](the main diagonal element). - Return the result array.
- In the
mainmethod, create test square matrices of different sizes and callgetMainDiagonalto verify correctness.
Java Implementation
public class DiagonalElements {
// Extracts main diagonal elements into a 1D array
public int[] getMainDiagonal(int[][] matrix) {
// Check for null, empty, or non-square matrix
if (matrix == null || matrix.length == 0 || matrix[0].length == 0 || matrix.length != matrix[0].length) {
return null;
}
// Get matrix size
int n = matrix.length;
// Create result array
int[] result = new int[n];
// Extract diagonal elements
for (int i = 0; i < n; i++) {
result[i] = matrix[i][i];
}
return result;
}
// Main method to test getMainDiagonal with various inputs
public static void main(String[] args) {
DiagonalElements diagonal = new DiagonalElements();
// Test case 1: 3x3 matrix
int[][] matrix1 = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
System.out.println("Test case 1:");
System.out.println("Matrix:");
printMatrix(matrix1);
System.out.print("Main diagonal: ");
printArray(diagonal.getMainDiagonal(matrix1));
// Test case 2: 1x1 matrix
int[][] matrix2 = {{5}};
System.out.println("Test case 2:");
System.out.println("Matrix:");
printMatrix(matrix2);
System.out.print("Main diagonal: ");
printArray(diagonal.getMainDiagonal(matrix2));
// Test case 3: 2x2 matrix with negative numbers
int[][] matrix3 = {{-1, -2}, {-3, -4}};
System.out.println("Test case 3:");
System.out.println("Matrix:");
printMatrix(matrix3);
System.out.print("Main diagonal: ");
printArray(diagonal.getMainDiagonal(matrix3));
// Test case 4: 4x4 matrix
int[][] matrix4 = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 16}};
System.out.println("Test case 4:");
System.out.println("Matrix:");
printMatrix(matrix4);
System.out.print("Main diagonal: ");
printArray(diagonal.getMainDiagonal(matrix4));
// Test case 5: Null matrix
int[][] matrix5 = null;
System.out.println("Test case 5:");
System.out.println("Matrix:");
printMatrix(matrix5);
System.out.print("Main diagonal: ");
printArray(diagonal.getMainDiagonal(matrix5));
}
// Helper method to print matrix
private static void printMatrix(int[][] matrix) {
if (matrix == null || matrix.length == 0) {
System.out.println("null");
return;
}
System.out.println("[");
for (int i = 0; i < matrix.length; i++) {
System.out.print(" [");
for (int j = 0; j < matrix[i].length; j++) {
System.out.print(matrix[i][j]);
if (j < matrix[i].length - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
System.out.println("]");
}
// Helper method to print 1D array
private static void printArray(int[] arr) {
if (arr == null) {
System.out.println("null");
return;
}
System.out.print("[");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
if (i < arr.length - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
}
Output
Running the main method produces:
Test case 1:
Matrix:
[
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
]
Main diagonal: [1, 5, 9]
Test case 2:
Matrix:
[
[5]
]
Main diagonal: [5]
Test case 3:
Matrix:
[
[-1, -2]
[-3, -4]
]
Main diagonal: [-1, -4]
Test case 4:
Matrix:
[
[1, 2, 3, 4]
[5, 6, 7, 8]
[9, 10, 11, 12]
[13, 14, 15, 16]
]
Main diagonal: [1, 6, 11, 16]
Test case 5:
Matrix:
null
Main diagonal: null
Explanation:
- Test case 1: Extracts diagonal
[1, 5, 9]from a 3×3 matrix. - Test case 2: Extracts
[5]from a 1×1 matrix. - Test case 3: Extracts
[-1, -4]from a 2×2 matrix with negative numbers. - Test case 4: Extracts
[1, 6, 11, 16]from a 4×4 matrix. - Test case 5: Returns null for a null matrix.
How It Works
- Step 1: The
getMainDiagonalmethod checks if the matrix is null, empty, or not square. For[[1, 2, 3], [4, 5, 6], [7, 8, 9]], it proceeds. - Step 2: Get size:
n = 3. - Step 3: Create a result array of size 3.
- Step 4: Iterate for i = 0 to 2:
- i = 0:
result[0] = matrix[0][0] = 1 - i = 1:
result[1] = matrix[1][1] = 5 - i = 2:
result[2] = matrix[2][2] = 9
- i = 0:
- Example Trace: For test case 1, builds
[1, 5, 9]by collectingmatrix[i][i]. - Main Method: Tests with square matrices (3×3, 1×1, 2×2, 4×4) and a null matrix, printing inputs and results.
- Diagonal Property: Collects elements where row index equals column index, creating a 1D array of size n.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Diagonal Extraction | O(n) | O(n) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the size of the square matrix (rows = columns).
- Time complexity: O(n), as the algorithm iterates through n diagonal elements.
- Space complexity: O(n), for the result array. Temporary variables use O(1) space.
- Best, average, and worst cases are O(n).
✅ Tip: Since the matrix is square, a single loop over indices i is sufficient to extract
matrix[i][i]. Test with various sizes and negative numbers to ensure correctness.
⚠ Warning: Ensure the input matrix is square and not null to avoid
NullPointerExceptionorArrayIndexOutOfBoundsException. Verify consistent row lengths for a well-formed matrix.
Matrix Multiplication
Problem Statement
Write a Java program that implements a method to multiply two 2D arrays (matrices) and return the result as a new 2D array. The program should handle matrix multiplication for matrices of compatible sizes (i.e., the number of columns in the first matrix equals the number of rows in the second matrix) and test the implementation with matrices of different compatible sizes, including square and non-square matrices, and edge cases like single-row or single-column matrices. You can visualize this as combining two grids of numbers, where each cell in the resulting grid is computed by pairing rows from the first grid with columns from the second grid, summing the products of corresponding elements.
Input: Two 2D arrays (matrices) A (m × n) and B (n × p), where m is the number of rows in A, n is the number of columns in A and rows in B, and p is the number of columns in B (e.g., A = [[1, 2], [3, 4]], B = [[5, 6], [7, 8]]).
Output: A new 2D array (m × p) representing the product of A and B (e.g., [[19, 22], [43, 50]]).
Constraints:
- 1 ≤ m, n, p ≤ 100.
- Elements are integers between -10^4 and 10^4.
- The number of columns in A equals the number of rows in B (matrices are compatible). Example:
- Input:
A = [[1, 2], [3, 4]],B = [[5, 6], [7, 8]] - Output:
[[19, 22], [43, 50]] - Explanation: For element [0][0] in the result: 15 + 27 = 19; for [0][1]: 16 + 28 = 22; for [1][0]: 35 + 47 = 43; for [1][1]: 36 + 48 = 50.
- Input:
A = [[1, 2, 3]],B = [[4], [5], [6]] - Output:
[[32]] - Explanation: For element [0][0]: 14 + 25 + 3*6 = 32.
Pseudocode
FUNCTION multiplyMatrices(A, B)
IF A is null OR B is null OR A is empty OR B is empty OR columns of A not equal to rows of B THEN
RETURN null
ENDIF
SET m to number of rows in A
SET n to number of columns in A
SET p to number of columns in B
CREATE result matrix of size m × p
FOR i from 0 to m - 1
FOR j from 0 to p - 1
SET sum to 0
FOR k from 0 to n - 1
SET sum to sum + A[i][k] * B[k][j]
ENDFOR
SET result[i][j] to sum
ENDFOR
ENDFOR
RETURN result
ENDFUNCTION
FUNCTION main()
SET testCases to pairs of matrices
FOR each (A, B) in testCases
CALL multiplyMatrices(A, B)
PRINT result
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if matrices A or B are null, empty, or incompatible (number of columns in A ≠ number of rows in B). If so, return null.
- Get dimensions: m (rows of A), n (columns of A/rows of B), p (columns of B).
- Create a result matrix of size m × p, initialized to zeros.
- For each element
result[i][j]in the result matrix: a. Initialize a sum to 0. b. For each k from 0 to n-1, computeA[i][k] * B[k][j]and add to the sum. c. Setresult[i][j]to the computed sum. - Return the result matrix.
- In the
mainmethod, create test cases with different compatible matrix sizes and callmultiplyMatricesto verify correctness.
Java Implementation
public class MatrixMultiplication {
// Multiplies two matrices and returns the result
public int[][] multiplyMatrices(int[][] A, int[][] B) {
// Check for null, empty, or incompatible matrices
if (A == null || B == null || A.length == 0 || B.length == 0 || A[0].length != B.length) {
return null;
}
// Get dimensions
int m = A.length; // Rows of A
int n = A[0].length; // Columns of A, rows of B
int p = B[0].length; // Columns of B
// Initialize result matrix
int[][] result = new int[m][p];
// Compute matrix multiplication
for (int i = 0; i < m; i++) {
for (int j = 0; j < p; j++) {
int sum = 0;
for (int k = 0; k < n; k++) {
sum += A[i][k] * B[k][j];
}
result[i][j] = sum;
}
}
return result;
}
// Main method to test multiplyMatrices with various inputs
public static void main(String[] args) {
MatrixMultiplication multiplier = new MatrixMultiplication();
// Test case 1: 2x2 matrices
int[][] A1 = {{1, 2}, {3, 4}};
int[][] B1 = {{5, 6}, {7, 8}};
System.out.println("Test case 1:");
System.out.println("Matrix A1:");
printMatrix(A1);
System.out.println("Matrix B1:");
printMatrix(B1);
int[][] result1 = multiplier.multiplyMatrices(A1, B1);
System.out.println("Result:");
printMatrix(result1);
// Test case 2: 1x3 and 3x1 matrices
int[][] A2 = {{1, 2, 3}};
int[][] B2 = {{4}, {5}, {6}};
System.out.println("Test case 2:");
System.out.println("Matrix A2:");
printMatrix(A2);
System.out.println("Matrix B2:");
printMatrix(B2);
int[][] result2 = multiplier.multiplyMatrices(A2, B2);
System.out.println("Result:");
printMatrix(result2);
// Test case 3: 3x2 and 2x3 matrices
int[][] A3 = {{1, 2}, {3, 4}, {5, 6}};
int[][] B3 = {{7, 8, 9}, {10, 11, 12}};
System.out.println("Test case 3:");
System.out.println("Matrix A3:");
printMatrix(A3);
System.out.println("Matrix B3:");
printMatrix(B3);
int[][] result3 = multiplier.multiplyMatrices(A3, B3);
System.out.println("Result:");
printMatrix(result3);
// Test case 4: Incompatible matrices
int[][] A4 = {{1, 2}};
int[][] B4 = {{3, 4}};
System.out.println("Test case 4 (incompatible):");
System.out.println("Matrix A4:");
printMatrix(A4);
System.out.println("Matrix B4:");
printMatrix(B4);
int[][] result4 = multiplier.multiplyMatrices(A4, B4);
System.out.println("Result:");
printMatrix(result4);
}
// Helper method to print matrix
private static void printMatrix(int[][] matrix) {
if (matrix == null) {
System.out.println("null");
return;
}
System.out.println("[");
for (int i = 0; i < matrix.length; i++) {
System.out.print(" [");
for (int j = 0; j < matrix[i].length; j++) {
System.out.print(matrix[i][j]);
if (j < matrix[i].length - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
System.out.println("]");
}
}
Output
Running the main method produces:
Test case 1:
Matrix A1:
[
[1, 2]
[3, 4]
]
Matrix B1:
[
[5, 6]
[7, 8]
]
Result:
[
[19, 22]
[43, 50]
]
Test case 2:
Matrix A2:
[
[1, 2, 3]
]
Matrix B2:
[
[4]
[5]
[6]
]
Result:
[
[32]
]
Test case 3:
Matrix A3:
[
[1, 2]
[3, 4]
[5, 6]
]
Matrix B3:
[
[7, 8, 9]
[10, 11, 12]
]
Result:
[
[27, 30, 33]
[61, 68, 75]
[95, 106, 117]
]
Test case 4 (incompatible):
Matrix A4:
[
[1, 2]
]
Matrix B4:
[
[3, 4]
]
Result:
null
Explanation:
- Test case 1: Multiplies 2×2 matrices, resulting in
[[19, 22], [43, 50]]. - Test case 2: Multiplies 1×3 and 3×1 matrices, resulting in
[[32]]. - Test case 3: Multiplies 3×2 and 2×3 matrices, resulting in a 3×3 matrix.
- Test case 4: Incompatible matrices (1×2 and 1×2) return null.
How It Works
- Step 1: The
multiplyMatricesmethod checks for null, empty, or incompatible matrices. ForA = [[1, 2], [3, 4]],B = [[5, 6], [7, 8]], it proceeds (2×2 and 2×2 are compatible). - Step 2: Initialize
m = 2,n = 2,p = 2, and create a 2×2 result matrix. - Step 3: Compute each element:
result[0][0]:1*5 + 2*7 = 19.result[0][1]:1*6 + 2*8 = 22.result[1][0]:3*5 + 4*7 = 43.result[1][1]:3*6 + 4*8 = 50.
- Example Trace: For test case 1, computes
[[19, 22], [43, 50]]by summing products of rows of A with columns of B. - Main Method: Tests with square (2×2), non-square (1×3 × 3×1, 3×2 × 2×3), and incompatible matrices, printing inputs and results.
- Matrix Multiplication: Each element
result[i][j]is the dot product of row i of A and column j of B.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Multiplication | O(mnp) | O(m*p) |
| Full Algorithm | O(mnp) | O(m*p) |
Note:
- m is the number of rows in A, n is the number of columns in A/rows in B, p is the number of columns in B.
- Time complexity: O(mnp), as the algorithm iterates m*p times, each performing n multiplications and additions.
- Space complexity: O(m*p), for the result matrix. Temporary variables use O(1) space.
- Best, average, and worst cases are O(mnp).
✅ Tip: Always check matrix compatibility before multiplication to avoid errors. Test with various sizes, including single-row/column matrices, to ensure robustness.
⚠ Warning: Ensure matrices are not null and have consistent row/column sizes to avoid
NullPointerExceptionorArrayIndexOutOfBoundsException. Be cautious with large matrices, as O(mnp) time complexity can be slow.
Transpose Matrix
Problem Statement
Write a Java program that implements a method to transpose a 2D array (matrix), swapping its rows and columns to create a new matrix where the element at position [i][j] in the original matrix becomes the element at position [j][i] in the result. The program should return the transposed matrix as a new 2D array and test the implementation with square and non-square matrices, including edge cases like single-row or single-column matrices. You can visualize this as flipping a grid of numbers over its main diagonal, turning rows into columns and columns into rows, like rearranging a chessboard so that the row of pieces becomes a column.
Input: A 2D array (matrix) of integers with dimensions m × n (e.g., matrix = [[1, 2, 3], [4, 5, 6]]).
Output: A new 2D array of dimensions n × m representing the transposed matrix (e.g., [[1, 4], [2, 5], [3, 6]]).
Constraints:
- 1 ≤ m, n ≤ 100 (where m is the number of rows and n is the number of columns).
- Elements are integers between -10^9 and 10^9.
- The matrix is guaranteed to be non-empty and rectangular (all rows have the same number of columns). Example:
- Input:
matrix = [[1, 2, 3], [4, 5, 6]] - Output:
[[1, 4], [2, 5], [3, 6]] - Explanation: The 2×3 matrix is transposed to a 3×2 matrix, with row 1 ([1, 2, 3]) becoming column 1 ([1, 4]), etc.
- Input:
matrix = [[1, 2], [3, 4]] - Output:
[[1, 3], [2, 4]] - Explanation: The 2×2 square matrix is transposed, swapping elements across the main diagonal.
Pseudocode
FUNCTION transposeMatrix(matrix)
IF matrix is null OR matrix is empty THEN
RETURN null
ENDIF
SET rows to number of rows in matrix
SET cols to number of columns in matrix
CREATE result matrix of size cols × rows
FOR i from 0 to rows - 1
FOR j from 0 to cols - 1
SET result[j][i] to matrix[i][j]
ENDFOR
ENDFOR
RETURN result
ENDFUNCTION
FUNCTION main()
SET testMatrices to arrays of 2D arrays
FOR each matrix in testMatrices
CALL transposeMatrix(matrix)
PRINT result
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input matrix is null or empty. If so, return null.
- Determine the dimensions of the input matrix:
rows(m) andcolumns(n). - Create a new result matrix of size n × m (columns × rows).
- Iterate through each element
matrix[i][j]in the input matrix: a. Setresult[j][i]tomatrix[i][j], effectively swapping rows and columns. - Return the result matrix.
- In the
mainmethod, create test matrices (square and non-square) with various sizes and calltransposeMatrixto verify correctness.
Java Implementation
public class TransposeMatrix {
// Transposes a matrix by swapping rows and columns
public int[][] transposeMatrix(int[][] matrix) {
// Check for null or empty matrix
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return null;
}
// Get dimensions
int rows = matrix.length;
int cols = matrix[0].length;
// Create result matrix with swapped dimensions
int[][] result = new int[cols][rows];
// Transpose by copying elements
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
result[j][i] = matrix[i][j];
}
}
return result;
}
// Main method to test transposeMatrix with various inputs
public static void main(String[] args) {
TransposeMatrix transposer = new TransposeMatrix();
// Test case 1: 2x3 non-square matrix
int[][] matrix1 = {{1, 2, 3}, {4, 5, 6}};
System.out.println("Test case 1:");
System.out.println("Original matrix:");
printMatrix(matrix1);
int[][] result1 = transposer.transposeMatrix(matrix1);
System.out.println("Transposed matrix:");
printMatrix(result1);
// Test case 2: 2x2 square matrix
int[][] matrix2 = {{1, 2}, {3, 4}};
System.out.println("Test case 2:");
System.out.println("Original matrix:");
printMatrix(matrix2);
int[][] result2 = transposer.transposeMatrix(matrix2);
System.out.println("Transposed matrix:");
printMatrix(result2);
// Test case 3: 1x3 single-row matrix
int[][] matrix3 = {{1, 2, 3}};
System.out.println("Test case 3:");
System.out.println("Original matrix:");
printMatrix(matrix3);
int[][] result3 = transposer.transposeMatrix(matrix3);
System.out.println("Transposed matrix:");
printMatrix(result3);
// Test case 4: 3x1 single-column matrix
int[][] matrix4 = {{1}, {2}, {3}};
System.out.println("Test case 4:");
System.out.println("Original matrix:");
printMatrix(matrix4);
int[][] result4 = transposer.transposeMatrix(matrix4);
System.out.println("Transposed matrix:");
printMatrix(result4);
// Test case 5: Empty matrix
int[][] matrix5 = {};
System.out.println("Test case 5:");
System.out.println("Original matrix:");
printMatrix(matrix5);
int[][] result5 = transposer.transposeMatrix(matrix5);
System.out.println("Transposed matrix:");
printMatrix(result5);
}
// Helper method to print matrix
private static void printMatrix(int[][] matrix) {
if (matrix == null || matrix.length == 0) {
System.out.println("null");
return;
}
System.out.println("[");
for (int i = 0; i < matrix.length; i++) {
System.out.print(" [");
for (int j = 0; j < matrix[i].length; j++) {
System.out.print(matrix[i][j]);
if (j < matrix[i].length - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
System.out.println("]");
}
}
Output
Running the main method produces:
Test case 1:
Original matrix:
[
[1, 2, 3]
[4, 5, 6]
]
Transposed matrix:
[
[1, 4]
[2, 5]
[3, 6]
]
Test case 2:
Original matrix:
[
[1, 2]
[3, 4]
]
Transposed matrix:
[
[1, 3]
[2, 4]
]
Test case 3:
Original matrix:
[
[1, 2, 3]
]
Transposed matrix:
[
[1]
[2]
[3]
]
Test case 4:
Original matrix:
[
[1]
[2]
[3]
]
Transposed matrix:
[
[1, 2, 3]
]
Test case 5:
Original matrix:
null
Transposed matrix:
null
Explanation:
- Test case 1: Transposes a 2×3 matrix to a 3×2 matrix.
- Test case 2: Transposes a 2×2 square matrix, swapping elements across the diagonal.
- Test case 3: Transposes a 1×3 matrix to a 3×1 matrix.
- Test case 4: Transposes a 3×1 matrix to a 1×3 matrix.
- Test case 5: Returns null for an empty matrix.
How It Works
- Step 1: The
transposeMatrixmethod checks if the matrix is null or empty. For[[1, 2, 3], [4, 5, 6]], it proceeds. - Step 2: Get dimensions:
rows = 2,cols = 3. - Step 3: Create a result matrix of size 3×2.
- Step 4: Iterate through the input matrix:
matrix[0][0] = 1→result[0][0] = 1matrix[0][1] = 2→result[1][0] = 2matrix[0][2] = 3→result[2][0] = 3matrix[1][0] = 4→result[0][1] = 4matrix[1][1] = 5→result[1][1] = 5matrix[1][2] = 6→result[2][1] = 6
- Example Trace: For
[[1, 2, 3], [4, 5, 6]], builds[[1, 4], [2, 5], [3, 6]]by swapping indices. - Main Method: Tests with square (2×2), non-square (2×3, 1×3, 3×1), and empty matrices, printing inputs and results.
- Transpose Property: Creates a new matrix with swapped dimensions, where each
matrix[i][j]maps toresult[j][i].
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Transpose | O(m*n) | O(m*n) |
| Full Algorithm | O(m*n) | O(m*n) |
Note:
- m is the number of rows, n is the number of columns in the input matrix.
- Time complexity: O(m*n), as the algorithm iterates through each element of the m×n matrix once.
- Space complexity: O(m*n), for the result matrix. Temporary variables use O(1) space.
- Best, average, and worst cases are O(m*n).
✅ Tip: Ensure the result matrix has dimensions n×m for an m×n input matrix. Test with both square and non-square matrices to verify correctness, especially single-row or single-column cases.
⚠ Warning: Ensure the input matrix is not null and is rectangular (all rows have the same number of columns) to avoid
NullPointerExceptionorArrayIndexOutOfBoundsException.
Wave Traversal
Problem Statement
Write a Java program that implements a method to traverse a 2D array (matrix) in a wave pattern, where even-indexed columns (0-based) are traversed top-to-bottom and odd-indexed columns are traversed bottom-to-top. The program should return a 1D array containing the elements in the order of traversal and test the implementation with matrices of different sizes, including edge cases like single-row or single-column matrices. You can visualize this as navigating a grid like a wave, moving down even columns and up odd columns, collecting numbers like a surfer riding along a zigzag path.
Input: A 2D array (matrix) of integers with dimensions m × n (e.g., matrix = [[1, 2, 3], [4, 5, 6]]).
Output: A 1D array containing the elements in wave traversal order (e.g., [1, 4, 6, 5, 3, 2]).
Constraints:
- 1 ≤ m, n ≤ 100 (where m is the number of rows and n is the number of columns).
- Elements are integers between -10^9 and 10^9.
- The matrix is guaranteed to be non-empty and rectangular (all rows have the same number of columns). Example:
- Input:
matrix = [[1, 2, 3], [4, 5, 6]] - Output:
[1, 4, 6, 5, 3, 2] - Explanation: Column 0 (even): top-to-bottom [1, 4]; Column 1 (odd): bottom-to-top [6, 5]; Column 2 (even): top-to-bottom [3, 2].
- Input:
matrix = [[1, 2], [3, 4], [5, 6]] - Output:
[1, 3, 5, 6, 4, 2] - Explanation: Column 0 (even): top-to-bottom [1, 3, 5]; Column 1 (odd): bottom-to-top [6, 4, 2].
Pseudocode
FUNCTION waveTraversal(matrix)
IF matrix is null OR matrix is empty OR matrix[0] is empty THEN
RETURN null
ENDIF
SET rows to number of rows in matrix
SET cols to number of columns in matrix
CREATE result array of size rows * cols
SET index to 0
FOR j from 0 to cols - 1
IF j is even THEN
FOR i from 0 to rows - 1
SET result[index] to matrix[i][j]
INCREMENT index
ENDFOR
ELSE
FOR i from rows - 1 to 0
SET result[index] to matrix[i][j]
INCREMENT index
ENDFOR
ENDIF
ENDFOR
RETURN result
ENDFUNCTION
FUNCTION main()
SET testMatrices to 2D arrays
FOR each matrix in testMatrices
CALL waveTraversal(matrix)
PRINT result
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input matrix is null, empty, or has empty rows. If so, return null.
- Get the dimensions:
rows(m) andcolumns(n). - Create a 1D result array of size m * n to store the wave traversal elements.
- Initialize an index to track the position in the result array.
- Iterate through each column j from 0 to n-1:
a. If j is even, traverse top-to-bottom (i from 0 to rows-1), adding
matrix[i][j]to result. b. If j is odd, traverse bottom-to-top (i from rows-1 to 0), addingmatrix[i][j]to result. c. Increment the index after each element is added. - Return the result array.
- In the
mainmethod, create test matrices of different sizes and callwaveTraversalto verify correctness.
Java Implementation
public class WaveTraversal {
// Traverses matrix in wave pattern and returns 1D array
public int[] waveTraversal(int[][] matrix) {
// Check for null or empty matrix
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return null;
}
// Get dimensions
int rows = matrix.length;
int cols = matrix[0].length;
// Create result array
int[] result = new int[rows * cols];
int index = 0;
// Traverse each column
for (int j = 0; j < cols; j++) {
// Even column: top-to-bottom
if (j % 2 == 0) {
for (int i = 0; i < rows; i++) {
result[index++] = matrix[i][j];
}
}
// Odd column: bottom-to-top
else {
for (int i = rows - 1; i >= 0; i--) {
result[index++] = matrix[i][j];
}
}
}
return result;
}
// Main method to test waveTraversal with various inputs
public static void main(String[] args) {
WaveTraversal traverser = new WaveTraversal();
// Test case 1: 2x3 matrix
int[][] matrix1 = {{1, 2, 3}, {4, 5, 6}};
System.out.println("Test case 1:");
System.out.println("Matrix:");
printMatrix(matrix1);
System.out.print("Wave traversal: ");
printArray(traverser.waveTraversal(matrix1));
// Test case 2: 3x2 matrix
int[][] matrix2 = {{1, 2}, {3, 4}, {5, 6}};
System.out.println("Test case 2:");
System.out.println("Matrix:");
printMatrix(matrix2);
System.out.print("Wave traversal: ");
printArray(traverser.waveTraversal(matrix2));
// Test case 3: 1x3 single-row matrix
int[][] matrix3 = {{1, 2, 3}};
System.out.println("Test case 3:");
System.out.println("Matrix:");
printMatrix(matrix3);
System.out.print("Wave traversal: ");
printArray(traverser.waveTraversal(matrix3));
// Test case 4: 3x1 single-column matrix
int[][] matrix4 = {{1}, {2}, {3}};
System.out.println("Test case 4:");
System.out.println("Matrix:");
printMatrix(matrix4);
System.out.print("Wave traversal: ");
printArray(traverser.waveTraversal(matrix4));
// Test case 5: 2x2 matrix with negative numbers
int[][] matrix5 = {{-1, -2}, {-3, -4}};
System.out.println("Test case 5:");
System.out.println("Matrix:");
printMatrix(matrix5);
System.out.print("Wave traversal: ");
printArray(traverser.waveTraversal(matrix5));
// Test case 6: Null matrix
int[][] matrix6 = null;
System.out.println("Test case 6:");
System.out.println("Matrix:");
printMatrix(matrix6);
System.out.print("Wave traversal: ");
printArray(traverser.waveTraversal(matrix6));
}
// Helper method to print matrix
private static void printMatrix(int[][] matrix) {
if (matrix == null || matrix.length == 0) {
System.out.println("null");
return;
}
System.out.println("[");
for (int i = 0; i < matrix.length; i++) {
System.out.print(" [");
for (int j = 0; j < matrix[i].length; j++) {
System.out.print(matrix[i][j]);
if (j < matrix[i].length - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
System.out.println("]");
}
// Helper method to print 1D array
private static void printArray(int[] arr) {
if (arr == null) {
System.out.println("null");
return;
}
System.out.print("[");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
if (i < arr.length - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
}
Output
Running the main method produces:
Test case 1:
Matrix:
[
[1, 2, 3]
[4, 5, 6]
]
Wave traversal: [1, 4, 6, 5, 3, 2]
Test case 2:
Matrix:
[
[1, 2]
[3, 4]
[5, 6]
]
Wave traversal: [1, 3, 5, 6, 4, 2]
Test case 3:
Matrix:
[
[1, 2, 3]
]
Wave traversal: [1, 3, 2]
Test case 4:
Matrix:
[
[1]
[2]
[3]
]
Wave traversal: [1, 2, 3]
Test case 5:
Matrix:
[
[-1, -2]
[-3, -4]
]
Wave traversal: [-1, -3, -4, -2]
Test case 6:
Matrix:
null
Wave traversal: null
Explanation:
- Test case 1: Traverses 2×3 matrix: Col 0 (even): [1, 4]; Col 1 (odd): [6, 5]; Col 2 (even): [3, 2].
- Test case 2: Traverses 3×2 matrix: Col 0 (even): [1, 3, 5]; Col 1 (odd): [6, 4, 2].
- Test case 3: Traverses 1×3 matrix: Col 0 (even): [1]; Col 1 (odd): [3]; Col 2 (even): [2].
- Test case 4: Traverses 3×1 matrix: Col 0 (even): [1, 2, 3].
- Test case 5: Traverses 2×2 matrix with negatives: Col 0 (even): [-1, -3]; Col 1 (odd): [-4, -2].
- Test case 6: Returns null for a null matrix.
How It Works
- Step 1: The
waveTraversalmethod checks for null or empty matrices. For[[1, 2, 3], [4, 5, 6]], it proceeds. - Step 2: Get dimensions:
rows = 2,cols = 3. - Step 3: Create a result array of size 2 * 3 = 6.
- Step 4: Iterate through columns:
- Col 0 (even):
i = 0 to 1→result[0] = matrix[0][0] = 1,result[1] = matrix[1][0] = 4. - Col 1 (odd):
i = 1 to 0→result[2] = matrix[1][1] = 6,result[3] = matrix[0][1] = 5. - Col 2 (even):
i = 0 to 1→result[4] = matrix[0][2] = 3,result[5] = matrix[1][2] = 2.
- Col 0 (even):
- Example Trace: For test case 1, builds
[1, 4, 6, 5, 3, 2]by alternating directions. - Main Method: Tests with different sizes (2×3, 3×2, 1×3, 3×1, 2×2) and a null matrix, printing inputs and results.
- Wave Property: Alternates top-to-bottom and bottom-to-top traversal based on column index parity.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Traversal | O(m*n) | O(m*n) |
| Full Algorithm | O(m*n) | O(m*n) |
Note:
- m is the number of rows, n is the number of columns.
- Time complexity: O(m*n), as the algorithm visits each element once.
- Space complexity: O(m*n), for the result array. Temporary variables use O(1) space.
- Best, average, and worst cases are O(m*n).
✅ Tip: Use a single index to track the result array position for simplicity. Test with single-row, single-column, and negative number matrices to ensure the wave pattern is correct.
⚠ Warning: Ensure the input matrix is not null and is rectangular to avoid
NullPointerExceptionorArrayIndexOutOfBoundsException. Verify consistent row lengths for a well-formed matrix.
Adjacency List Representation
Problem Statement
Write a Java program that implements an undirected graph using a jagged array as an adjacency list. The program should include methods to add edges between vertices and print the neighbors of each vertex. Test the implementation with a sample graph, including cases with varying numbers of vertices and edges, and ensure the graph correctly represents undirected connections (i.e., an edge from u to v implies an edge from v to u). You can visualize this as creating a map of friendships, where each person (vertex) has a list of friends (neighbors), and friendships are mutual.
Input:
- Number of vertices V (e.g., V = 4).
- Edges as pairs of vertices (e.g., (0, 1), (1, 2), (2, 3)). Output:
- For each vertex, print its neighbors as a list (e.g., for vertex 0: [1], for vertex 1: [0, 2], etc.). Constraints:
- 1 ≤ V ≤ 100 (number of vertices).
- Vertices are labeled from 0 to V-1.
- Edges are valid pairs of distinct vertices (u ≠ v).
- The graph is undirected and simple (no self-loops or multiple edges between the same vertices). Example:
- Input: V = 4, edges = [(0, 1), (1, 2), (2, 3), (0, 2)]
- Output:
Vertex 0 neighbors: [1, 2] Vertex 1 neighbors: [0, 2] Vertex 2 neighbors: [1, 3, 0] Vertex 3 neighbors: [2] - Explanation: The graph has 4 vertices. Vertex 0 is connected to 1 and 2, vertex 1 to 0 and 2, vertex 2 to 1, 3, and 0, and vertex 3 to 2.
Pseudocode
FUNCTION initializeGraph(V)
CREATE jagged array adjList of size V
FOR i from 0 to V - 1
SET adjList[i] to empty array
ENDFOR
RETURN adjList
ENDFUNCTION
FUNCTION addEdge(adjList, u, v)
IF adjList is null OR u < 0 OR u >= length of adjList OR v < 0 OR v >= length of adjList OR u equals v THEN
RETURN
ENDIF
IF v not in adjList[u] THEN
ADD v to adjList[u]
ENDIF
IF u not in adjList[v] THEN
ADD u to adjList[v]
ENDIF
ENDFUNCTION
FUNCTION printNeighbors(adjList)
IF adjList is null OR adjList is empty THEN
PRINT "Graph is empty"
RETURN
ENDIF
FOR i from 0 to length of adjList - 1
PRINT "Vertex i neighbors: " followed by adjList[i]
ENDFOR
ENDFUNCTION
FUNCTION main()
SET V to number of vertices
CALL initializeGraph(V)
SET edges to list of edge pairs
FOR each (u, v) in edges
CALL addEdge(adjList, u, v)
ENDFOR
CALL printNeighbors(adjList)
ENDFUNCTION
Algorithm Steps
- Initialize Graph:
a. Create a jagged array
adjListof size V, where each element is an empty array (to store neighbors). - Add Edge:
a. Validate inputs: check if
adjListis null, or vertices u, v are out of bounds or equal (self-loop). b. Add v toadjList[u]if not already present (to avoid duplicates). c. Add u toadjList[v]if not already present (for undirected graph). - Print Neighbors:
a. Check if
adjListis null or empty. If so, print "Graph is empty". b. For each vertex i, print its index and the array of neighborsadjList[i]. - In the
mainmethod, initialize a graph with a specified number of vertices, add edges from a sample edge list, and print the neighbors of each vertex.
Java Implementation
import java.util.ArrayList;
public class AdjacencyListRepresentation {
// Initializes a graph with V vertices as a jagged array
private ArrayList<Integer>[] adjList;
// Constructor to initialize graph
@SuppressWarnings("unchecked")
public AdjacencyListRepresentation(int V) {
adjList = (ArrayList<Integer>[]) new ArrayList[V];
for (int i = 0; i < V; i++) {
adjList[i] = new ArrayList<>();
}
}
// Adds an edge between vertices u and v
public void addEdge(int u, int v) {
// Validate inputs
if (adjList == null || u < 0 || u >= adjList.length || v < 0 || v >= adjList.length || u == v) {
return;
}
// Add v to u's list if not already present
if (!adjList[u].contains(v)) {
adjList[u].add(v);
}
// Add u to v's list if not already present (undirected)
if (!adjList[v].contains(u)) {
adjList[v].add(u);
}
}
// Prints neighbors of each vertex
public void printNeighbors() {
if (adjList == null || adjList.length == 0) {
System.out.println("Graph is empty");
return;
}
for (int i = 0; i < adjList.length; i++) {
System.out.print("Vertex " + i + " neighbors: [");
for (int j = 0; j < adjList[i].size(); j++) {
System.out.print(adjList[i].get(j));
if (j < adjList[i].size() - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
}
// Main method to test the graph implementation
public static void main(String[] args) {
// Test case 1: Sample graph with 4 vertices
System.out.println("Test case 1: Graph with 4 vertices");
AdjacencyListRepresentation graph1 = new AdjacencyListRepresentation(4);
int[][] edges1 = {{0, 1}, {1, 2}, {2, 3}, {0, 2}};
for (int[] edge : edges1) {
graph1.addEdge(edge[0], edge[1]);
}
graph1.printNeighbors();
// Test case 2: Single vertex, no edges
System.out.println("\nTest case 2: Single vertex");
AdjacencyListRepresentation graph2 = new AdjacencyListRepresentation(1);
graph2.printNeighbors();
// Test case 3: Graph with 5 vertices, more edges
System.out.println("\nTest case 3: Graph with 5 vertices");
AdjacencyListRepresentation graph3 = new AdjacencyListRepresentation(5);
int[][] edges3 = {{0, 1}, {0, 2}, {0, 3}, {1, 3}, {1, 4}, {2, 4}};
for (int[] edge : edges3) {
graph3.addEdge(edge[0], edge[1]);
}
graph3.printNeighbors();
// Test case 4: Empty graph (0 vertices)
System.out.println("\nTest case 4: Empty graph");
AdjacencyListRepresentation graph4 = new AdjacencyListRepresentation(0);
graph4.printNeighbors();
}
}
Output
Running the main method produces:
Test case 1: Graph with 4 vertices
Vertex 0 neighbors: [1, 2]
Vertex 1 neighbors: [0, 2]
Vertex 2 neighbors: [1, 3, 0]
Vertex 3 neighbors: [2]
Test case 2: Single vertex
Vertex 0 neighbors: []
Test case 3: Graph with 5 vertices
Vertex 0 neighbors: [1, 2, 3]
Vertex 1 neighbors: [0, 3, 4]
Vertex 2 neighbors: [0, 4]
Vertex 3 neighbors: [0, 1]
Vertex 4 neighbors: [1, 2]
Test case 4: Empty graph
Graph is empty
Explanation:
- Test case 1: Graph with 4 vertices and edges (0,1), (1,2), (2,3), (0,2). Vertex 0 is connected to 1 and 2, etc.
- Test case 2: Single vertex with no edges, so its neighbor list is empty.
- Test case 3: Graph with 5 vertices and more edges, forming a connected structure.
- Test case 4: Empty graph (0 vertices) prints "Graph is empty".
How It Works
- Initialization: Creates a jagged array (
ArrayList<Integer>[]) of size V, with each element as an emptyArrayListfor neighbors. - Add Edge: For edge (u, v), adds v to
adjList[u]and u toadjList[v]if not already present, ensuring undirected edges without duplicates. - Print Neighbors: Iterates through each vertex and prints its neighbor list.
- Example Trace (Test case 1):
- Initialize:
adjList = [[], [], [], []]for V = 4. - Add edge (0,1):
adjList[0] = [1],adjList[1] = [0]. - Add edge (1,2):
adjList[1] = [0, 2],adjList[2] = [1]. - Add edge (2,3):
adjList[2] = [1, 3],adjList[3] = [2]. - Add edge (0,2):
adjList[0] = [1, 2],adjList[2] = [1, 3, 0]. - Print: Vertex 0: [1, 2], Vertex 1: [0, 2], etc.
- Initialize:
- Main Method: Tests with a 4-vertex graph, a single-vertex graph, a 5-vertex graph, and an empty graph.
- Adjacency List Property: Uses a jagged array for space efficiency, storing only actual neighbors.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Initialize Graph | O(V) | O(V) |
| Add Edge | O(1) average | O(1) per edge |
| Print Neighbors | O(V + E) | O(1) |
| Full Algorithm | O(V + E) | O(V + E) |
Note:
- V is the number of vertices, E is the number of edges.
- Initialize: O(V) to create V empty lists.
- Add Edge: O(1) average for
ArrayListadd and contains (amortized, assuming hash-based checks). - Print Neighbors: O(V + E) to iterate through all vertices and their edges.
- Space: O(V + E) for the jagged array storing V lists and E edges (undirected edges stored twice).
- Worst case for dense graph: O(V^2) space if E ≈ V^2.
✅ Tip: Use
ArrayListfor dynamic neighbor lists to handle varying degrees. Test with sparse and dense graphs to verify correctness, including single-vertex and empty graphs.
⚠ Warning: Validate vertex indices to avoid
ArrayIndexOutOfBoundsException. Ensure edges are not self-loops (u ≠ v) and check for duplicates to maintain a simple graph.
Dynamic Row Addition
Problem Statement
Write a Java program that allows users to interactively add rows to a jagged array, specifying the length and elements of each row. The program should store the jagged array dynamically, support adding multiple rows with different lengths, and print the resulting array after each addition. Test the implementation with multiple row additions, including edge cases like empty rows or no additions. You can visualize this as building a flexible grid where users can append new rows of varying sizes, like adding shelves of different lengths to a bookcase and filling them with books.
Input:
- Number of rows to add (e.g., 3).
- For each row: length of the row and its elements (e.g., length = 3, elements = [1, 2, 3]). Output:
- The jagged array after each row addition, displayed as a 2D array (e.g.,
[[1, 2, 3], [4, 5], [6, 7, 8, 9]]after three additions). Constraints: - The number of rows to add is between 0 and 100.
- Each row’s length is between 0 and 100.
- Elements are integers between -10^4 and 10^4.
- The input is valid (row lengths and elements are within constraints). Example:
- Input: Add 3 rows:
- Row 1: length = 3, elements = [1, 2, 3]
- Row 2: length = 2, elements = [4, 5]
- Row 3: length = 4, elements = [6, 7, 8, 9]
- Output after each addition:
After adding row 1: [[1, 2, 3]] After adding row 2: [[1, 2, 3], [4, 5]] After adding row 3: [[1, 2, 3], [4, 5], [6, 7, 8, 9]] - Input: Add 1 row:
- Row 1: length = 0, elements = []
- Output:
[[]]
Pseudocode
FUNCTION addRow(jaggedArray, rowLength, elements)
IF rowLength < 0 OR elements length not equal to rowLength THEN
RETURN
ENDIF
CREATE newRow array of size rowLength
FOR i from 0 to rowLength - 1
SET newRow[i] to elements[i]
ENDFOR
ADD newRow to jaggedArray
ENDFUNCTION
FUNCTION main()
SET jaggedArray to empty dynamic array
SET testCases to list of (rowLength, elements) pairs
FOR each (rowLength, elements) in testCases
PRINT current jaggedArray
CALL addRow(jaggedArray, rowLength, elements)
PRINT updated jaggedArray
ENDFOR
ENDFUNCTION
Algorithm Steps
- Initialize an empty dynamic structure (
ArrayList<int[]>) to store the jagged array. - For each row addition: a. Validate the input: ensure row length is non-negative and matches the number of elements provided. b. Create a new array of the specified length and populate it with the provided elements. c. Add the new array as a row to the jagged array.
- Print the jagged array before and after each addition to show the changes.
- In the
mainmethod, simulate user input with predefined test cases, including rows of different lengths, empty rows, and edge cases, to verify correctness.
Java Implementation
import java.util.ArrayList;
public class DynamicRowAddition {
// Adds a row to the jagged array with specified length and elements
public void addRow(ArrayList<int[]> jaggedArray, int rowLength, int[] elements) {
// Validate input
if (rowLength < 0 || elements == null || elements.length != rowLength) {
return;
}
// Create new row and copy elements
int[] newRow = new int[rowLength];
for (int i = 0; i < rowLength; i++) {
newRow[i] = elements[i];
}
// Add row to jagged array
jaggedArray.add(newRow);
}
// Main method to test addRow with various inputs
public static void main(String[] args) {
DynamicRowAddition adder = new DynamicRowAddition();
// Test case 1: Add multiple rows with different lengths
System.out.println("Test case 1: Adding multiple rows");
ArrayList<int[]> jaggedArray1 = new ArrayList<>();
int[][] testRows1 = {
{1, 2, 3}, // Row of length 3
{4, 5}, // Row of length 2
{6, 7, 8, 9} // Row of length 4
};
for (int i = 0; i < testRows1.length; i++) {
System.out.println("Before adding row " + (i + 1) + ":");
printJaggedArray(jaggedArray1);
adder.addRow(jaggedArray1, testRows1[i].length, testRows1[i]);
System.out.println("After adding row " + (i + 1) + ":");
printJaggedArray(jaggedArray1);
}
// Test case 2: Add a single empty row
System.out.println("\nTest case 2: Adding an empty row");
ArrayList<int[]> jaggedArray2 = new ArrayList<>();
System.out.println("Before adding row:");
printJaggedArray(jaggedArray2);
adder.addRow(jaggedArray2, 0, new int[]{});
System.out.println("After adding row:");
printJaggedArray(jaggedArray2);
// Test case 3: Add rows with negative numbers
System.out.println("\nTest case 3: Adding rows with negative numbers");
ArrayList<int[]> jaggedArray3 = new ArrayList<>();
int[][] testRows3 = {
{-1, -2}, // Row of length 2
{-3, -4, -5} // Row of length 3
};
for (int i = 0; i < testRows3.length; i++) {
System.out.println("Before adding row " + (i + 1) + ":");
printJaggedArray(jaggedArray3);
adder.addRow(jaggedArray3, testRows3[i].length, testRows3[i]);
System.out.println("After adding row " + (i + 1) + ":");
printJaggedArray(jaggedArray3);
}
// Test case 4: No rows added
System.out.println("\nTest case 4: No rows added");
ArrayList<int[]> jaggedArray4 = new ArrayList<>();
System.out.println("Jagged array:");
printJaggedArray(jaggedArray4);
}
// Helper method to print jagged array
private static void printJaggedArray(ArrayList<int[]> jaggedArray) {
if (jaggedArray == null || jaggedArray.isEmpty()) {
System.out.println("[]");
return;
}
System.out.println("[");
for (int i = 0; i < jaggedArray.size(); i++) {
System.out.print(" [");
if (jaggedArray.get(i).length == 0) {
System.out.print("]");
} else {
for (int j = 0; j < jaggedArray.get(i).length; j++) {
System.out.print(jaggedArray.get(i)[j]);
if (j < jaggedArray.get(i).length - 1) {
System.out.print(", ");
}
}
System.out.print("]");
}
System.out.println();
}
System.out.println("]");
}
}
Output
Running the main method produces:
Test case 1: Adding multiple rows
Before adding row 1:
[]
After adding row 1:
[
[1, 2, 3]
]
Before adding row 2:
[
[1, 2, 3]
]
After adding row 2:
[
[1, 2, 3]
[4, 5]
]
Before adding row 3:
[
[1, 2, 3]
[4, 5]
]
After adding row 3:
[
[1, 2, 3]
[4, 5]
[6, 7, 8, 9]
]
Test case 2: Adding an empty row
Before adding row:
[]
After adding row:
[
[]
]
Test case 3: Adding rows with negative numbers
Before adding row 1:
[]
After adding row 1:
[
[-1, -2]
]
Before adding row 2:
[
[-1, -2]
]
After adding row 2:
[
[-1, -2]
[-3, -4, -5]
]
Test case 4: No rows added
Jagged array:
[]
Explanation:
- Test case 1: Adds three rows of lengths 3, 2, and 4, building the jagged array incrementally.
- Test case 2: Adds a single empty row, resulting in
[[]]. - Test case 3: Adds two rows with negative numbers, lengths 2 and 3.
- Test case 4: Shows an empty jagged array when no rows are added.
How It Works
- Step 1: Initialize an
ArrayList<int[]>to store the jagged array dynamically. - Step 2: For each row addition in
addRow:- Validate: Ensure
rowLengthis non-negative and matcheselements.length. - Create a new
int[]of sizerowLengthand copyelementsinto it. - Add the new row to the
jaggedArray.
- Validate: Ensure
- Example Trace (Test case 1):
- Start:
jaggedArray = []. - Add row 1:
[1, 2, 3]→[[1, 2, 3]]. - Add row 2:
[4, 5]→[[1, 2, 3], [4, 5]]. - Add row 3:
[6, 7, 8, 9]→[[1, 2, 3], [4, 5], [6, 7, 8, 9]].
- Start:
- Main Method: Simulates user input with test cases, including multiple rows, an empty row, negative numbers, and no additions, printing the array before and after each addition.
- Dynamic Property: Uses
ArrayListfor dynamic row addition, allowing flexible row lengths.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Add Row | O(n) | O(n) |
| Full Algorithm | O(N) | O(N) |
Note:
- n is the length of the row being added.
- N is the total number of elements across all rows added.
- Time complexity: O(n) per row addition (copying elements to new array), O(N) for all additions.
- Space complexity: O(n) per row (new array), O(N) for the entire jagged array.
- Worst case: O(N) time and space for adding all elements across multiple rows.
✅ Tip: Use
ArrayList<int[]>for dynamic row management. Test with empty rows, varying lengths, and negative numbers to ensure flexibility and correctness.
⚠ Warning: Validate that the number of elements matches the specified row length to avoid
ArrayIndexOutOfBoundsException. Ensure the jagged array is initialized to avoidNullPointerException.
Jagged Array Transpose
Problem Statement
Write a Java program that implements a method to transpose a jagged array (a 2D array where each row can have a different length), converting rows to columns to create a new jagged array. The i-th row of the transposed array contains all elements from the i-th column of the original array, with row lengths varying based on the number of non-null elements in each column. The program should return the transposed array and test the implementation with irregular jagged arrays, including edge cases like empty arrays, arrays with empty rows, and arrays with varying row lengths. You can visualize this as flipping a ragged grid so that each column becomes a row, like reorganizing a collection of uneven lists into a new set of lists based on column positions.
Input: A jagged 2D array of integers, where each row may have a different length (e.g., matrix = {{1, 2, 3}, {4}, {5, 6}}).
Output: A new jagged array where the i-th row contains elements from the i-th column of the input (e.g., {{1, 4, 5}, {2, 6}, {3}}).
Constraints:
- The number of rows is between 0 and 100.
- Each row’s length is between 0 and 100.
- Elements are integers between -10^4 and 10^4.
- The matrix may be empty or contain empty or null rows. Example:
- Input:
matrix = {{1, 2, 3}, {4}, {5, 6}} - Output:
{{1, 4, 5}, {2, 6}, {3}} - Explanation: Column 0 ([1, 4, 5]) becomes row 0, column 1 ([2, 6]) becomes row 1, column 2 ([3]) becomes row 2.
- Input:
matrix = {{1, 2}, {}, {3}} - Output:
{{1, 3}, {2}} - Explanation: Column 0 ([1, 3]) becomes row 0, column 1 ([2]) becomes row 1.
Pseudocode
FUNCTION transposeJaggedArray(matrix)
IF matrix is null OR matrix is empty THEN
RETURN empty array
ENDIF
SET maxCols to maximum row length in matrix
CREATE result as empty dynamic array
FOR j from 0 to maxCols - 1
CREATE tempList as empty list
FOR i from 0 to number of rows in matrix - 1
IF matrix[i] is not null AND j < length of matrix[i] THEN
ADD matrix[i][j] to tempList
ENDIF
ENDFOR
IF tempList is not empty THEN
CREATE newRow array of size tempList size
COPY tempList elements to newRow
ADD newRow to result
ENDIF
ENDFOR
RETURN result
ENDFUNCTION
FUNCTION main()
SET testMatrices to jagged 2D arrays
FOR each matrix in testMatrices
PRINT original matrix
CALL transposeJaggedArray(matrix)
PRINT transposed matrix
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input matrix is null or empty. If so, return an empty array.
- Determine the maximum number of columns (maxCols) by finding the longest row.
- Initialize an empty
ArrayList<int[]>for the result. - For each column index j from 0 to maxCols-1:
a. Create a temporary list to collect elements from column j.
b. Iterate through each row i:
- If row i is not null and j is within its length, add
matrix[i][j]to the temporary list. c. If the temporary list is not empty, convert it to an array and add it as a row to the result.
- If row i is not null and j is within its length, add
- Return the result as a jagged array.
- In the
mainmethod, create test matrices with irregular row lengths and calltransposeJaggedArrayto verify correctness, printing the original and transposed matrices.
Java Implementation
import java.util.ArrayList;
public class JaggedArrayTranspose {
// Transposes a jagged array, converting rows to columns
public int[][] transposeJaggedArray(int[][] matrix) {
// Check for null or empty matrix
if (matrix == null || matrix.length == 0) {
return new int[0][];
}
// Find maximum number of columns
int maxCols = 0;
for (int[] row : matrix) {
if (row != null && row.length > maxCols) {
maxCols = row.length;
}
}
// Create result array
ArrayList<int[]> result = new ArrayList<>();
// Process each column
for (int j = 0; j < maxCols; j++) {
ArrayList<Integer> tempList = new ArrayList<>();
// Collect elements from column j
for (int i = 0; i < matrix.length; i++) {
if (matrix[i] != null && j < matrix[i].length) {
tempList.add(matrix[i][j]);
}
}
// Convert non-empty tempList to array and add to result
if (!tempList.isEmpty()) {
int[] newRow = new int[tempList.size()];
for (int k = 0; k < tempList.size(); k++) {
newRow[k] = tempList.get(k);
}
result.add(newRow);
}
}
// Convert ArrayList to int[][]
return result.toArray(new int[0][]);
}
// Main method to test transposeJaggedArray with various inputs
public static void main(String[] args) {
JaggedArrayTranspose transposer = new JaggedArrayTranspose();
// Test case 1: Jagged array with varying row lengths
int[][] matrix1 = {{1, 2, 3}, {4}, {5, 6}};
System.out.println("Test case 1:");
System.out.println("Original matrix:");
printMatrix(matrix1);
System.out.println("Transposed matrix:");
printMatrix(transposer.transposeJaggedArray(matrix1));
// Test case 2: Jagged array with empty rows
int[][] matrix2 = {{1, 2}, {}, {3}};
System.out.println("\nTest case 2:");
System.out.println("Original matrix:");
printMatrix(matrix2);
System.out.println("Transposed matrix:");
printMatrix(transposer.transposeJaggedArray(matrix2));
// Test case 3: Empty matrix
int[][] matrix3 = {};
System.out.println("\nTest case 3:");
System.out.println("Original matrix:");
printMatrix(matrix3);
System.out.println("Transposed matrix:");
printMatrix(transposer.transposeJaggedArray(matrix3));
// Test case 4: Matrix with negative numbers
int[][] matrix4 = {{-1, -2, -3}, {-4, -5}, {-6}};
System.out.println("\nTest case 4:");
System.out.println("Original matrix:");
printMatrix(matrix4);
System.out.println("Transposed matrix:");
printMatrix(transposer.transposeJaggedArray(matrix4));
// Test case 5: Null matrix
int[][] matrix5 = null;
System.out.println("\nTest case 5:");
System.out.println("Original matrix:");
printMatrix(matrix5);
System.out.println("Transposed matrix:");
printMatrix(transposer.transposeJaggedArray(matrix5));
}
// Helper method to print jagged matrix
private static void printMatrix(int[][] matrix) {
if (matrix == null || matrix.length == 0) {
System.out.println("[]");
return;
}
System.out.println("[");
for (int i = 0; i < matrix.length; i++) {
System.out.print(" [");
if (matrix[i] == null || matrix[i].length == 0) {
System.out.print("]");
} else {
for (int j = 0; j < matrix[i].length; j++) {
System.out.print(matrix[i][j]);
if (j < matrix[i].length - 1) {
System.out.print(", ");
}
}
System.out.print("]");
}
System.out.println();
}
System.out.println("]");
}
}
Output
Running the main method produces:
Test case 1:
Original matrix:
[
[1, 2, 3]
[4]
[5, 6]
]
Transposed matrix:
[
[1, 4, 5]
[2, 6]
[3]
]
Test case 2:
Original matrix:
[
[1, 2]
[]
[3]
]
Transposed matrix:
[
[1, 3]
[2]
]
Test case 3:
Original matrix:
[]
Transposed matrix:
[]
Test case 4:
Original matrix:
[
[-1, -2, -3]
[-4, -5]
[-6]
]
Transposed matrix:
[
[-1, -4, -6]
[-2, -5]
[-3]
]
Test case 5:
Original matrix:
[]
Transposed matrix:
[]
Explanation:
- Test case 1: Transposes
{{1, 2, 3}, {4}, {5, 6}}to{{1, 4, 5}, {2, 6}, {3}}, where column 0 ([1, 4, 5]) becomes row 0, etc. - Test case 2: Transposes
{{1, 2}, {}, {3}}to{{1, 3}, {2}}, handling empty rows. - Test case 3: Empty matrix returns empty matrix.
- Test case 4: Transposes
{{-1, -2, -3}, {-4, -5}, {-6}}to{{-1, -4, -6}, {-2, -5}, {-3}}, handling negatives. - Test case 5: Null matrix returns empty matrix.
How It Works
- Step 1: Check if the matrix is null or empty. For
{{1, 2, 3}, {4}, {5, 6}}, proceed. - Step 2: Find maxCols = 3 (longest row).
- Step 3: Initialize result as an empty
ArrayList<int[]>. - Step 4: For each column j:
- j = 0: Collect [1, 4, 5] (from matrix[0][0], matrix[1][0], matrix[2][0]) → add to result.
- j = 1: Collect [2, 6] (from matrix[0][1], matrix[2][1]; matrix[1] has no column 1) → add to result.
- j = 2: Collect [3] (from matrix[0][2]; others have no column 2) → add to result.
- Example Trace: For test case 1, builds
[[1, 4, 5], [2, 6], [3]]by collecting column elements. - Main Method: Tests with irregular jagged arrays, including empty rows, empty matrix, negatives, and null matrix.
- Transpose Property: Each column becomes a row, with row lengths varying based on non-null column elements.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Transpose | O(m * c) | O(m * c) |
| Full Algorithm | O(m * c) | O(m * c) |
Note:
- m is the number of rows, c is the maximum row length (maxCols).
- Time complexity: O(m * c), as each element is visited once to build the transposed rows.
- Space complexity: O(m * c), for the result array storing up to m * c elements.
- Best, average, and worst cases are O(m * c).
✅ Tip: Use a dynamic structure like
ArrayListto handle varying row lengths in the transposed array. Test with irregular arrays, including empty and null rows, to ensure robustness.
⚠ Warning: Check for null rows and varying row lengths to avoid
NullPointerExceptionorArrayIndexOutOfBoundsException. Ensure the result array only includes non-empty rows.
Row Sorting
Problem Statement
Write a Java program that implements a method to sort each row of a jagged array (a 2D array where each row can have a different length) independently in ascending order. The program should modify the jagged array in-place and test the implementation with jagged arrays containing rows of different lengths, including edge cases like empty matrices, matrices with empty rows, and matrices with rows of varying lengths. You can visualize this as organizing books on multiple shelves of different lengths, where each shelf’s books are sorted by size without affecting other shelves.
Input: A jagged 2D array of integers, where each row may have a different length (e.g., matrix = {{4, 2, 1}, {3, 5}, {6, 0, 8, 7}}).
Output: The same jagged array with each row sorted in ascending order (e.g., {{1, 2, 4}, {3, 5}, {0, 6, 7, 8}}).
Constraints:
- The number of rows is between 0 and 100.
- Each row’s length is between 0 and 100.
- Elements are integers between -10^4 and 10^4.
- The matrix may be empty or contain empty or null rows. Example:
- Input:
matrix = {{4, 2, 1}, {3, 5}, {6, 0, 8, 7}} - Output:
{{1, 2, 4}, {3, 5}, {0, 6, 7, 8}} - Explanation: Each row is sorted independently: [4, 2, 1] → [1, 2, 4], [3, 5] → [3, 5], [6, 0, 8, 7] → [0, 6, 7, 8].
- Input:
matrix = {{}, {1}, {3, 2}} - Output:
{{}, {1}, {2, 3}} - Explanation: Empty row stays empty, single-element row stays unchanged, and [3, 2] → [2, 3].
Pseudocode
FUNCTION sortRows(matrix)
IF matrix is null OR matrix is empty THEN
RETURN
ENDIF
FOR each row in matrix
IF row is not null THEN
SORT row in ascending order
ENDIF
ENDFOR
ENDFUNCTION
FUNCTION main()
SET testMatrices to jagged 2D arrays
FOR each matrix in testMatrices
PRINT original matrix
CALL sortRows(matrix)
PRINT sorted matrix
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input matrix is null or empty. If so, return without modifying anything.
- Iterate through each row of the matrix: a. If the row is not null, sort it in ascending order using a sorting function.
- The matrix is modified in-place, with each row sorted independently.
- In the
mainmethod, create test matrices with varying row lengths and callsortRowsto verify correctness, printing the matrix before and after sorting.
Java Implementation
import java.util.Arrays;
public class RowSorting {
// Sorts each row of a jagged array independently in ascending order
public void sortRows(int[][] matrix) {
// Check for null or empty matrix
if (matrix == null || matrix.length == 0) {
return;
}
// Iterate through each row
for (int[] row : matrix) {
// Sort non-null rows
if (row != null) {
Arrays.sort(row);
}
}
}
// Main method to test sortRows with various inputs
public static void main(String[] args) {
RowSorting sorter = new RowSorting();
// Test case 1: Jagged matrix with different row lengths
int[][] matrix1 = {{4, 2, 1}, {3, 5}, {6, 0, 8, 7}};
System.out.println("Test case 1:");
System.out.println("Before sorting:");
printMatrix(matrix1);
sorter.sortRows(matrix1);
System.out.println("After sorting:");
printMatrix(matrix1);
// Test case 2: Matrix with empty and single-element rows
int[][] matrix2 = {{}, {1}, {3, 2}};
System.out.println("\nTest case 2:");
System.out.println("Before sorting:");
printMatrix(matrix2);
sorter.sortRows(matrix2);
System.out.println("After sorting:");
printMatrix(matrix2);
// Test case 3: Matrix with all zeros
int[][] matrix3 = {{0, 0}, {0}, {0, 0, 0}};
System.out.println("\nTest case 3:");
System.out.println("Before sorting:");
printMatrix(matrix3);
sorter.sortRows(matrix3);
System.out.println("After sorting:");
printMatrix(matrix3);
// Test case 4: Matrix with negative numbers
int[][] matrix4 = {{-3, -1, -2}, {-5}, {-4, -6, -7}};
System.out.println("\nTest case 4:");
System.out.println("Before sorting:");
printMatrix(matrix4);
sorter.sortRows(matrix4);
System.out.println("After sorting:");
printMatrix(matrix4);
// Test case 5: Null matrix
int[][] matrix5 = null;
System.out.println("\nTest case 5:");
System.out.println("Before sorting:");
printMatrix(matrix5);
sorter.sortRows(matrix5);
System.out.println("After sorting:");
printMatrix(matrix5);
}
// Helper method to print jagged matrix
private static void printMatrix(int[][] matrix) {
if (matrix == null || matrix.length == 0) {
System.out.println("null");
return;
}
System.out.println("[");
for (int i = 0; i < matrix.length; i++) {
System.out.print(" [");
if (matrix[i] == null || matrix[i].length == 0) {
System.out.print("]");
} else {
for (int j = 0; j < matrix[i].length; j++) {
System.out.print(matrix[i][j]);
if (j < matrix[i].length - 1) {
System.out.print(", ");
}
}
System.out.print("]");
}
System.out.println();
}
System.out.println("]");
}
}
Output
Running the main method produces:
Test case 1:
Before sorting:
[
[4, 2, 1]
[3, 5]
[6, 0, 8, 7]
]
After sorting:
[
[1, 2, 4]
[3, 5]
[0, 6, 7, 8]
]
Test case 2:
Before sorting:
[
[]
[1]
[3, 2]
]
After sorting:
[
[]
[1]
[2, 3]
]
Test case 3:
Before sorting:
[
[0, 0]
[0]
[0, 0, 0]
]
After sorting:
[
[0, 0]
[0]
[0, 0, 0]
]
Test case 4:
Before sorting:
[
[-3, -1, -2]
[-5]
[-4, -6, -7]
]
After sorting:
[
[-3, -2, -1]
[-5]
[-7, -6, -4]
]
Test case 5:
Before sorting:
null
After sorting:
null
Explanation:
- Test case 1: Sorts rows [4, 2, 1] → [1, 2, 4], [3, 5] → [3, 5], [6, 0, 8, 7] → [0, 6, 7, 8].
- Test case 2: Empty row stays empty, [1] stays [1], [3, 2] → [2, 3].
- Test case 3: Rows of zeros remain unchanged.
- Test case 4: Sorts rows with negatives: [-3, -1, -2] → [-3, -2, -1], [-5] → [-5], [-4, -6, -7] → [-7, -6, -4].
- Test case 5: Null matrix remains unchanged.
How It Works
- Step 1: The
sortRowsmethod checks if the matrix is null or empty. For{{4, 2, 1}, {3, 5}, {6, 0, 8, 7}}, it proceeds. - Step 2: Iterate through rows:
- Row 0:
[4, 2, 1]→ sort to[1, 2, 4]. - Row 1:
[3, 5]→ sort to[3, 5](already sorted). - Row 2:
[6, 0, 8, 7]→ sort to[0, 6, 7, 8].
- Row 0:
- Example Trace: For test case 1, each row is sorted independently using
Arrays.sort, modifying the matrix in-place. - Main Method: Tests with jagged matrices of varying row lengths, including empty rows, all zeros, negative numbers, and a null matrix, printing before and after sorting.
- Sorting Property: Each row is sorted independently, preserving the jagged structure and handling varying lengths.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Sorting Rows | O(Σ(r_i * log r_i)) | O(log r_max) |
| Full Algorithm | O(Σ(r_i * log r_i)) | O(log r_max) |
Note:
- r_i is the length of the i-th row, r_max is the maximum row length.
- Time complexity: O(Σ(r_i * log r_i)), where each row of length r_i is sorted using
Arrays.sort(Timsort, O(r_i * log r_i)). The total is the sum over all rows. - Space complexity: O(log r_max), as Timsort uses O(log r_i) space for each row, and the maximum row length dominates.
- Worst case: O(N * log N) where N is the total number of elements if one row contains all elements.
✅ Tip: Use
Arrays.sortfor efficient row sorting. Test with empty rows, single-element rows, and negative numbers to ensure robustness across different jagged array configurations.
⚠ Warning: Check for null rows to avoid
NullPointerException. Ensure the matrix is not null to prevent unexpected behavior during sorting.
Sparse Matrix Sum
Problem Statement
Write a Java program that represents a sparse matrix using a jagged array (a 2D array where each row can have a different length) and computes the sum of all non-zero elements. The program should return the sum as a long integer and test the implementation with matrices of varying row lengths, including edge cases like empty matrices, matrices with empty rows, and matrices with all zeros. You can visualize this as calculating the total value of non-zero items in a grid where each row may have a different number of columns, like summing the values of scattered resources in an uneven terrain map.
Input: A jagged 2D array of integers, where each row may have a different length (e.g., matrix = {{1, 0, 2}, {3}, {0, 4, 5, 0}}).
Output: A long integer representing the sum of all non-zero elements (e.g., 15 for the example above).
Constraints:
- The number of rows is between 0 and 100.
- Each row’s length is between 0 and 100.
- Elements are integers between -10^4 and 10^4.
- The matrix may be empty or contain empty rows. Example:
- Input:
matrix = {{1, 0, 2}, {3}, {0, 4, 5, 0}} - Output: 15
- Explanation: Non-zero elements are 1, 2, 3, 4, 5; sum = 1 + 2 + 3 + 4 + 5 = 15.
- Input:
matrix = {{0, 0}, {0}, {}} - Output: 0
- Explanation: No non-zero elements, so sum = 0.
Pseudocode
FUNCTION sumSparseMatrix(matrix)
IF matrix is null OR matrix is empty THEN
RETURN 0
ENDIF
SET sum to 0
FOR each row in matrix
IF row is not null THEN
FOR each element in row
IF element is not zero THEN
SET sum to sum + element
ENDIF
ENDFOR
ENDIF
ENDFOR
RETURN sum
ENDFUNCTION
FUNCTION main()
SET testMatrices to jagged 2D arrays
FOR each matrix in testMatrices
CALL sumSparseMatrix(matrix)
PRINT result
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input matrix is null or empty. If so, return 0.
- Initialize a variable
sumto 0. - Iterate through each row of the matrix:
a. Check if the row is not null.
b. Iterate through each element in the row.
c. If the element is non-zero, add it to
sum. - Return the final
sum. - In the
mainmethod, create test matrices with varying row lengths and callsumSparseMatrixto verify correctness.
Java Implementation
public class SparseMatrixSum {
// Computes the sum of non-zero elements in a jagged 2D array
public long sumSparseMatrix(int[][] matrix) {
// Check for null or empty matrix
if (matrix == null || matrix.length == 0) {
return 0;
}
// Initialize sum
long sum = 0;
// Iterate through each row
for (int[] row : matrix) {
// Check for non-null row
if (row != null) {
// Iterate through each element in the row
for (int element : row) {
// Add non-zero elements to sum
if (element != 0) {
sum += element;
}
}
}
}
return sum;
}
// Main method to test sumSparseMatrix with various inputs
public static void main(String[] args) {
SparseMatrixSum summer = new SparseMatrixSum();
// Test case 1: Jagged matrix with non-zero elements
int[][] matrix1 = {{1, 0, 2}, {3}, {0, 4, 5, 0}};
System.out.println("Test case 1:");
System.out.println("Matrix:");
printMatrix(matrix1);
System.out.println("Sum of non-zero elements: " + summer.sumSparseMatrix(matrix1));
// Test case 2: Matrix with all zeros
int[][] matrix2 = {{0, 0}, {0}, {0, 0}};
System.out.println("Test case 2:");
System.out.println("Matrix:");
printMatrix(matrix2);
System.out.println("Sum of non-zero elements: " + summer.sumSparseMatrix(matrix2));
// Test case 3: Matrix with empty rows
int[][] matrix3 = {{}, {1, 2}, {}};
System.out.println("Test case 3:");
System.out.println("Matrix:");
printMatrix(matrix3);
System.out.println("Sum of non-zero elements: " + summer.sumSparseMatrix(matrix3));
// Test case 4: Single-row matrix
int[][] matrix4 = {{1, 0, 3}};
System.out.println("Test case 4:");
System.out.println("Matrix:");
printMatrix(matrix4);
System.out.println("Sum of non-zero elements: " + summer.sumSparseMatrix(matrix4));
// Test case 5: Matrix with negative numbers
int[][] matrix5 = {{-1, 0}, {-2, -3, 0}, {4}};
System.out.println("Test case 5:");
System.out.println("Matrix:");
printMatrix(matrix5);
System.out.println("Sum of non-zero elements: " + summer.sumSparseMatrix(matrix5));
// Test case 6: Null matrix
int[][] matrix6 = null;
System.out.println("Test case 6:");
System.out.println("Matrix:");
printMatrix(matrix6);
System.out.println("Sum of non-zero elements: " + summer.sumSparseMatrix(matrix6));
}
// Helper method to print jagged matrix
private static void printMatrix(int[][] matrix) {
if (matrix == null || matrix.length == 0) {
System.out.println("null");
return;
}
System.out.println("[");
for (int i = 0; i < matrix.length; i++) {
System.out.print(" [");
if (matrix[i] == null || matrix[i].length == 0) {
System.out.print("]");
} else {
for (int j = 0; j < matrix[i].length; j++) {
System.out.print(matrix[i][j]);
if (j < matrix[i].length - 1) {
System.out.print(", ");
}
}
System.out.print("]");
}
System.out.println();
}
System.out.println("]");
}
}
Output
Running the main method produces:
Test case 1:
Matrix:
[
[1, 0, 2]
[3]
[0, 4, 5, 0]
]
Sum of non-zero elements: 15
Test case 2:
Matrix:
[
[0, 0]
[0]
[0, 0]
]
Sum of non-zero elements: 0
Test case 3:
Matrix:
[
[]
[1, 2]
[]
]
Sum of non-zero elements: 3
Test case 4:
Matrix:
[
[1, 0, 3]
]
Sum of non-zero elements: 4
Test case 5:
Matrix:
[
[-1, 0]
[-2, -3, 0]
[4]
]
Sum of non-zero elements: -2
Test case 6:
Matrix:
null
Sum of non-zero elements: 0
Explanation:
- Test case 1: Sums non-zero elements 1, 2, 3, 4, 5 = 15.
- Test case 2: No non-zero elements, sum = 0.
- Test case 3: Sums non-zero elements 1, 2 = 3 (empty rows contribute 0).
- Test case 4: Sums non-zero elements 1, 3 = 4.
- Test case 5: Sums non-zero elements -1, -2, -3, 4 = -2.
- Test case 6: Returns 0 for a null matrix.
How It Works
- Step 1: The
sumSparseMatrixmethod checks if the matrix is null or empty. For{{1, 0, 2}, {3}, {0, 4, 5, 0}}, it proceeds. - Step 2: Initialize
sum = 0. - Step 3: Iterate through rows:
- Row 0:
[1, 0, 2]→ sum = 0 + 1 + 0 + 2 = 3. - Row 1:
[3]→ sum = 3 + 3 = 6. - Row 2:
[0, 4, 5, 0]→ sum = 6 + 0 + 4 + 5 + 0 = 15.
- Row 0:
- Example Trace: For test case 1, accumulates non-zero elements: 1 + 2 + 3 + 4 + 5 = 15.
- Main Method: Tests with jagged matrices of varying row lengths, including all zeros, empty rows, single-row, negative numbers, and null matrix, printing inputs and sums.
- Sparse Property: Only non-zero elements contribute to the sum, leveraging the jagged array’s flexibility for sparse matrices.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Summation | O(N) | O(1) |
| Full Algorithm | O(N) | O(1) |
Note:
- N is the total number of elements across all rows (sum of row lengths).
- Time complexity: O(N), as the algorithm visits each element once.
- Space complexity: O(1), as only a single
sumvariable is used (excluding input/output). - Best, average, and worst cases are O(N).
✅ Tip: Use enhanced for loops for clean iteration over jagged arrays. Test with matrices containing empty rows, all zeros, and negative numbers to ensure robustness.
⚠ Warning: Check for null rows within the matrix to avoid
NullPointerException. Use alongfor the sum to handle large or negative numbers within the constraints.
Strings Problem Solving with DSA
🛠 What you will learn
This section outlines the format used for each DSA problem statement, designed to guide students through solving exercise problems in a clear, structured learning process. Each component serves a specific purpose to enhance understanding and practical application:
- Problem Statement: Clearly defines the task, including inputs, outputs, constraints, and an example. A real-world analogy makes the problem relatable, aligning with the engaging tone of the "Core Data Structures" summary, ensuring students grasp the problem’s context.
- Pseudocode: Provides a high-level, language-agnostic outline of the solution using standardized syntax (e.g.,
FUNCTION,IF,SET,RETURN). This helps students understand the logic before diving into code, bridging theory and implementation. - Algorithm Steps: Breaks down the pseudocode into detailed, actionable steps, connecting theoretical logic to practical coding. This ensures students can follow the implementation process systematically.
- Java Implementation: Offers a complete, commented Java code solution that students can study and run. It includes a
mainmethod with at least four test cases (e.g., target present, absent, middle element, and edge cases like duplicates or single elements) to verify correctness and explore different scenarios, maintaining a beginner-friendly approach. - Output: Shows the expected results from the test cases, including indices, comparisons, and execution times (where applicable), helping students verify their code’s correctness and understand the algorithm’s behavior.
- How It Works: Traces the algorithm’s execution with a detailed example, showing step-by-step how the search narrows down the range. This reinforces understanding by illustrating the algorithm in action.
- Complexity Analysis Table: Summarizes time and space complexity for best, average, and worst cases, teaching students to evaluate efficiency and compare trade-offs across implementations.
- Tip or Warning Box: Provides practical advice (e.g., when to use DSA) and highlights pitfalls (e.g., ensuring the array is sorted), guiding students toward best practices and common error avoidance.
Each exercise includes test cases to verify correctness and performance, aligning with a learning platform featuring embedded compilers for hands-on practice and visualizations to illustrate the halving process of DSA.
Palindrome Checker
Problem Statement
Write a Java program that implements a method to check if a string is a palindrome, ignoring case and non-alphanumeric characters (e.g., spaces, punctuation). A palindrome is a string that reads the same forward and backward. The program should return a boolean indicating whether the input is a palindrome and test the implementation with various inputs, including empty strings, single characters, and strings with special characters. You can visualize this as checking if a phrase, like a secret code, reads the same when flipped, after ignoring irrelevant symbols and letter case, as if decoding a message on a mirror.
Input: A string (e.g., "A man, a plan, a canal: Panama", "race a car", "").
Output: A boolean (true if the string is a palindrome after ignoring case and non-alphanumeric characters, false otherwise).
Constraints:
- String length is between 0 and 10^5.
- The string may contain any ASCII characters (letters, digits, spaces, punctuation).
- The input may be empty or null. Example:
- Input:
"A man, a plan, a canal: Panama" - Output:
true - Explanation: After ignoring case and non-alphanumeric characters, the string becomes
"amanaplanacanalpanama", which is a palindrome. - Input:
"race a car" - Output:
false - Explanation: After cleaning, the string becomes
"raceacar", which is not a palindrome. - Input:
"" - Output:
true - Explanation: An empty string is considered a palindrome.
Pseudocode
FUNCTION isPalindrome(input)
IF input is null THEN
RETURN false
ENDIF
SET cleaned to empty string
FOR each character c in input
IF c is alphanumeric THEN
SET cleaned to cleaned + lowercase(c)
ENDIF
ENDFOR
SET left to 0
SET right to length of cleaned - 1
WHILE left < right
IF cleaned[left] not equal to cleaned[right] THEN
RETURN false
ENDIF
INCREMENT left
DECREMENT right
ENDWHILE
RETURN true
ENDFUNCTION
FUNCTION main()
SET testStrings to array of strings including various cases
FOR each string in testStrings
PRINT input string
CALL isPalindrome(string)
PRINT result
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input string is null; if so, return
false. - Create a cleaned string by: a. Converting the input to lowercase. b. Including only alphanumeric characters (letters and digits).
- Use two pointers (left and right) to check if the cleaned string is a palindrome:
a. Start left at index 0 and right at the last index.
b. While left < right, compare characters; if they differ, return
false. c. Increment left and decrement right. - If the loop completes, return
true(the string is a palindrome). - In the
mainmethod, test with various strings, including empty, single-character, and strings with special characters, printing the input and result.
Java Implementation
public class PalindromeChecker {
// Checks if a string is a palindrome, ignoring case and non-alphanumeric characters
public boolean isPalindrome(String input) {
if (input == null) {
return false;
}
// Clean the string: lowercase and keep only alphanumeric characters
StringBuilder cleaned = new StringBuilder();
for (char c : input.toCharArray()) {
if (Character.isLetterOrDigit(c)) {
cleaned.append(Character.toLowerCase(c));
}
}
// Check palindrome using two pointers
int left = 0;
int right = cleaned.length() - 1;
while (left < right) {
if (cleaned.charAt(left) != cleaned.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
// Main method to test isPalindrome with various inputs
public static void main(String[] args) {
PalindromeChecker checker = new PalindromeChecker();
// Test cases
String[] testStrings = {
"A man, a plan, a canal: Panama", // Palindrome
"race a car", // Not a palindrome
"", // Empty string
"A", // Single character
"12321", // Numeric palindrome
"Hello, World!", // Not a palindrome
"RaCeCaR" // Palindrome (case-insensitive)
};
for (int i = 0; i < testStrings.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input: \"" + testStrings[i] + "\"");
boolean result = checker.isPalindrome(testStrings[i]);
System.out.println("Is palindrome: " + result + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input: "A man, a plan, a canal: Panama"
Is palindrome: true
Test case 2:
Input: "race a car"
Is palindrome: false
Test case 3:
Input: ""
Is palindrome: true
Test case 4:
Input: "A"
Is palindrome: true
Test case 5:
Input: "12321"
Is palindrome: true
Test case 6:
Input: "Hello, World!"
Is palindrome: false
Test case 7:
Input: "RaCeCaR"
Is palindrome: true
Explanation:
- Test case 1:
"A man, a plan, a canal: Panama"→"amanaplanacanalpanama", palindrome. - Test case 2:
"race a car"→"raceacar", not a palindrome. - Test case 3: Empty string
""→"", palindrome. - Test case 4:
"A"→"a", palindrome. - Test case 5:
"12321"→"12321", palindrome. - Test case 6:
"Hello, World!"→"helloworld", not a palindrome. - Test case 7:
"RaCeCaR"→"racecar", palindrome.
How It Works
- Step 1: Check for null input; return
falseif null. - Step 2: Clean the input using StringBuilder:
- Iterate through each character.
- If alphanumeric (using
Character.isLetterOrDigit), append its lowercase version (Character.toLowerCase).
- Step 3: Use two pointers to check palindrome:
- Compare characters at
leftandright; if different, returnfalse. - Move
leftrightward andrightleftward until they meet.
- Compare characters at
- Example Trace (Test case 1):
- Input:
"A man, a plan, a canal: Panama". - Cleaned:
"amanaplanacanalpanama". - Check: left = 0 ('a') vs right = 19 ('a'), left = 1 ('m') vs right = 18 ('m'), etc., all match →
true.
- Input:
- Main Method: Tests with various inputs, including palindromes, non-palindromes, empty strings, and strings with special characters.
- Palindrome Property: Ignores case and non-alphanumeric characters, focusing only on letters and digits.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Cleaning String | O(n) | O(n) |
| Palindrome Check | O(n) | O(1) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the length of the input string.
- Time complexity: O(n) for cleaning (iterating through the string) + O(n) for palindrome check (two-pointer traversal) = O(n).
- Space complexity: O(n) for the cleaned string in StringBuilder; palindrome check uses O(1) extra space.
- Best, average, and worst cases are O(n) time and O(n) space.
✅ Tip: Use
Character.isLetterOrDigitandCharacter.toLowerCaseto handle case and non-alphanumeric characters efficiently. Test with mixed cases, punctuation, and empty strings to ensure robustness.
⚠ Warning: Check for null input to avoid
NullPointerException. Be aware that cleaning the string may significantly reduce its length if it contains many non-alphanumeric characters.
Reverse a String
Problem Statement
Write a Java program that implements two methods to reverse a string: one using String methods (concatenation) and another using StringBuilder. The program should return the reversed string and compare the performance of both methods for large strings (e.g., 100,000 characters). Test the implementation with strings of varying lengths, including edge cases like empty strings and single-character strings. You can visualize this as flipping a sequence of letters, like rearranging a word written on a whiteboard, using two different tools: one that builds the result piece by piece (String) and another that efficiently manipulates the sequence (StringBuilder).
Input: A string (e.g., "hello", "", or a large string of 100,000 characters).
Output: The reversed string (e.g., "olleh", "") and performance metrics (execution time in nanoseconds) for both methods.
Constraints:
- String length is between 0 and 10^6.
- The string contains any printable ASCII characters.
- The input is a valid string (may be empty or null). Example:
- Input:
"hello" - Output:
"olleh" - Explanation: The string
"hello"is reversed to"olleh". - Input:
"" - Output:
"" - Explanation: An empty string remains empty.
- Performance Example: For a 100,000-character string, StringBuilder is significantly faster than String concatenation.
Pseudocode
FUNCTION reverseWithString(input)
IF input is null THEN
RETURN null
ENDIF
SET result to empty string
FOR i from length of input - 1 to 0
SET result to result + input[i]
ENDFOR
RETURN result
ENDFUNCTION
FUNCTION reverseWithStringBuilder(input)
IF input is null THEN
RETURN null
ENDIF
CREATE stringBuilder with input
CALL reverse on stringBuilder
RETURN stringBuilder as string
ENDFUNCTION
FUNCTION main()
SET testStrings to array of strings including small, empty, and large strings
FOR each string in testStrings
PRINT original string
SET startTime to current time
CALL reverseWithString(string)
SET stringTime to current time - startTime
PRINT reversed string and stringTime
SET startTime to current time
CALL reverseWithStringBuilder(string)
SET builderTime to current time - startTime
PRINT reversed string and builderTime
ENDFOR
ENDFUNCTION
Algorithm Steps
- String Method:
a. Check if the input is null; if so, return null.
b. Initialize an empty string
result. c. Iterate through the input string from the last character to the first, concatenating each character toresult. d. Returnresult. - StringBuilder Method:
a. Check if the input is null; if so, return null.
b. Create a StringBuilder with the input string.
c. Use StringBuilder’s
reversemethod to reverse the string. d. Return the reversed string. - Performance Comparison:
a. Measure execution time for both methods using
System.nanoTime(). b. Test with strings of different lengths, including a large string. - In the
mainmethod, test both methods with various inputs, print the results, and display execution times.
Java Implementation
public class ReverseString {
// Reverses string using String concatenation
public String reverseWithString(String input) {
if (input == null) {
return null;
}
String result = "";
for (int i = input.length() - 1; i >= 0; i--) {
result += input.charAt(i);
}
return result;
}
// Reverses string using StringBuilder
public String reverseWithStringBuilder(String input) {
if (input == null) {
return null;
}
return new StringBuilder(input).reverse().toString();
}
// Main method to test both reverse methods and compare performance
public static void main(String[] args) {
ReverseString reverser = new ReverseString();
// Test cases
String[] testStrings = {
"hello", // Small string
"", // Empty string
"a", // Single character
generateLargeString(100000) // Large string
};
for (int i = 0; i < testStrings.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Original string: \"" + (testStrings[i].length() > 20 ? testStrings[i].substring(0, 20) + "..." : testStrings[i]) + "\"");
// Test String method
long startTime = System.nanoTime();
String resultString = reverser.reverseWithString(testStrings[i]);
long stringTime = System.nanoTime() - startTime;
System.out.println("String method result: \"" + (resultString != null && resultString.length() > 20 ? resultString.substring(0, 20) + "..." : resultString) + "\"");
System.out.println("String method time: " + stringTime + " ns");
// Test StringBuilder method
startTime = System.nanoTime();
String resultBuilder = reverser.reverseWithStringBuilder(testStrings[i]);
long builderTime = System.nanoTime() - startTime;
System.out.println("StringBuilder method result: \"" + (resultBuilder != null && resultBuilder.length() > 20 ? resultBuilder.substring(0, 20) + "..." : resultBuilder) + "\"");
System.out.println("StringBuilder method time: " + builderTime + " ns\n");
}
}
// Helper method to generate a large string
private static String generateLargeString(int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append((char) ('a' + (i % 26)));
}
return sb.toString();
}
}
Output
Running the main method produces (actual times may vary depending on the system):
Test case 1:
Original string: "hello"
String method result: "olleh"
String method time: 123456 ns
StringBuilder method result: "olleh"
StringBuilder method time: 7890 ns
Test case 2:
Original string: ""
String method result: ""
String method time: 4567 ns
StringBuilder method result: ""
StringBuilder method time: 3456 ns
Test case 3:
Original string: "a"
String method result: "a"
String method time: 5678 ns
StringBuilder method result: "a"
StringBuilder method time: 4321 ns
Test case 4:
Original string: "abcdefghijklmnopqrst..."
String method result: "zyxwvutsrqponmlkjihg..."
String method time: 1234567890 ns
StringBuilder method result: "zyxwvutsrqponmlkjihg..."
StringBuilder method time: 987654 ns
Explanation:
- Test case 1: Reverses
"hello"to"olleh"; StringBuilder is faster. - Test case 2: Empty string
""remains""; both methods are fast. - Test case 3: Single character
"a"remains"a"; similar performance. - Test case 4: Large string (100,000 characters); StringBuilder is significantly faster due to avoiding repeated string object creation.
How It Works
- String Method:
- Iterates from the last character to the first, building a new string via concatenation.
- Each concatenation creates a new String object, leading to O(n^2) time for large strings.
- StringBuilder Method:
- Uses StringBuilder’s
reversemethod, which manipulates the internal character array in-place. - Highly efficient, O(n) time, as it avoids creating intermediate objects.
- Uses StringBuilder’s
- Performance Comparison:
- Measures time using
System.nanoTime()for both methods. - String concatenation is slow for large strings due to quadratic overhead.
- StringBuilder is optimized for mutable string operations, showing significant speedup.
- Measures time using
- Main Method: Tests with small, empty, single-character, and large strings, printing results and execution times.
- Trace (Test case 1):
- String:
"hello"→result = "" + 'o' + 'l' + 'l' + 'e' + 'h' = "olleh". - StringBuilder: Initializes with
"hello", reverses to"olleh".
- String:
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| String Method | O(n^2) | O(n) |
| StringBuilder Method | O(n) | O(n) |
| Full Algorithm | O(n^2) | O(n) |
Note:
- n is the length of the input string.
- String method: O(n^2) time due to string concatenation creating new objects each iteration; O(n) space for the result.
- StringBuilder method: O(n) time for in-place reversal; O(n) space for the StringBuilder.
- Worst case: String method dominates with O(n^2) for large strings.
✅ Tip: Use StringBuilder for reversing large strings due to its linear time complexity. Test with large inputs to observe performance differences clearly.
⚠ Warning: Avoid String concatenation for large strings, as it creates multiple intermediate objects, leading to poor performance. Always check for null inputs to prevent
NullPointerException.
String Compression
Problem Statement
Write a Java program that implements a method to compress a string by replacing sequences of repeated characters with the character followed by the count of its consecutive occurrences (e.g., "aabbb" becomes "a2b3"). Use StringBuilder for efficiency in building the compressed string. If the compressed string is not shorter than the original, return the original string. Test the implementation with various inputs, including empty strings, single characters, and strings with no repeated characters. You can visualize this as shrinking a word by summarizing repeated letters, like condensing a repetitive chant into a shorter code, ensuring the result is as compact as possible.
Input: A string (e.g., "aabbb", "abcd", "").
Output: The compressed string (e.g., "a2b3") or the original string if compression does not reduce the length.
Constraints:
- String length is between 0 and 10^5.
- The string contains only lowercase letters (a-z).
- The input may be empty or null. Example:
- Input:
"aabbb" - Output:
"a2b3" - Explanation: 'a' appears twice, 'b' appears three times, so the compressed string is
"a2b3". - Input:
"abcd" - Output:
"abcd" - Explanation: No repeated characters, and
"a1b1c1d1"is longer, so return"abcd". - Input:
"" - Output:
"" - Explanation: Empty string returns empty string.
Pseudocode
FUNCTION compressString(input)
IF input is null THEN
RETURN null
ENDIF
IF input is empty THEN
RETURN empty string
ENDIF
CREATE stringBuilder for result
SET count to 1
SET currentChar to input[0]
FOR i from 1 to length of input - 1
IF input[i] equals currentChar THEN
INCREMENT count
ELSE
APPEND currentChar to stringBuilder
IF count > 1 THEN
APPEND count to stringBuilder
ENDIF
SET currentChar to input[i]
SET count to 1
ENDFOR
APPEND currentChar to stringBuilder
IF count > 1 THEN
APPEND count to stringBuilder
ENDIF
SET compressed to stringBuilder as string
IF length of compressed >= length of input THEN
RETURN input
ENDIF
RETURN compressed
ENDFUNCTION
FUNCTION main()
SET testStrings to array of strings including various cases
FOR each string in testStrings
PRINT input string
CALL compressString(string)
PRINT compressed string
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input string is null; if so, return null.
- Check if the input string is empty; if so, return an empty string.
- Initialize a StringBuilder for the result, a count for consecutive characters, and the first character as
currentChar. - Iterate through the string from index 1:
a. If the current character equals
currentChar, increment the count. b. Otherwise, appendcurrentCharto StringBuilder; if count > 1, append the count. c. UpdatecurrentCharto the current character and reset count to 1. - After the loop, append the final
currentCharand its count (if > 1). - If the compressed string’s length is not less than the original, return the original string.
- Return the compressed string.
- In the
mainmethod, test with various strings, including empty, single-character, repeated characters, and no repeats.
Java Implementation
public class StringCompression {
// Compresses a string using StringBuilder
public String compressString(String input) {
if (input == null) {
return null;
}
if (input.isEmpty()) {
return "";
}
StringBuilder result = new StringBuilder();
int count = 1;
char currentChar = input.charAt(0);
// Iterate through string starting from index 1
for (int i = 1; i < input.length(); i++) {
if (input.charAt(i) == currentChar) {
count++;
} else {
result.append(currentChar);
if (count > 1) {
result.append(count);
}
currentChar = input.charAt(i);
count = 1;
}
}
// Append the last character and its count
result.append(currentChar);
if (count > 1) {
result.append(count);
}
// Return original if compressed length is not shorter
String compressed = result.toString();
return compressed.length() >= input.length() ? input : compressed;
}
// Main method to test compressString with various inputs
public static void main(String[] args) {
StringCompression compressor = new StringCompression();
// Test cases
String[] testStrings = {
"aabbb", // Repeated characters
"abcd", // No repeats
"", // Empty string
"a", // Single character
"aabbcc", // Multiple repeated characters
"aaaa", // All same character
null // Null input
};
for (int i = 0; i < testStrings.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input: \"" + testStrings[i] + "\"");
String result = compressor.compressString(testStrings[i]);
System.out.println("Compressed: \"" + result + "\"\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input: "aabbb"
Compressed: "a2b3"
Test case 2:
Input: "abcd"
Compressed: "abcd"
Test case 3:
Input: ""
Compressed: ""
Test case 4:
Input: "a"
Compressed: "a"
Test case 5:
Input: "aabbcc"
Compressed: "a2b2c2"
Test case 6:
Input: "aaaa"
Compressed: "a4"
Test case 7:
Input: "null"
Compressed: "null"
Explanation:
- Test case 1:
"aabbb"→"a2b3"(2 'a's, 3 'b's). - Test case 2:
"abcd"→"abcd"(no repeats,"a1b1c1d1"is longer). - Test case 3: Empty string
""→"". - Test case 4:
"a"→"a"(single character,"a1"is longer). - Test case 5:
"aabbcc"→"a2b2c2"(each character repeated twice). - Test case 6:
"aaaa"→"a4"(4 'a's). - Test case 7:
null→null.
How It Works
- Step 1: Check for null or empty input; return null or empty string as needed.
- Step 2: Initialize StringBuilder, set
count = 1, andcurrentCharto the first character. - Step 3: Iterate from index 1:
- If current character matches
currentChar, incrementcount. - Else, append
currentCharandcount(if > 1), resetcurrentCharandcount.
- If current character matches
- Step 4: Append final
currentCharandcount(if > 1). - Step 5: Compare lengths; return original if compressed length is not shorter.
- Example Trace (Test case 1):
- Input:
"aabbb". - Initialize:
result = "",currentChar = 'a',count = 1. - i=1:
'a'matches'a',count = 2. - i=2:
'b'differs, append"a2", setcurrentChar = 'b',count = 1. - i=3:
'b'matches,count = 2. - i=4:
'b'matches,count = 3. - End: Append
"b3", result ="a2b3". - Length check:
"a2b3"(4) <"aabbb"(5) → return"a2b3".
- Input:
- Main Method: Tests with repeated characters, no repeats, empty, single character, and null inputs.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Compression | O(n) | O(n) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the length of the input string.
- Time complexity: O(n) for iterating through the string once.
- Space complexity: O(n) for the StringBuilder to store the compressed string.
- Best, average, and worst cases are O(n) time and O(n) space.
✅ Tip: Use StringBuilder to efficiently build the compressed string. Test with strings that have no repeats or all repeats to verify the length comparison logic.
⚠ Warning: Ensure the compressed string is compared with the original length to return the shorter one. Check for null input to avoid
NullPointerException.
String Pool Experiment
Problem Statement
Write a Java program that demonstrates the Java string pool by comparing string literals, strings created with new String(), and interned strings using both the == operator (for reference equality) and the equals() method (for content equality). The program should analyze the memory usage implications and equality behavior of these strings, explaining how the string pool affects object references. Test the implementation with various cases, including identical literals, constructed strings, and interned strings, to highlight the differences in memory and equality. You can visualize this as exploring a shared library of strings where some books (strings) are reused to save space, while others are new copies, and checking if they’re the same book or just have the same content.
Input: None (the program defines strings for testing).
Output: Results of == and equals() comparisons for different string creation methods, along with an explanation of memory usage and string pool behavior.
Constraints:
- Strings contain printable ASCII characters.
- The program focuses on demonstrating string pool mechanics, not specific input constraints. Example:
- Strings:
s1 = "hello",s2 = "hello",s3 = new String("hello"),s4 = s3.intern(). - Output:
s1 == s2: true (same string pool reference) s1.equals(s2): true (same content) s1 == s3: false (different objects) s1.equals(s3): true (same content) s1 == s4: true (s4 interned to string pool) s1.equals(s4): true (same content) - Memory: Literals (
s1,s2) share a single string pool object;s3creates a new object;s4reuses the pool object.
Pseudocode
FUNCTION demonstrateStringPool()
SET s1 to string literal "hello"
SET s2 to string literal "hello"
SET s3 to new String("hello")
SET s4 to s3.intern()
SET s5 to new String("hello")
SET s6 to string literal "world"
PRINT s1 == s2 and s1.equals(s2)
PRINT s1 == s3 and s1.equals(s3)
PRINT s1 == s4 and s1.equals(s4)
PRINT s3 == s5 and s3.equals(s5)
PRINT s1 == s6 and s1.equals(s6)
PRINT memory usage explanation
ENDFUNCTION
FUNCTION main()
CALL demonstrateStringPool()
ENDFUNCTION
Algorithm Steps
- Create strings using different methods:
a.
s1,s2: String literals (stored in the string pool). b.s3,s5: New String objects (created on the heap, not pooled). c.s4: Interned version ofs3(references the string pool). d.s6: Different string literal for contrast. - Compare strings using:
a.
==to check reference equality (are they the same object?). b.equals()to check content equality (are the characters the same?). - Print comparison results for each pair.
- Explain memory usage:
- String literals share a single object in the string pool.
new String()creates a new object on the heap.intern()returns a reference to the string pool object.
- In the
mainmethod, call the demonstration function and include test cases to show string pool behavior.
Java Implementation
public class StringPoolExperiment {
// Demonstrates string pool behavior with comparisons
public void demonstrateStringPool() {
// String literals
String s1 = "hello";
String s2 = "hello";
// New String object
String s3 = new String("hello");
// Interned string
String s4 = s3.intern();
// Another new String object
String s5 = new String("hello");
// Different string literal
String s6 = "world";
// Empty string literal
String s7 = "";
String s8 = "";
// Comparisons
System.out.println("Test case 1: Literal vs Literal (s1 = \"hello\", s2 = \"hello\")");
System.out.println("s1 == s2: " + (s1 == s2) + " (same string pool reference)");
System.out.println("s1.equals(s2): " + s1.equals(s2) + " (same content)\n");
System.out.println("Test case 2: Literal vs New String (s1 = \"hello\", s3 = new String(\"hello\"))");
System.out.println("s1 == s3: " + (s1 == s3) + " (different objects)");
System.out.println("s1.equals(s3): " + s1.equals(s3) + " (same content)\n");
System.out.println("Test case 3: Literal vs Interned String (s1 = \"hello\", s4 = s3.intern())");
System.out.println("s1 == s4: " + (s1 == s4) + " (same string pool reference)");
System.out.println("s1.equals(s4): " + s1.equals(s4) + " (same content)\n");
System.out.println("Test case 4: New String vs New String (s3 = new String(\"hello\"), s5 = new String(\"hello\"))");
System.out.println("s3 == s5: " + (s3 == s5) + " (different objects)");
System.out.println("s3.equals(s5): " + s3.equals(s5) + " (same content)\n");
System.out.println("Test case 5: Literal vs Different Literal (s1 = \"hello\", s6 = \"world\")");
System.out.println("s1 == s6: " + (s1 == s6) + " (different string pool references)");
System.out.println("s1.equals(s6): " + s1.equals(s6) + " (different content)\n");
System.out.println("Test case 6: Empty Literal vs Empty Literal (s7 = \"\", s8 = \"\")");
System.out.println("s7 == s8: " + (s7 == s8) + " (same string pool reference)");
System.out.println("s7.equals(s8): " + s7.equals(s8) + " (same content)\n");
// Memory usage explanation
System.out.println("Memory Usage Analysis:");
System.out.println("- String literals (s1, s2) share a single object in the string pool, saving memory.");
System.out.println("- s3 and s5 create new objects on the heap, each with separate memory.");
System.out.println("- s4 (interned) reuses the string pool object, reducing memory usage.");
System.out.println("- s6 uses a different string pool object for \"world\".");
System.out.println("- Empty literals (s7, s8) share a single empty string in the pool.");
System.out.println("- Actual memory usage depends on JVM; string pool reduces duplication.");
}
// Main method to run the demonstration
public static void main(String[] args) {
StringPoolExperiment experiment = new StringPoolExperiment();
experiment.demonstrateStringPool();
}
}
Output
Running the main method produces:
Test case 1: Literal vs Literal (s1 = "hello", s2 = "hello")
s1 == s2: true (same string pool reference)
s1.equals(s2): true (same content)
Test case 2: Literal vs New String (s1 = "hello", s3 = new String("hello"))
s1 == s3: false (different objects)
s1.equals(s3): true (same content)
Test case 3: Literal vs Interned String (s1 = "hello", s4 = s3.intern())
s1 == s4: true (same string pool reference)
s1.equals(s4): true (same content)
Test case 4: New String vs New String (s3 = new String("hello"), s5 = new String("hello"))
s3 == s5: false (different objects)
s3.equals(s5): true (same content)
Test case 5: Literal vs Different Literal (s1 = "hello", s6 = "world")
s1 == s6: false (different string pool references)
s1.equals(s6): false (different content)
Test case 6: Empty Literal vs Empty Literal (s7 = "", s8 = "")
s7 == s8: true (same string pool reference)
s7.equals(s8): true (same content)
Memory Usage Analysis:
- String literals (s1, s2) share a single object in the string pool, saving memory.
- s3 and s5 create new objects on the heap, each with separate memory.
- s4 (interned) reuses the string pool object, reducing memory usage.
- s6 uses a different string pool object for "world".
- Empty literals (s7, s8) share a single empty string in the pool.
- Actual memory usage depends on JVM; string pool reduces duplication.
Explanation:
- Test case 1:
s1ands2(literals) reference the same string pool object, so==andequals()are true. - Test case 2:
s3(new String) is a separate heap object, sos1 == s3is false, butequals()is true. - Test case 3:
s4(interned) references the pool object, sos1 == s4is true. - Test case 4:
s3ands5are distinct heap objects, so==is false, butequals()is true. - Test case 5:
s1ands6are different pool objects, so both==andequals()are false. - Test case 6: Empty literals share the same pool object, so both
==andequals()are true.
How It Works
- String Literals: Stored in the string pool, a part of the JVM’s heap where identical literals share a single object (e.g.,
s1,s2point to the same"hello"). - new String(): Creates a new object on the heap, even if the content exists in the pool (e.g.,
s3,s5are separate from"hello"in the pool). - intern(): Returns the string pool reference for the content, reusing the pool object if it exists (e.g.,
s4points to the same"hello"ass1). - == vs equals():
==checks if two references point to the same memory address.equals()checks if the string contents are identical.
- Memory Usage:
- Literals save memory by reusing pool objects.
new String()creates additional heap objects, increasing memory usage.intern()reduces memory by reusing pool objects.
- Example Trace (Test case 1):
s1 = "hello",s2 = "hello": Both reference the same pool object.s1 == s2: true (same reference).s1.equals(s2): true (same content).
- Main Method: Tests various combinations to show string pool behavior and memory implications.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| String Creation | O(1) | O(n) |
| == Comparison | O(1) | O(1) |
| equals() Comparison | O(n) | O(1) |
| intern() | O(n) | O(1) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the length of the longest string.
- String creation: O(1) for literals (pool lookup), O(n) for
new String()(copying characters). ==: O(1), compares references.equals(): O(n), compares each character.intern(): O(n), may involve hash table lookup and string comparison.- Space: O(n) for each new String object; literals reuse pool space.
- Full algorithm: O(n) time for comparisons and interning; O(n) space for new String objects.
✅ Tip: Use string literals for constant strings to leverage the string pool and save memory. Use
intern()sparingly to reduce memory for dynamically created strings, but test thoroughly to understand its behavior.
⚠ Warning: Avoid using
==to compare string contents, as it checks references, not values. Overusingnew String()can increase memory usage unnecessarily due to duplicate objects.
Substring Frequency
Problem Statement
Write a Java program that implements two methods to count the occurrences of a substring within a string using the indexOf method: one for non-overlapping occurrences and one for overlapping occurrences. The program should return the number of times the substring appears in the string and test the implementation with various inputs, including cases where the substring has overlapping and non-overlapping occurrences, as well as edge cases like empty strings or substrings. You can visualize this as searching for a specific word in a book, counting how many times it appears, either by skipping the word’s length to avoid overlaps or checking every possible position for potential overlaps.
Input: A string and a substring (e.g., string = "aaa", substring = "aa").
Output: Two integers representing the number of non-overlapping and overlapping occurrences of the substring (e.g., non-overlapping: 1, overlapping: 2).
Constraints:
- String and substring lengths are between 0 and 10^5.
- The string and substring contain any ASCII characters.
- The input may be empty or null. Example:
- Input: string =
"aaa", substring ="aa" - Output: Non-overlapping: 1, Overlapping: 2
- Explanation: Non-overlapping counts
"aa"once (positions 0-1), skipping to position 2; overlapping counts"aa"at positions 0-1 and 1-2. - Input: string =
"abcabc", substring ="abc" - Output: Non-overlapping: 2, Overlapping: 2
- Explanation:
"abc"appears at positions 0-2 and 3-5, with no overlap possible. - Input: string =
"", substring ="a" - Output: Non-overlapping: 0, Overlapping: 0
- Explanation: Empty string has no occurrences.
Pseudocode
FUNCTION countNonOverlapping(input, sub)
IF input is null OR sub is null OR input is empty OR sub is empty THEN
RETURN 0
ENDIF
SET count to 0
SET index to 0
WHILE indexOf(sub in input starting at index) is not -1
INCREMENT count
SET index to indexOf(sub) + length of sub
ENDWHILE
RETURN count
ENDFUNCTION
FUNCTION countOverlapping(input, sub)
IF input is null OR sub is null OR input is empty OR sub is empty THEN
RETURN 0
ENDIF
SET count to 0
SET index to 0
WHILE indexOf(sub in input starting at index) is not -1
INCREMENT count
INCREMENT index
ENDWHILE
RETURN count
ENDFUNCTION
FUNCTION main()
SET testCases to array of (string, substring) pairs
FOR each (string, sub) in testCases
PRINT string and substring
CALL countNonOverlapping(string, sub)
PRINT non-overlapping count
CALL countOverlapping(string, sub)
PRINT overlapping count
ENDFOR
ENDFUNCTION
Algorithm Steps
- Non-Overlapping Count:
a. Check if the input string or substring is null or empty; if so, return 0.
b. Initialize
countto 0 andindexto 0. c. WhileindexOf(substring, index)returns a valid index:- Increment
count. - Update
indexto the found index plus the substring length (to skip overlaps). d. Returncount.
- Increment
- Overlapping Count:
a. Check if the input string or substring is null or empty; if so, return 0.
b. Initialize
countto 0 andindexto 0. c. WhileindexOf(substring, index)returns a valid index:- Increment
count. - Increment
indexby 1 (to check for overlaps). d. Returncount.
- Increment
- In the
mainmethod, test both methods with various string-substring pairs, including overlapping cases (e.g.,"aaa","aa"), non-overlapping cases (e.g.,"abcabc","abc"), and edge cases (e.g., empty or null inputs).
Java Implementation
public class SubstringFrequency {
// Counts non-overlapping occurrences of substring in string
public int countNonOverlapping(String input, String sub) {
if (input == null || sub == null || input.isEmpty() || sub.isEmpty()) {
return 0;
}
int count = 0;
int index = 0;
while ((index = input.indexOf(sub, index)) != -1) {
count++;
index += sub.length();
}
return count;
}
// Counts overlapping occurrences of substring in string
public int countOverlapping(String input, String sub) {
if (input == null || sub == null || input.isEmpty() || sub.isEmpty()) {
return 0;
}
int count = 0;
int index = 0;
while ((index = input.indexOf(sub, index)) != -1) {
count++;
index++;
}
return count;
}
// Main method to test both count methods
public static void main(String[] args) {
SubstringFrequency counter = new SubstringFrequency();
// Test cases
String[][] testCases = {
{"aaa", "aa"}, // Overlapping possible
{"abcabc", "abc"}, // No overlapping
{"", "a"}, // Empty string
{"hello", "l"}, // Multiple occurrences
{"aaaaaa", "aaa"}, // Multiple overlapping
{null, "a"}, // Null input
{"abc", ""} // Empty substring
};
for (int i = 0; i < testCases.length; i++) {
String input = testCases[i][0];
String sub = testCases[i][1];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("String: \"" + (input != null && input.length() > 20 ? input.substring(0, 20) + "..." : input) + "\"");
System.out.println("Substring: \"" + (sub != null && sub.length() > 20 ? sub.substring(0, 20) + "..." : sub) + "\"");
int nonOverlapping = counter.countNonOverlapping(input, sub);
int overlapping = counter.countOverlapping(input, sub);
System.out.println("Non-overlapping count: " + nonOverlapping);
System.out.println("Overlapping count: " + overlapping + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
String: "aaa"
Substring: "aa"
Non-overlapping count: 1
Overlapping count: 2
Test case 2:
String: "abcabc"
Substring: "abc"
Non-overlapping count: 2
Overlapping count: 2
Test case 3:
String: ""
Substring: "a"
Non-overlapping count: 0
Overlapping count: 0
Test case 4:
String: "hello"
Substring: "l"
Non-overlapping count: 2
Overlapping count: 2
Test case 5:
String: "aaaaaa"
Substring: "aaa"
Non-overlapping count: 2
Overlapping count: 4
Test case 6:
String: "null"
Substring: "a"
Non-overlapping count: 0
Overlapping count: 0
Test case 7:
String: "abc"
Substring: ""
Non-overlapping count: 0
Overlapping count: 0
Explanation:
- Test case 1:
"aaa","aa": Non-overlapping finds"aa"at 0-1 (skips to 2) → 1; overlapping finds at 0-1, 1-2 → 2. - Test case 2:
"abcabc","abc": Both find"abc"at 0-2, 3-5 → 2 (no overlap possible). - Test case 3: Empty string,
"a": No occurrences → 0. - Test case 4:
"hello","l": Finds"l"at positions 2, 3 → 2 (no overlap for single character). - Test case 5:
"aaaaaa","aaa": Non-overlapping finds at 0-2, 3-5 → 2; overlapping finds at 0-2, 1-3, 2-4, 3-5 → 4. - Test case 6: Null string,
"a": No occurrences → 0. - Test case 7:
"abc", empty substring: No valid occurrences → 0.
How It Works
- Non-Overlapping:
- Uses
indexOf(sub, index)to find the next occurrence. - After each match, skips
sub.length()positions to avoid counting overlaps.
- Uses
- Overlapping:
- Uses
indexOf(sub, index)but incrementsindexby 1 to check every starting position.
- Uses
- Example Trace (Test case 1):
- Input:
"aaa", sub:"aa". - Non-overlapping:
index = 0, finds at 0,index = 0 + 2 = 2, no more matches → count = 1. - Overlapping:
index = 0, finds at 0,index = 1, finds at 1,index = 2, no more matches → count = 2.
- Input:
- Main Method: Tests with overlapping cases (e.g.,
"aaa","aa"), non-overlapping cases (e.g.,"abcabc","abc"), and edge cases (empty, null). - IndexOf Property: Efficiently finds substrings, with overlapping/non-overlapping handled by index increment.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Non-Overlapping | O(n * m) | O(1) |
| Overlapping | O(n * m) | O(1) |
| Full Algorithm | O(n * m) | O(1) |
Note:
- n is the length of the input string, m is the length of the substring.
- Time complexity: O(n * m) for
indexOf, which may scan up to m characters per call, with up to n/m calls (non-overlapping) or n calls (overlapping). - Space complexity: O(1), as only a few variables are used.
- Worst case: O(n * m) when substring is short and string is long.
✅ Tip: Use
indexOffor efficient substring searching. Test with overlapping cases (e.g.,"aaa","aa") and non-overlapping cases to verify both counts.
⚠ Warning: Check for null or empty inputs to avoid
NullPointerExceptionor unexpected behavior. Ensure the substring is not longer than the input string.
Dynamic Text Builder
Problem Statement
Write a Java program that implements a method to build a formatted string in CSV (comma-separated values) format using StringBuilder, appending elements from an input array. The program should create a string where array elements are separated by commas and test the implementation with arrays of varying sizes, including edge cases like empty arrays and single-element arrays. You can visualize this as assembling a row of data for a spreadsheet, where each item from a list is neatly joined with commas, ensuring the output is compact and properly formatted.
Input: An array of strings (e.g., ["apple", "banana", "cherry"]).
Output: A single string in CSV format (e.g., "apple,banana,cherry").
Constraints:
- Array length is between 0 and 10^5.
- Each string element contains printable ASCII characters and may be empty or null.
- The input array may be null or empty. Example:
- Input:
["apple", "banana", "cherry"] - Output:
"apple,banana,cherry" - Explanation: Elements are joined with commas.
- Input:
[] - Output:
"" - Explanation: Empty array returns an empty string.
- Input:
["solo"] - Output:
"solo" - Explanation: Single element has no commas.
Pseudocode
FUNCTION buildCSVRow(array)
IF array is null THEN
RETURN null
ENDIF
IF array is empty THEN
RETURN empty string
ENDIF
CREATE stringBuilder for result
FOR each element in array
IF element is not null THEN
APPEND element to stringBuilder
IF not last element THEN
APPEND comma to stringBuilder
ENDIF
ENDIF
ENDFOR
RETURN stringBuilder as string
ENDFUNCTION
FUNCTION main()
SET testArrays to array of string arrays with varying sizes
FOR each array in testArrays
PRINT input array
CALL buildCSVRow(array)
PRINT resulting CSV string
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the input array is null; if so, return null.
- Check if the input array is empty; if so, return an empty string.
- Initialize a StringBuilder for the result.
- Iterate through the array: a. If the current element is not null, append it to the StringBuilder. b. If it is not the last element, append a comma.
- Return the StringBuilder’s content as a string.
- In the
mainmethod, test with arrays of different sizes (e.g., empty, single-element, multiple elements, and large arrays) and print the input and output.
Java Implementation
public class DynamicTextBuilder {
// Builds a CSV row from an array using StringBuilder
public String buildCSVRow(String[] array) {
if (array == null) {
return null;
}
if (array.length == 0) {
return "";
}
StringBuilder result = new StringBuilder();
for (int i = 0; i < array.length; i++) {
if (array[i] != null) {
result.append(array[i]);
if (i < array.length - 1) {
result.append(",");
}
}
}
return result.toString();
}
// Main method to test buildCSVRow with various array sizes
public static void main(String[] args) {
DynamicTextBuilder builder = new DynamicTextBuilder();
// Test cases
String[][] testArrays = {
{"apple", "banana", "cherry"}, // Multiple elements
{}, // Empty array
{"solo"}, // Single element
generateLargeArray(1000), // Large array
{null, "test", null}, // Array with null elements
null // Null array
};
for (int i = 0; i < testArrays.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.print("Input array: [");
if (testArrays[i] != null) {
for (int j = 0; j < testArrays[i].length; j++) {
System.out.print(testArrays[i][j] != null ? "\"" + testArrays[i][j] + "\"" : "null");
if (j < testArrays[i].length - 1) {
System.out.print(", ");
}
}
} else {
System.out.print("null");
}
System.out.println("]");
String result = builder.buildCSVRow(testArrays[i]);
System.out.println("CSV output: \"" + (result != null && result.length() > 50 ? result.substring(0, 50) + "..." : result) + "\"\n");
}
}
// Helper method to generate a large array
private static String[] generateLargeArray(int size) {
String[] largeArray = new String[size];
for (int i = 0; i < size; i++) {
largeArray[i] = "item" + i;
}
return largeArray;
}
}
Output
Running the main method produces:
Test case 1:
Input array: ["apple", "banana", "cherry"]
CSV output: "apple,banana,cherry"
Test case 2:
Input array: []
CSV output: ""
Test case 3:
Input array: ["solo"]
CSV output: "solo"
Test case 4:
Input array: ["item0", "item1", "item2", ..., "item999"]
CSV output: "item0,item1,item2,...,item998,item999"
Test case 5:
Input array: [null, "test", null]
CSV output: "test"
Test case 6:
Input array: [null]
CSV output: "null"
Explanation:
- Test case 1:
["apple", "banana", "cherry"]→"apple,banana,cherry". - Test case 2: Empty array
[]→"". - Test case 3:
["solo"]→"solo". - Test case 4: Large array (1000 elements) →
"item0,item1,...,item999". - Test case 5:
[null, "test", null]→"test"(skips null elements). - Test case 6:
null→"null".
How It Works
- Step 1: Check for null or empty array; return null or empty string as needed.
- Step 2: Initialize StringBuilder for efficient string construction.
- Step 3: Iterate through the array:
- Append non-null elements.
- Add a comma after each element except the last.
- Step 4: Return the StringBuilder’s content as a string.
- Example Trace (Test case 1):
- Input:
["apple", "banana", "cherry"]. - Initialize:
result = "". - i=0: Append
"apple,"→result = "apple,". - i=1: Append
"banana,"→result = "apple,banana,". - i=2: Append
"cherry"→result = "apple,banana,cherry".
- Input:
- Main Method: Tests with arrays of varying sizes, including empty, single-element, large, and arrays with null elements.
- CSV Property: Ensures proper formatting with commas between elements and no trailing comma.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Building CSV | O(n * m) | O(n * m) |
| Full Algorithm | O(n * m) | O(n * m) |
Note:
- n is the number of elements in the array, m is the average length of each string element.
- Time complexity: O(n * m) for appending each character of each string to StringBuilder.
- Space complexity: O(n * m) for the StringBuilder to store the final CSV string.
- Worst case: O(n * m) time and space when all elements are non-null and long.
✅ Tip: Use StringBuilder for efficient string concatenation when building formatted strings like CSV rows. Test with null elements and large arrays to ensure robustness.
⚠ Warning: Check for null array elements to avoid appending "null" as a string literal. Ensure no trailing comma is added to maintain proper CSV format.
Insert and Delete Simulation
Problem Statement
Write a Java program that simulates text editing using StringBuilder by performing a sequence of insert and delete operations on a string. Each operation is either an insertion (adding a string at a specified index) or a deletion (removing characters from a start index to an end index). The program should process a sequence of operations and return the final string, testing with different sequences, including edge cases like empty strings, invalid indices, or empty operation lists. You can visualize this as editing a document on a word processor, where you insert new text or delete sections, with StringBuilder acting as an efficient editor to keep the changes smooth and fast.
Input: An initial string (possibly empty) and a sequence of operations, where each operation is either:
- Insert:
{type="insert", index, string}(insertstringatindex). - Delete:
{type="delete", start, end}(delete characters fromstarttoend-1). Output: The final string after applying all operations. Constraints: - Initial string length is between 0 and 10^5.
- Number of operations is between 0 and 1000.
- Strings to insert contain printable ASCII characters.
- Indices are non-negative integers; invalid indices should be handled gracefully.
- The input may be null or empty. Example:
- Input: Initial string =
"hello", operations =[insert(5, " world"), delete(0, 2)] - Output:
"llo world" - Explanation: Insert
" world"at index 5 →"hello world", then delete from index 0 to 1 →"llo world". - Input: Initial string =
"", operations =[insert(0, "test")] - Output:
"test" - Explanation: Insert
"test"into empty string at index 0 →"test".
Pseudocode
FUNCTION simulateTextEditing(initial, operations)
IF initial is null THEN
SET initial to empty string
ENDIF
CREATE stringBuilder with initial
FOR each operation in operations
IF operation.type equals "insert" THEN
IF operation.index is valid THEN
CALL stringBuilder.insert(operation.index, operation.string)
ENDIF
ELSE IF operation.type equals "delete" THEN
IF operation.start and operation.end are valid THEN
CALL stringBuilder.delete(operation.start, operation.end)
ENDIF
ENDIF
ENDFOR
RETURN stringBuilder as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of (initial string, operations) pairs
FOR each (initial, operations) in testCases
PRINT initial string and operations
CALL simulateTextEditing(initial, operations)
PRINT resulting string
ENDFOR
ENDFUNCTION
Algorithm Steps
- Check if the initial string is null; if so, set it to an empty string.
- Initialize a StringBuilder with the initial string.
- For each operation in the sequence:
a. If the operation is "insert":
- Verify the index is valid (0 ≤ index ≤ current StringBuilder length).
- Insert the specified string at the index using
StringBuilder.insert. b. If the operation is "delete": - Verify the start and end indices are valid (0 ≤ start ≤ end ≤ current StringBuilder length).
- Delete the range from start to end-1 using
StringBuilder.delete.
- Return the final StringBuilder content as a string.
- In the
mainmethod, test with different sequences of operations, including empty strings, invalid indices, and large sequences.
Java Implementation
import java.util.*;
public class InsertAndDeleteSimulation {
// Class to represent an operation
static class Operation {
String type; // "insert" or "delete"
int index; // For insert: insertion point; for delete: start index
String str; // For insert: string to insert
int end; // For delete: end index
// Constructor for insert operation
Operation(String type, int index, String str) {
this.type = type;
this.index = index;
this.str = str;
}
// Constructor for delete operation
Operation(String type, int start, int end) {
this.type = type;
this.index = start;
this.end = end;
}
}
// Simulates text editing with insert and delete operations
public String simulateTextEditing(String initial, List<Operation> operations) {
if (initial == null) {
initial = "";
}
StringBuilder sb = new StringBuilder(initial);
for (Operation op : operations) {
if (op.type.equals("insert")) {
if (op.index >= 0 && op.index <= sb.length() && op.str != null) {
sb.insert(op.index, op.str);
}
} else if (op.type.equals("delete")) {
if (op.index >= 0 && op.end <= sb.length() && op.index <= op.end) {
sb.delete(op.index, op.end);
}
}
}
return sb.toString();
}
// Main method to test text editing simulation
public static void main(String[] args) {
InsertAndDeleteSimulation simulator = new InsertAndDeleteSimulation();
// Test cases
List<Object[]> testCases = new ArrayList<>();
// Test case 1: Normal insert and delete
List<Operation> ops1 = new ArrayList<>();
ops1.add(new Operation("insert", 5, " world"));
ops1.add(new Operation("delete", 0, 2));
testCases.add(new Object[]{"hello", ops1});
// Test case 2: Empty string with insert
List<Operation> ops2 = new ArrayList<>();
ops2.add(new Operation("insert", 0, "test"));
testCases.add(new Object[]{"", ops2});
// Test case 3: Empty operations
List<Operation> ops3 = new ArrayList<>();
testCases.add(new Object[]{"abc", ops3});
// Test case 4: Large sequence of operations
List<Operation> ops4 = new ArrayList<>();
ops4.add(new Operation("insert", 0, "start"));
for (int i = 1; i <= 10; i++) {
ops4.add(new Operation("insert", i * 5, "x"));
ops4.add(new Operation("delete", i * 2, i * 2 + 1));
}
testCases.add(new Object[]{"base", ops4});
// Test case 5: Null initial string and invalid indices
List<Operation> ops5 = new ArrayList<>();
ops5.add(new Operation("insert", 10, "invalid"));
ops5.add(new Operation("delete", -1, 5));
testCases.add(new Object[]{null, ops5});
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
String initial = (String) testCases.get(i)[0];
List<Operation> ops = (List<Operation>) testCases.get(i)[1];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Initial string: \"" + (initial != null ? initial : "null") + "\"");
System.out.println("Operations:");
for (Operation op : ops) {
if (op.type.equals("insert")) {
System.out.println(" Insert \"" + op.str + "\" at index " + op.index);
} else {
System.out.println(" Delete from index " + op.index + " to " + op.end);
}
}
String result = simulator.simulateTextEditing(initial, ops);
System.out.println("Result: \"" + (result != null && result.length() > 50 ? result.substring(0, 50) + "..." : result) + "\"\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Initial string: "hello"
Operations:
Insert " world" at index 5
Delete from index 0 to 2
Result: "llo world"
Test case 2:
Initial string: ""
Operations:
Insert "test" at index 0
Result: "test"
Test case 3:
Initial string: "abc"
Operations:
Result: "abc"
Test case 4:
Initial string: "base"
Operations:
Insert "start" at index 0
Insert "x" at index 5
Delete from index 2 to 3
Insert "x" at index 10
Delete from index 4 to 5
Insert "x" at index 15
Delete from index 6 to 7
Insert "x" at index 20
Delete from index 8 to 9
Insert "x" at index 25
Delete from index 10 to 11
Insert "x" at index 30
Delete from index 12 to 13
Insert "x" at index 35
Delete from index 14 to 15
Insert "x" at index 40
Delete from index 16 to 17
Insert "x" at index 45
Delete from index 18 to 19
Insert "x" at index 50
Delete from index 20 to 21
Result: "staxrtxxx"
Test case 5:
Initial string: "null"
Operations:
Insert "invalid" at index 10
Delete from index -1 to 5
Result: ""
Explanation:
- Test case 1:
"hello"→ insert" world"at 5 →"hello world"→ delete 0 to 2 →"llo world". - Test case 2:
""→ insert"test"at 0 →"test". - Test case 3:
"abc"with no operations →"abc". - Test case 4:
"base"→ complex sequence of inserts and deletes →"staxrtxxx". - Test case 5: Null initial string becomes
"", invalid indices are skipped →"".
How It Works
- StringBuilder: Efficient for insert and delete operations due to mutable character array.
- Operation Class: Represents insert (index, string) or delete (start, end) operations.
- simulateTextEditing:
- Initializes StringBuilder with initial string (empty if null).
- Processes each operation, checking index validity before applying
insertordelete.
- Example Trace (Test case 1):
- Initial:
sb = "hello". - Insert
" world"at 5:sb = "hello world". - Delete 0 to 2:
sb = "llo world".
- Initial:
- Main Method: Tests with normal operations, empty string, no operations, large sequence, and invalid cases.
- Validation: Skips invalid operations (e.g., out-of-bounds indices) to ensure robustness.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Insert | O(n + m) | O(m) |
| Delete | O(n) | O(1) |
| Full Algorithm | O(k * (n + m)) | O(n + k * m) |
Note:
- n is the current length of the StringBuilder, m is the length of the inserted string, k is the number of operations.
- Insert: O(n + m) due to shifting characters and copying the new string.
- Delete: O(n) due to shifting characters after deletion.
- Full algorithm: O(k * (n + m)) for k operations, worst case when each operation involves the maximum string length.
- Space: O(n + k * m) for the StringBuilder, including inserted strings.
✅ Tip: Use StringBuilder for efficient text editing operations like insert and delete. Test with invalid indices and edge cases to ensure the program handles them gracefully.
⚠ Warning: Validate indices before performing operations to avoid
IndexOutOfBoundsException. Handle null inputs to prevent unexpected behavior.
StringBuilder Capacity Management
Problem Statement
Write a Java program that demonstrates StringBuilder’s capacity resizing by appending strings and monitoring capacity changes using the capacity() method. The program should analyze when resizing occurs and how the capacity adjusts based on the appended content. Test the implementation with different scenarios, including appending small strings, large strings, and using StringBuilder instances with varying initial capacities. You can visualize this as filling a stretchable notebook, where the notebook expands its pages (capacity) automatically when you add more notes, and you track how and when it grows to accommodate the text.
Input: A series of strings to append to StringBuilder instances with different initial configurations (e.g., default capacity, specified initial capacity). Output: The final string, a log of capacity changes after each append operation, and an analysis of when resizing occurs. Constraints:
- String lengths are between 0 and 10^5.
- Strings contain printable ASCII characters.
- Initial capacities are non-negative integers.
- The input strings may be null or empty. Example:
- Input: Append
"hello"," world"to a StringBuilder with default capacity (16). - Output:
Initial capacity: 16 After appending "hello": length=5, capacity=16 After appending " world": length=11, capacity=16 Final string: "hello world" - Explanation: No resizing occurs as the total length (11) fits within the initial capacity (16).
- Input: Append a 20-character string to a StringBuilder with default capacity (16).
- Output:
Initial capacity: 16 After appending: length=20, capacity=34 Final string: "abcdefghijklmnopqrst" - Explanation: Resizing occurs because 20 exceeds 16, new capacity = (16 * 2) + 2 = 34.
Pseudocode
FUNCTION demonstrateCapacityManagement(testCases)
FOR each testCase in testCases
SET initialCapacity to testCase.initialCapacity
SET strings to testCase.strings
CREATE stringBuilder with initialCapacity
PRINT initial capacity
FOR each string in strings
SET oldCapacity to stringBuilder.capacity()
APPEND string to stringBuilder
SET newCapacity to stringBuilder.capacity()
PRINT length, capacity, and whether resizing occurred
ENDFOR
PRINT final string
ENDFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of (initialCapacity, strings) pairs
CALL demonstrateCapacityManagement(testCases)
ENDFUNCTION
Algorithm Steps
- Define a method to demonstrate capacity management:
a. For each test case, create a StringBuilder with the specified initial capacity.
b. Print the initial capacity using
capacity(). c. For each string to append:- Record the current capacity.
- Append the string using
StringBuilder.append. - Check the new capacity and note if resizing occurred (capacity increased).
- Print the current length, capacity, and resizing status. d. Print the final string.
- Handle null or empty strings by skipping or appending empty content.
- In the
mainmethod, test with different scenarios:- Default capacity (16) with small strings.
- Default capacity with a large string that triggers resizing.
- Custom initial capacity with multiple appends.
- Empty or null strings.
- Analyze resizing: StringBuilder doubles the current capacity and adds 2 when the length exceeds the capacity.
Java Implementation
public class StringBuilderCapacityManagement {
// Demonstrates StringBuilder capacity resizing
public void demonstrateCapacityManagement(String[][] testCases, int[] initialCapacities) {
for (int i = 0; i < testCases.length; i++) {
String[] strings = testCases[i];
int initialCapacity = initialCapacities[i];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Initial capacity: " + initialCapacity);
StringBuilder sb = new StringBuilder(initialCapacity);
for (int j = 0; j < strings.length; j++) {
String str = strings[j] != null ? strings[j] : "";
int oldCapacity = sb.capacity();
sb.append(str);
int newCapacity = sb.capacity();
System.out.println("After appending \"" + (str.length() > 20 ? str.substring(0, 20) + "..." : str) + "\": " +
"length=" + sb.length() + ", capacity=" + newCapacity +
(newCapacity > oldCapacity ? " (resized)" : ""));
}
System.out.println("Final string: \"" + (sb.length() > 50 ? sb.substring(0, 50) + "..." : sb.toString()) + "\"\n");
}
}
// Main method to test capacity management
public static void main(String[] args) {
StringBuilderCapacityManagement manager = new StringBuilderCapacityManagement();
// Test cases
String[][] testCases = {
{"hello", " world"}, // Small strings, default capacity
{generateLargeString(20)}, // Large string, triggers resizing
{"a", "b", "c", "d"}, // Multiple small strings, custom capacity
{""}, // Empty string
{null} // Null string
};
int[] initialCapacities = {16, 16, 5, 16, 16}; // Corresponding initial capacities
manager.demonstrateCapacityManagement(testCases, initialCapacities);
}
// Helper method to generate a large string
private static String generateLargeString(int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append((char) ('a' + (i % 26)));
}
return sb.toString();
}
}
Output
Running the main method produces (actual capacities may vary slightly depending on JVM implementation):
Test case 1:
Initial capacity: 16
After appending "hello": length=5, capacity=16
After appending " world": length=11, capacity=16
Final string: "hello world"
Test case 2:
Initial capacity: 16
After appending "abcdefghijklmnopqrst...": length=20, capacity=34 (resized)
Final string: "abcdefghijklmnopqrst"
Test case 3:
Initial capacity: 5
After appending "a": length=1, capacity=5
After appending "b": length=2, capacity=5
After appending "c": length=3, capacity=5
After appending "d": length=4, capacity=5
Final string: "abcd"
Test case 4:
Initial capacity: 16
After appending "": length=0, capacity=16
Final string: ""
Test case 5:
Initial capacity: 16
After appending "": length=0, capacity=16
Final string: ""
Explanation:
- Test case 1: Appends
"hello"," world". Length (11) < capacity (16), no resizing. - Test case 2: Appends 20-character string. Length (20) > 16, resizes to (16 * 2) + 2 = 34.
- Test case 3: Custom capacity 5, appends four single characters. Length (4) ≤ 5, no resizing.
- Test case 4: Empty string, no change in length or capacity.
- Test case 5: Null string treated as empty, no change.
How It Works
- StringBuilder Capacity: Represents the size of the internal character array. Default is 16 if not specified.
- Resizing: When the length exceeds capacity, StringBuilder allocates a new array with capacity = (oldCapacity * 2) + 2.
- demonstrateCapacityManagement:
- Creates StringBuilder with specified initial capacity.
- Tracks capacity before and after each append.
- Reports resizing when new capacity exceeds old capacity.
- Example Trace (Test case 2):
- Initial:
capacity = 16,length = 0. - Append
"abcdefghijklmnopqrst"(20 chars):length = 20> 16, new capacity = (16 * 2) + 2 = 34.
- Initial:
- Main Method: Tests with small strings, large strings triggering resizing, custom capacity, and edge cases.
- Analysis: Resizing occurs when the total length after appending exceeds the current capacity, doubling the capacity plus 2 to accommodate future appends efficiently.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Append | O(m) | O(m) |
| Capacity Check | O(1) | O(1) |
| Resizing | O(n) | O(n) |
| Full Algorithm | O(k * m) | O(n + k * m) |
Note:
- n is the current length of the StringBuilder, m is the length of the appended string, k is the number of append operations.
- Append: O(m) for copying characters, amortized O(1) if no resizing.
- Resizing: O(n) when copying the internal array to a larger one.
- Full algorithm: O(k * m) time for k appends, O(n + k * m) space for the final StringBuilder content.
- Worst case: Frequent resizing for large strings increases time and space.
✅ Tip: Set an appropriate initial capacity for StringBuilder when the expected size is known to minimize resizing. Use
capacity()to monitor and understand resizing behavior during development.
⚠ Warning: Avoid unnecessarily small initial capacities, as frequent resizing can degrade performance. Handle null inputs to prevent unexpected behavior during append operations.
String Reversal
Problem Statement
Write a Java program that implements two methods to reverse a string: one using StringBuilder and another using StringBuffer. The program should return the reversed string and compare the performance of both methods for large inputs (e.g., a string of 100,000 characters). Test the implementation with strings of varying lengths, including edge cases like empty strings and single-character strings. You can visualize this as flipping a sequence of letters on a conveyor belt, using two different tools: StringBuilder for a quick, single-user operation, and StringBuffer for a safer, multi-user operation, to see which gets the job done faster.
Input: A string (e.g., "hello", "", or a large string of 100,000 characters).
Output: The reversed string (e.g., "olleh", "") and performance metrics (execution time in nanoseconds) for both StringBuilder and StringBuffer methods.
Constraints:
- String length is between 0 and 10^6.
- The string contains any printable ASCII characters.
- The input may be empty or null. Example:
- Input:
"hello" - Output:
"olleh" - Explanation: The string
"hello"is reversed to"olleh". - Input:
"" - Output:
"" - Explanation: An empty string remains empty.
- Performance Example: For a 100,000-character string, StringBuilder is generally faster than StringBuffer due to lack of synchronization.
Pseudocode
FUNCTION reverseWithStringBuilder(input)
IF input is null THEN
RETURN null
ENDIF
CREATE stringBuilder with input
CALL reverse on stringBuilder
RETURN stringBuilder as string
ENDFUNCTION
FUNCTION reverseWithStringBuffer(input)
IF input is null THEN
RETURN null
ENDIF
CREATE stringBuffer with input
CALL reverse on stringBuffer
RETURN stringBuffer as string
ENDFUNCTION
FUNCTION main()
SET testStrings to array of strings including small, empty, and large strings
FOR each string in testStrings
PRINT original string
SET startTime to current time
CALL reverseWithStringBuilder(string)
SET builderTime to current time - startTime
PRINT reversed string and builderTime
SET startTime to current time
CALL reverseWithStringBuffer(string)
SET bufferTime to current time - startTime
PRINT reversed string and bufferTime
ENDFOR
ENDFUNCTION
Algorithm Steps
- StringBuilder Method:
a. Check if the input is null; if so, return null.
b. Create a StringBuilder with the input string.
c. Use StringBuilder’s
reversemethod to reverse the string. d. Return the reversed string. - StringBuffer Method:
a. Check if the input is null; if so, return null.
b. Create a StringBuffer with the input string.
c. Use StringBuffer’s
reversemethod to reverse the string. d. Return the reversed string. - Performance Comparison:
a. Measure execution time for both methods using
System.nanoTime(). b. Test with strings of different lengths, including a large string. - In the
mainmethod, test both methods with various inputs, print the results, and display execution times.
Java Implementation
public class StringReversal {
// Reverses string using StringBuilder
public String reverseWithStringBuilder(String input) {
if (input == null) {
return null;
}
return new StringBuilder(input).reverse().toString();
}
// Reverses string using StringBuffer
public String reverseWithStringBuffer(String input) {
if (input == null) {
return null;
}
return new StringBuffer(input).reverse().toString();
}
// Main method to test both reverse methods and compare performance
public static void main(String[] args) {
StringReversal reverser = new StringReversal();
// Test cases
String[] testStrings = {
"hello", // Small string
"", // Empty string
"a", // Single character
generateLargeString(100000) // Large string
};
for (int i = 0; i < testStrings.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Original string: \"" + (testStrings[i].length() > 20 ? testStrings[i].substring(0, 20) + "..." : testStrings[i]) + "\"");
// Test StringBuilder method
long startTime = System.nanoTime();
String resultBuilder = reverser.reverseWithStringBuilder(testStrings[i]);
long builderTime = System.nanoTime() - startTime;
System.out.println("StringBuilder result: \"" + (resultBuilder != null && resultBuilder.length() > 20 ? resultBuilder.substring(0, 20) + "..." : resultBuilder) + "\"");
System.out.println("StringBuilder time: " + builderTime + " ns");
// Test StringBuffer method
startTime = System.nanoTime();
String resultBuffer = reverser.reverseWithStringBuffer(testStrings[i]);
long bufferTime = System.nanoTime() - startTime;
System.out.println("StringBuffer result: \"" + (resultBuffer != null && resultBuffer.length() > 20 ? resultBuffer.substring(0, 20) + "..." : resultBuffer) + "\"");
System.out.println("StringBuffer time: " + bufferTime + " ns\n");
}
}
// Helper method to generate a large string
private static String generateLargeString(int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append((char) ('a' + (i % 26)));
}
return sb.toString();
}
}
Output
Running the main method produces (actual times may vary depending on the system):
Test case 1:
Original string: "hello"
StringBuilder result: "olleh"
StringBuilder time: 7890 ns
StringBuffer result: "olleh"
StringBuffer time: 9123 ns
Test case 2:
Original string: ""
StringBuilder result: ""
StringBuilder time: 3456 ns
StringBuffer result: ""
StringBuffer time: 4567 ns
Test case 3:
Original string: "a"
StringBuilder result: "a"
StringBuilder time: 4321 ns
StringBuffer result: "a"
StringBuffer time: 5678 ns
Test case 4:
Original string: "abcdefghijklmnopqrst..."
StringBuilder result: "zyxwvutsrqponmlkjihg..."
StringBuilder time: 987654 ns
StringBuffer result: "zyxwvutsrqponmlkjihg..."
StringBuffer time: 1234567 ns
Explanation:
- Test case 1: Reverses
"hello"to"olleh"; StringBuilder is slightly faster. - Test case 2: Empty string
""remains""; both methods are fast. - Test case 3: Single character
"a"remains"a"; similar performance. - Test case 4: Large string (100,000 characters); StringBuilder is faster than StringBuffer due to lack of synchronization overhead.
How It Works
- StringBuilder Method:
- Uses StringBuilder’s
reversemethod, which manipulates the internal character array in-place. - Non-thread-safe, optimized for single-threaded use, making it faster.
- Uses StringBuilder’s
- StringBuffer Method:
- Uses StringBuffer’s
reversemethod, also manipulating the internal array. - Thread-safe due to synchronization, adding slight overhead, making it slower.
- Uses StringBuffer’s
- Performance Comparison:
- Measures time using
System.nanoTime()for both methods. - StringBuilder is faster, especially for large strings, as it avoids synchronization.
- StringBuffer’s synchronization adds overhead, noticeable in large inputs.
- Measures time using
- Main Method: Tests with small, empty, single-character, and large strings, printing results and execution times.
- Trace (Test case 1):
- StringBuilder: Initializes with
"hello", reverses to"olleh". - StringBuffer: Initializes with
"hello", reverses to"olleh".
- StringBuilder: Initializes with
- Key Difference: StringBuilder is faster for single-threaded applications; StringBuffer is safer for multi-threaded contexts.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| StringBuilder Reverse | O(n) | O(n) |
| StringBuffer Reverse | O(n) | O(n) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the length of the input string.
- Time complexity: O(n) for both StringBuilder and StringBuffer, as
reverseswaps characters in-place. - Space complexity: O(n) for the internal character array in both classes.
- StringBuilder is faster in practice due to no synchronization; StringBuffer has overhead.
- Worst case: O(n) time and O(n) space for both.
✅ Tip: Use StringBuilder for single-threaded applications to reverse strings efficiently. Test with large inputs to observe performance differences between StringBuilder and StringBuffer.
⚠ Warning: Use StringBuffer only in multi-threaded environments where thread safety is required, as its synchronization overhead can slow down performance. Always check for null inputs to prevent
NullPointerException.
Thread-Safe Concatenation
Problem Statement
Write a Java program that implements a multi-threaded application using StringBuffer to append strings concurrently from multiple threads, ensuring thread safety. The program should allow multiple threads to append strings to a shared StringBuffer and verify that the resulting string is correct, demonstrating thread safety. Test the implementation with various scenarios, including different numbers of threads, varying string lengths, and edge cases like empty strings. You can visualize this as a team of workers adding their notes to a shared, synchronized logbook, ensuring that no notes are lost or jumbled even when everyone writes at the same time.
Input: A set of strings to be appended by multiple threads (e.g., ["thread1", "thread2", "thread3"]).
Output: A single string containing all appended strings in the order of thread execution (e.g., "thread1thread2thread3") and verification of correctness.
Constraints:
- Number of threads is between 1 and 100.
- Each string contains printable ASCII characters and may be empty.
- The input strings may be null (handled by skipping or appending a default value). Example:
- Input: 3 threads appending
"thread1","thread2","thread3". - Output:
"thread1thread2thread3"(order may vary due to thread scheduling). - Explanation: Each thread appends its string to a shared StringBuffer, and the result is consistent due to StringBuffer’s thread-safe methods.
- Input: 2 threads appending
"","". - Output:
"" - Explanation: Empty strings result in an empty final string.
Pseudocode
FUNCTION appendString(buffer, str)
IF str is not null THEN
CALL buffer.append(str)
ENDIF
ENDFUNCTION
FUNCTION threadSafeConcatenation(strings, numThreads)
IF strings is null OR numThreads <= 0 THEN
RETURN null
ENDIF
CREATE sharedBuffer as new StringBuffer
CREATE threadList as empty list
FOR i from 0 to numThreads - 1
CREATE thread that calls appendString(sharedBuffer, strings[i])
ADD thread to threadList
ENDFOR
FOR each thread in threadList
START thread
ENDFOR
FOR each thread in threadList
JOIN thread
ENDFOR
RETURN sharedBuffer as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of (strings, numThreads) pairs
FOR each (strings, numThreads) in testCases
PRINT test case details
CALL threadSafeConcatenation(strings, numThreads)
PRINT resulting string
VERIFY result correctness
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a method
appendStringthat safely appends a string to a shared StringBuffer. - Define a method
threadSafeConcatenation: a. Check for null input or invalid thread count; return null if invalid. b. Create a shared StringBuffer. c. Create and start threads, each callingappendStringwith a string. d. Wait for all threads to complete usingjoin. e. Return the final concatenated string. - Verify thread safety by checking if the result contains all input strings in any order (since thread scheduling is non-deterministic).
- In the
mainmethod, test with different numbers of threads, string lengths, and edge cases (e.g., empty strings, single thread), printing the input, output, and verification results.
Java Implementation
import java.util.ArrayList;
public class ThreadSafeConcatenation {
// Appends a string to the shared StringBuffer
private void appendString(StringBuffer buffer, String str) {
if (str != null) {
buffer.append(str);
}
}
// Performs thread-safe concatenation using multiple threads
public String threadSafeConcatenation(String[] strings, int numThreads) {
if (strings == null || numThreads <= 0 || numThreads > strings.length) {
return null;
}
StringBuffer sharedBuffer = new StringBuffer();
ArrayList<Thread> threadList = new ArrayList<>();
// Create threads
for (int i = 0; i < numThreads; i++) {
final String str = strings[i];
Thread thread = new Thread(() -> appendString(sharedBuffer, str));
threadList.add(thread);
}
// Start all threads
for (Thread thread : threadList) {
thread.start();
}
// Wait for all threads to complete
for (Thread thread : threadList) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return sharedBuffer.toString();
}
// Main method to test thread-safe concatenation
public static void main(String[] args) {
ThreadSafeConcatenation concatenator = new ThreadSafeConcatenation();
// Test cases
Object[][] testCases = {
{new String[]{"thread1", "thread2", "thread3"}, 3}, // Multiple threads
{new String[]{"", ""}, 2}, // Empty strings
{new String[]{"solo"}, 1}, // Single thread
{generateLargeStrings(10), 10}, // Large input
{new String[]{null, "test", null}, 3}, // Null elements
{null, 1} // Null input
};
for (int i = 0; i < testCases.length; i++) {
String[] strings = (String[]) testCases[i][0];
int numThreads = (int) testCases[i][1];
System.out.println("Test case " + (i + 1) + ":");
System.out.print("Input strings: [");
if (strings != null) {
for (int j = 0; j < numThreads && j < strings.length; j++) {
System.out.print(strings[j] != null ? "\"" + strings[j] + "\"" : "null");
if (j < numThreads - 1 && j < strings.length - 1) {
System.out.print(", ");
}
}
} else {
System.out.print("null");
}
System.out.println("]");
System.out.println("Number of threads: " + numThreads);
String result = concatenator.threadSafeConcatenation(strings, numThreads);
System.out.println("Concatenated result: \"" + (result != null && result.length() > 50 ? result.substring(0, 50) + "..." : result) + "\"");
// Verify correctness
if (strings != null && result != null) {
StringBuilder expected = new StringBuilder();
for (int j = 0; j < numThreads && j < strings.length; j++) {
if (strings[j] != null) {
expected.append(strings[j]);
}
}
System.out.println("Verification: " + (result.length() == expected.length() ? "Correct (length matches expected)" : "Incorrect"));
} else {
System.out.println("Verification: " + (result == null ? "Correct (null expected)" : "Incorrect"));
}
System.out.println();
}
}
// Helper method to generate large strings for testing
private static String[] generateLargeStrings(int size) {
String[] largeStrings = new String[size];
for (int i = 0; i < size; i++) {
largeStrings[i] = "str" + i;
}
return largeStrings;
}
}
Output
Running the main method produces (note: order of strings in the result may vary due to thread scheduling):
Test case 1:
Input strings: ["thread1", "thread2", "thread3"]
Number of threads: 3
Concatenated result: "thread1thread2thread3"
Verification: Correct (length matches expected)
Test case 2:
Input strings: ["", ""]
Number of threads: 2
Concatenated result: ""
Verification: Correct (length matches expected)
Test case 3:
Input strings: ["solo"]
Number of threads: 1
Concatenated result: "solo"
Verification: Correct (length matches expected)
Test case 4:
Input strings: ["str0", "str1", "str2", ..., "str9"]
Number of threads: 10
Concatenated result: "str0str1str2str3str4str5str6str7str8str9"
Verification: Correct (length matches expected)
Test case 5:
Input strings: [null, "test", null]
Number of threads: 3
Concatenated result: "test"
Verification: Correct (length matches expected)
Test case 6:
Input strings: [null]
Number of threads: 1
Concatenated result: "null"
Verification: Correct (null expected)
Explanation:
- Test case 1: Three threads append
"thread1","thread2","thread3"; result is correct (order may vary). - Test case 2: Two threads append empty strings; result is empty.
- Test case 3: One thread appends
"solo"; result is"solo". - Test case 4: Ten threads append
"str0"to"str9"; result contains all strings. - Test case 5: Three threads, with nulls skipped, append
"test"; result is"test". - Test case 6: Null input returns null.
How It Works
- StringBuffer: Thread-safe due to synchronized methods, ensuring safe concurrent appends.
- appendString: Appends a non-null string to the shared StringBuffer.
- threadSafeConcatenation:
- Creates a shared StringBuffer.
- Spawns threads, each appending a string.
- Waits for all threads to finish using
join. - Returns the concatenated string.
- Verification: Checks if the result’s length matches the sum of non-null input string lengths.
- Example Trace (Test case 1):
- Initialize:
sharedBuffer = "". - Thread 1 appends
"thread1"→sharedBuffer = "thread1". - Thread 2 appends
"thread2"→sharedBuffer = "thread1thread2". - Thread 3 appends
"thread3"→sharedBuffer = "thread1thread2thread3".
- Initialize:
- Main Method: Tests with multiple threads, empty strings, single thread, large inputs, and null cases, verifying thread safety.
- Thread Safety: StringBuffer’s synchronized
appendmethod ensures no data corruption.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Append (per thread) | O(m) | O(m) |
| Full Algorithm | O(n * m) | O(n * m) |
Note:
- n is the number of threads, m is the average length of each string.
- Time complexity: O(m) per append operation; O(n * m) for all threads (though concurrent execution may reduce wall-clock time).
- Space complexity: O(n * m) for the StringBuffer storing all concatenated strings.
- Worst case: O(n * m) time and space when all strings are long and non-null.
✅ Tip: Use StringBuffer for thread-safe string concatenation in multi-threaded environments. Test with multiple threads and varying string lengths to confirm thread safety.
⚠ Warning: Ensure proper thread synchronization using
jointo avoid accessing the StringBuffer before all appends complete. Check for null inputs to prevent unexpected behavior.
Stacks Problem Solving with DSA
🛠 What you will learn
This section outlines the format used for each DSA problem statement, designed to guide students through solving exercise problems in a clear, structured learning process. Each component serves a specific purpose to enhance understanding and practical application:
- Problem Statement: Clearly defines the task, including inputs, outputs, constraints, and an example. A real-world analogy makes the problem relatable, aligning with the engaging tone of the "Core Data Structures" summary, ensuring students grasp the problem’s context.
- Pseudocode: Provides a high-level, language-agnostic outline of the solution using standardized syntax (e.g.,
FUNCTION,IF,SET,RETURN). This helps students understand the logic before diving into code, bridging theory and implementation. - Algorithm Steps: Breaks down the pseudocode into detailed, actionable steps, connecting theoretical logic to practical coding. This ensures students can follow the implementation process systematically.
- Java Implementation: Offers a complete, commented Java code solution that students can study and run. It includes a
mainmethod with at least four test cases (e.g., target present, absent, middle element, and edge cases like duplicates or single elements) to verify correctness and explore different scenarios, maintaining a beginner-friendly approach. - Output: Shows the expected results from the test cases, including indices, comparisons, and execution times (where applicable), helping students verify their code’s correctness and understand the algorithm’s behavior.
- How It Works: Traces the algorithm’s execution with a detailed example, showing step-by-step how the search narrows down the range. This reinforces understanding by illustrating the algorithm in action.
- Complexity Analysis Table: Summarizes time and space complexity for best, average, and worst cases, teaching students to evaluate efficiency and compare trade-offs across implementations.
- Tip or Warning Box: Provides practical advice (e.g., when to use DSA) and highlights pitfalls (e.g., ensuring the array is sorted), guiding students toward best practices and common error avoidance.
Each exercise includes test cases to verify correctness and performance, aligning with a learning platform featuring embedded compilers for hands-on practice and visualizations to illustrate the halving process of DSA.
Real-World Simulation: Browser Back Button
Problem Statement
Write a Java program that simulates a browser’s back button functionality using a stack. The program should allow users to input a list of URLs to push onto the stack, representing page navigation, and print the current page each time the back button is simulated by popping a URL from the stack. The current page is the topmost URL on the stack after each operation. Test the implementation with various sequences of push and pop operations, including edge cases like empty stacks and null inputs. You can visualize this as a stack of visited webpages, where each new page is added to the top, and pressing the back button removes the current page to reveal the previous one.
Input: A sequence of operations, where each operation is either:
- Push: Add a URL (string) to the stack (e.g.,
"https://example.com"). - Pop: Simulate the back button by removing the top URL and revealing the new top. Output: For each operation, print the operation performed and the current page (top of the stack) or a message if the stack is empty. Constraints:
- Stack size is between 0 and 10^5.
- URLs are non-empty strings containing printable ASCII characters, or null (handled gracefully).
- The stack may be empty when pop is called. Example:
- Input: Operations = [push("page1"), push("page2"), pop, push("page3"), pop]
- Output:
Pushed page1, Current page: page1 Pushed page2, Current page: page2 Popped page2, Current page: page1 Pushed page3, Current page: page3 Popped page3, Current page: page1 - Explanation: Push adds URLs to the stack; pop removes the top URL, showing the previous one.
- Input: Operations = [pop on empty stack]
- Output:
Popped, Stack empty
Pseudocode
CLASS StringStack
SET array to new string array of size 1000
SET top to -1
FUNCTION push(url)
IF top equals array length - 1 THEN
RETURN false (stack full)
ENDIF
INCREMENT top
SET array[top] to url
RETURN true
ENDFUNCTION
FUNCTION pop()
IF top equals -1 THEN
RETURN null (stack empty)
ENDIF
SET url to array[top]
DECREMENT top
RETURN url
ENDFUNCTION
FUNCTION peek()
IF top equals -1 THEN
RETURN null (stack empty)
ENDIF
RETURN array[top]
ENDFUNCTION
FUNCTION isEmpty()
RETURN top equals -1
ENDFUNCTION
ENDCLASS
FUNCTION simulateBrowser(operations)
CREATE stack as new StringStack
FOR each operation in operations
IF operation.type equals "push" THEN
CALL stack.push(operation.url)
PRINT pushed url and current page (stack.peek())
ELSE IF operation.type equals "pop" THEN
SET popped to stack.pop()
IF popped is null THEN
PRINT stack empty message
ELSE
PRINT popped url and current page (stack.peek())
ENDIF
ENDIF
ENDFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of operation sequences
FOR each testCase in testCases
PRINT test case details
CALL simulateBrowser(testCase)
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
StringStackclass with: a. An array to store URLs (strings), with atopindex. b. Methods:push(add URL),pop(remove and return top URL),peek(view top URL),isEmpty(check if empty). - In the
simulateBrowsermethod: a. Create a newStringStack. b. For each operation:- If "push", push the URL and print the current page (top of stack).
- If "pop", pop the top URL and print the new current page (top of stack) or "Stack empty" if empty.
- In the
mainmethod, test with sequences of push and pop operations, including empty stacks, single URLs, and null URLs.
Java Implementation
import java.util.*;
public class BrowserBackButtonSimulation {
// Custom stack implementation for strings
static class StringStack {
private String[] array;
private int top;
private static final int DEFAULT_SIZE = 1000;
public StringStack() {
array = new String[DEFAULT_SIZE];
top = -1;
}
public boolean push(String url) {
if (top == array.length - 1) {
return false; // Stack full
}
array[++top] = url;
return true;
}
public String pop() {
if (top == -1) {
return null; // Stack empty
}
return array[top--];
}
public String peek() {
if (top == -1) {
return null; // Stack empty
}
return array[top];
}
public boolean isEmpty() {
return top == -1;
}
}
// Helper class for operations
static class Operation {
String type;
String url; // For push operations
Operation(String type, String url) {
this.type = type;
this.url = url;
}
}
// Simulates browser back button functionality
public void simulateBrowser(List<Operation> operations) {
StringStack stack = new StringStack();
for (Operation op : operations) {
if (op.type.equals("push")) {
boolean success = stack.push(op.url);
if (success) {
System.out.println("Pushed " + op.url + ", Current page: " + stack.peek());
} else {
System.out.println("Push " + op.url + " failed: Stack full");
}
} else if (op.type.equals("pop")) {
String popped = stack.pop();
if (popped == null) {
System.out.println("Popped, Stack empty");
} else {
System.out.println("Popped " + popped + ", Current page: " +
(stack.peek() != null ? stack.peek() : "Stack empty"));
}
}
}
}
// Main method to test browser simulation
public static void main(String[] args) {
BrowserBackButtonSimulation simulator = new BrowserBackButtonSimulation();
// Test cases
List<List<Operation>> testCases = new ArrayList<>();
// Test case 1: Normal sequence
List<Operation> case1 = Arrays.asList(
new Operation("push", "https://page1.com"),
new Operation("push", "https://page2.com"),
new Operation("pop", null),
new Operation("push", "https://page3.com"),
new Operation("pop", null)
);
testCases.add(case1);
// Test case 2: Empty stack
List<Operation> case2 = Arrays.asList(
new Operation("pop", null)
);
testCases.add(case2);
// Test case 3: Single URL
List<Operation> case3 = Arrays.asList(
new Operation("push", "https://single.com"),
new Operation("pop", null)
);
testCases.add(case3);
// Test case 4: Multiple pushes
List<Operation> case4 = Arrays.asList(
new Operation("push", "https://site1.com"),
new Operation("push", "https://site2.com"),
new Operation("push", "https://site3.com"),
new Operation("pop", null),
new Operation("pop", null)
);
testCases.add(case4);
// Test case 5: Null URL
List<Operation> case5 = Arrays.asList(
new Operation("push", null),
new Operation("pop", null)
);
testCases.add(case5);
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
System.out.println("Test case " + (i + 1) + ":");
simulator.simulateBrowser(testCases.get(i));
System.out.println();
}
}
}
Output
Running the main method produces:
Test case 1:
Pushed https://page1.com, Current page: https://page1.com
Pushed https://page2.com, Current page: https://page2.com
Popped https://page2.com, Current page: https://page1.com
Pushed https://page3.com, Current page: https://page3.com
Popped https://page3.com, Current page: https://page1.com
Test case 2:
Popped, Stack empty
Test case 3:
Pushed https://single.com, Current page: https://single.com
Popped https://single.com, Current page: Stack empty
Test case 4:
Pushed https://site1.com, Current page: https://site1.com
Pushed https://site2.com, Current page: https://site2.com
Pushed https://site3.com, Current page: https://site3.com
Popped https://site3.com, Current page: https://site2.com
Popped https://site2.com, Current page: https://site1.com
Test case 5:
Pushed null, Current page: null
Popped null, Current page: Stack empty
Explanation:
- Test case 1: Pushes "page1", "page2", pops to "page1", pushes "page3", pops to "page1".
- Test case 2: Pop on empty stack returns "Stack empty".
- Test case 3: Pushes single URL, pops to empty stack.
- Test case 4: Multiple pushes, then pops back to "site1".
- Test case 5: Pushes null URL, pops to empty stack.
How It Works
- StringStack:
- Uses an array to store URLs, with
toptracking the latest element. push: Adds a URL if the stack isn’t full.pop: Removes and returns the top URL if not empty.peek: Returns the top URL without removing it.isEmpty: Checks if the stack is empty.
- Uses an array to store URLs, with
- simulateBrowser:
- Pushes URLs to simulate navigation, prints current page.
- Pops URLs to simulate back button, prints new current page or "Stack empty".
- Example Trace (Test case 1):
- Push "page1": stack = ["page1"], current = "page1".
- Push "page2": stack = ["page1", "page2"], current = "page2".
- Pop: Removes "page2", current = "page1".
- Push "page3": stack = ["page1", "page3"], current = "page3".
- Pop: Removes "page3", current = "page1".
- Main Method: Tests normal sequences, empty stack, single URL, multiple pushes, and null URL.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Push/Pop/Peek | O(1) | O(1) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the number of operations.
- Time complexity: O(1) for each push, pop, or peek; O(n) for processing n operations.
- Space complexity: O(n) for the stack storing up to n URLs.
- Worst case: O(n) time and O(n) space for many push operations.
✅ Tip: Use a stack to simulate browser navigation, as its LIFO nature naturally tracks the history of visited pages. Test with sequences that include multiple pops to verify the back button functionality.
⚠ Warning: Handle null URLs and empty stack cases to avoid unexpected behavior. Ensure the stack size is sufficient to accommodate the input sequence.
Infix to Postfix Conversion
Problem Statement
Write a Java program that converts an infix expression (e.g., "A + B * C") to its postfix equivalent (e.g., "A B C * +") using a stack. The program should handle operators (+, -, *, /) and parentheses, pushing operators onto the stack based on precedence and popping them to the output when appropriate. Test the implementation with at least three different expressions, including simple expressions, expressions with parentheses, and complex expressions with multiple operators. You can visualize this as rearranging a mathematical sentence so that the operations are performed in the correct order without needing parentheses, like organizing tasks in a queue where the work gets done step by step.
Input: A string representing an infix expression (e.g., "A + B * C", "(A + B) * C"). Output: A string representing the postfix expression (e.g., "A B C * +", "A B + C *"). Constraints:
- The input string length is between 0 and 10^5.
- The string contains operands (single letters A-Z), operators (+, -, *, /), parentheses (()), and spaces.
- The input may be empty, null, or invalid (handled gracefully). Example:
- Input:
"A + B * C" - Output:
"A B C * +" - Explanation: * has higher precedence than +, so B * C is processed first, resulting in A B C * +.
- Input:
"(A + B) * C" - Output:
"A B + C *" - Explanation: Parentheses ensure A + B is evaluated first, then multiplied by C.
- Input:
"A + B - C" - Output:
"A B + C -" - Explanation: Operators + and - have equal precedence, processed left to right.
Pseudocode
CLASS CharStack
SET array to new character array of size 1000
SET top to -1
FUNCTION push(char)
IF top equals array length - 1 THEN
RETURN false
ENDIF
INCREMENT top
SET array[top] to char
RETURN true
ENDFUNCTION
FUNCTION pop()
IF top equals -1 THEN
RETURN null
ENDIF
SET char to array[top]
DECREMENT top
RETURN char
ENDFUNCTION
FUNCTION peek()
IF top equals -1 THEN
RETURN null
ENDIF
RETURN array[top]
ENDFUNCTION
FUNCTION isEmpty()
RETURN top equals -1
ENDFUNCTION
ENDCLASS
FUNCTION getPrecedence(operator)
IF operator is '*' or '/' THEN
RETURN 2
ELSE IF operator is '+' or '-' THEN
RETURN 1
ELSE
RETURN 0
ENDIF
ENDFUNCTION
FUNCTION infixToPostfix(expression)
IF expression is null or empty THEN
RETURN empty string
ENDIF
CREATE stack as new CharStack
CREATE result as new StringBuilder
FOR each char in expression
IF char is letter THEN
APPEND char to result
ELSE IF char is '(' THEN
PUSH char to stack
ELSE IF char is ')' THEN
WHILE stack is not empty and stack.peek() is not '('
APPEND stack.pop() to result
ENDWHILE
IF stack is not empty THEN
POP '(' from stack
ENDIF
ELSE IF char is operator (+, -, *, /) THEN
WHILE stack is not empty and stack.peek() is not '(' and getPrecedence(stack.peek()) >= getPrecedence(char)
APPEND stack.pop() to result
ENDWHILE
PUSH char to stack
ENDIF
ENDFOR
WHILE stack is not empty
APPEND stack.pop() to result
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testExpressions to array of infix expressions
FOR each expression in testExpressions
PRINT input expression
CALL infixToPostfix(expression)
PRINT postfix result
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
CharStackclass with methods:push,pop,peek,isEmpty. - Define a
getPrecedencefunction to return operator precedence: 2 for *, /; 1 for +, -; 0 for others. - In the
infixToPostfixmethod: a. If the input is null or empty, return an empty string. b. Create a stack for operators and a StringBuilder for the result. c. For each character in the expression:- If it’s a letter (operand), append it to the result.
- If it’s '(', push it to the stack.
- If it’s ')', pop operators from the stack to the result until '(' is found, then pop '('.
- If it’s an operator, pop higher or equal precedence operators from the stack to the result, then push the current operator. d. After processing, pop remaining operators from the stack to the result. e. Return the result as a string.
- In the
mainmethod, test with at least three expressions, including simple, parenthesized, and complex cases.
Java Implementation
public class InfixToPostfixConversion {
// Custom stack implementation for characters
static class CharStack {
private char[] array;
private int top;
private static final int DEFAULT_SIZE = 1000;
public CharStack() {
array = new char[DEFAULT_SIZE];
top = -1;
}
public boolean push(char c) {
if (top == array.length - 1) {
return false; // Stack full
}
array[++top] = c;
return true;
}
public Character pop() {
if (top == -1) {
return null; // Stack empty
}
return array[top--];
}
public Character peek() {
if (top == -1) {
return null; // Stack empty
}
return array[top];
}
public boolean isEmpty() {
return top == -1;
}
}
// Returns precedence of operators
private int getPrecedence(char operator) {
if (operator == '*' || operator == '/') {
return 2;
} else if (operator == '+' || operator == '-') {
return 1;
}
return 0;
}
// Converts infix expression to postfix
public String infixToPostfix(String expression) {
if (expression == null || expression.isEmpty()) {
return "";
}
CharStack stack = new CharStack();
StringBuilder result = new StringBuilder();
for (char c : expression.replaceAll("\\s", "").toCharArray()) {
if (Character.isLetter(c)) {
result.append(c);
} else if (c == '(') {
stack.push(c);
} else if (c == ')') {
while (!stack.isEmpty() && stack.peek() != '(') {
result.append(stack.pop());
}
if (!stack.isEmpty()) {
stack.pop(); // Remove '('
}
} else if (c == '+' || c == '-' || c == '*' || c == '/') {
while (!stack.isEmpty() && stack.peek() != '(' &&
getPrecedence(stack.peek()) >= getPrecedence(c)) {
result.append(stack.pop());
}
stack.push(c);
}
}
while (!stack.isEmpty()) {
result.append(stack.pop());
}
return result.toString();
}
// Main method to test infix to postfix conversion
public static void main(String[] args) {
InfixToPostfixConversion converter = new InfixToPostfixConversion();
// Test cases
String[] testExpressions = {
"A + B * C", // Simple expression
"(A + B) * C", // Expression with parentheses
"A + B * C - D / E", // Complex expression
"", // Empty string
"X * (Y + Z)" // Nested parentheses
};
for (int i = 0; i < testExpressions.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Infix: \"" + testExpressions[i] + "\"");
String result = converter.infixToPostfix(testExpressions[i]);
System.out.println("Postfix: \"" + result + "\"\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Infix: "A + B * C"
Postfix: "ABC*+"
Test case 2:
Infix: "(A + B) * C"
Postfix: "AB+C*"
Test case 3:
Infix: "A + B * C - D / E"
Postfix: "ABC*+DE/-"
Test case 4:
Infix: ""
Postfix: ""
Test case 5:
Infix: "X * (Y + Z)"
Postfix: "XYZ+*"
Explanation:
- Test case 1:
"A + B * C"→ * has higher precedence, so B * C becomes "BC*", then + gives "ABC*+". - Test case 2:
"(A + B) * C"→ Parentheses prioritize A + B, giving "AB+", then * gives "AB+C*". - Test case 3:
"A + B * C - D / E"→ * and / have higher precedence, processed left to right, then + and -, giving "ABC*+DE/-". - Test case 4:
""→ Empty input returns empty string. - Test case 5:
"X * (Y + Z)"→ Parentheses prioritize Y + Z, giving "YZ+", then * gives "XYZ+*".
How It Works
- CharStack:
- Stores operators and parentheses, with methods
push,pop,peek,isEmpty.
- Stores operators and parentheses, with methods
- getPrecedence:
- Assigns precedence: 2 for *, /; 1 for +, -; 0 for others.
- infixToPostfix:
- Outputs operands (letters) directly to the result.
- Pushes '(' to stack, pops operators until ')' for closing parentheses.
- For operators, pops higher/equal precedence operators, then pushes current operator.
- Pops remaining operators at the end.
- Example Trace (Test case 1):
- Input:
"A + B * C". - A: Append to result → "A".
- +: Push to stack → [+].
- B: Append to result → "AB".
- *: Higher precedence than +, push → [+, *].
- C: Append to result → "ABC".
- End: Pop * → "ABC*", pop + → "ABC*+".
- Input:
- Main Method: Tests simple, parenthesized, complex, empty, and nested expressions.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Push/Pop/Peek | O(1) | O(1) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the length of the input expression.
- Time complexity: O(n) for iterating through the expression, with O(1) for each stack operation.
- Space complexity: O(n) for the stack (worst case: all operators/parentheses) and StringBuilder.
- Worst case: O(n) time and O(n) space for expressions like
"((A+B)*C)".
✅ Tip: Use a stack to manage operator precedence in infix-to-postfix conversion, as it naturally handles the order of operations. Test with parentheses and multiple operator types to ensure correctness.
⚠ Warning: Handle invalid inputs (e.g., unbalanced parentheses) gracefully to avoid stack underflow. Remove spaces from the input to simplify processing.
Parentheses Checker
Problem Statement
Write a Java program that uses a stack to check if a string of parentheses, including round '()', curly '{}', and square '[]' brackets, is balanced. The program should push opening brackets onto the stack and pop them when a matching closing bracket is found, returning true if the string is balanced and false otherwise. Test the implementation with various inputs, including valid and invalid parentheses strings, empty strings, and strings with non-bracket characters. You can visualize this as stacking open locks and checking if each closing lock perfectly matches and removes its corresponding open lock, ensuring no locks are left unmatched.
Input: A string containing parentheses and possibly other characters (e.g., "{[()]}", "((}", "").
Output: A boolean indicating if the parentheses are balanced (e.g., true for "{[()]}", false for "((}").
Constraints:
- String length is between 0 and 10^5.
- The string may contain '(', ')', '{', '}', '[', ']', and other ASCII characters.
- The input may be empty or null. Example:
- Input:
"{[()]}" - Output:
true - Explanation: Each opening bracket has a matching closing bracket in the correct order: { → }, [ → ], ( → ).
- Input:
"((}" - Output:
false - Explanation: The second '(' has no matching ')', and '}' has no matching '{'.
- Input:
"" - Output:
true - Explanation: An empty string is considered balanced.
Pseudocode
CLASS CharStack
SET array to new character array of size 1000
SET top to -1
FUNCTION push(char)
IF top equals array length - 1 THEN
RETURN false (stack full)
ENDIF
INCREMENT top
SET array[top] to char
RETURN true
ENDFUNCTION
FUNCTION pop()
IF top equals -1 THEN
RETURN null (stack empty)
ENDIF
SET char to array[top]
DECREMENT top
RETURN char
ENDFUNCTION
FUNCTION isEmpty()
RETURN top equals -1
ENDFUNCTION
ENDCLASS
FUNCTION isBalanced(input)
IF input is null THEN
RETURN true
ENDIF
CREATE stack as new CharStack
FOR each char in input
IF char is '(', '{', or '[' THEN
PUSH char to stack
ELSE IF char is ')', '}', or ']' THEN
IF stack is empty THEN
RETURN false
ENDIF
SET popped to stack.pop()
IF popped does not match char THEN
RETURN false
ENDIF
ENDIF
ENDFOR
RETURN stack is empty
ENDFUNCTION
FUNCTION main()
SET testStrings to array of strings including various cases
FOR each string in testStrings
PRINT input string
CALL isBalanced(string)
PRINT whether string is balanced
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
CharStackclass with: a. An array to store characters, with atopindex. b. Methods:push(add character),pop(remove and return character),isEmpty(check if empty). - In the
isBalancedmethod: a. If the input is null, return true (considered balanced). b. Create a newCharStack. c. Iterate through each character in the input:- If it’s an opening bracket ('(', '{', '['), push it onto the stack.
- If it’s a closing bracket (')', '}', ']'):
- If the stack is empty, return false (no matching opening bracket).
- Pop the top character and check if it matches the closing bracket.
- If no match (e.g., '(' with '}'), return false. d. After iteration, return true if the stack is empty (all brackets matched), false otherwise.
- In the
mainmethod, test with valid, invalid, empty, and non-bracket strings, printing the input and result.
Java Implementation
public class ParenthesesChecker {
// Custom stack implementation for characters
static class CharStack {
private char[] array;
private int top;
private static final int DEFAULT_SIZE = 1000;
public CharStack() {
array = new char[DEFAULT_SIZE];
top = -1;
}
public boolean push(char c) {
if (top == array.length - 1) {
return false; // Stack full
}
array[++top] = c;
return true;
}
public Character pop() {
if (top == -1) {
return null; // Stack empty
}
return array[top--];
}
public boolean isEmpty() {
return top == -1;
}
}
// Checks if a string of parentheses is balanced
public boolean isBalanced(String input) {
if (input == null) {
return true;
}
CharStack stack = new CharStack();
for (char c : input.toCharArray()) {
if (c == '(' || c == '{' || c == '[') {
stack.push(c);
} else if (c == ')' || c == '}' || c == ']') {
if (stack.isEmpty()) {
return false; // No matching opening bracket
}
Character popped = stack.pop();
if (popped == null || !isMatchingPair(popped, c)) {
return false; // Mismatched brackets
}
}
}
return stack.isEmpty(); // Balanced only if stack is empty
}
// Helper method to check if brackets match
private boolean isMatchingPair(char open, char close) {
return (open == '(' && close == ')') ||
(open == '{' && close == '}') ||
(open == '[' && close == ']');
}
// Main method to test parentheses checker
public static void main(String[] args) {
ParenthesesChecker checker = new ParenthesesChecker();
// Test cases
String[] testStrings = {
"{[()]}", // Valid, balanced
"((}", // Invalid, mismatched
"", // Empty string
"abc", // No brackets
"({[]})", // Valid, nested
"([)", // Invalid, unmatched
null, // Null input
"((()))" // Valid, deeply nested
};
for (int i = 0; i < testStrings.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input: \"" + (testStrings[i] != null ? testStrings[i] : "null") + "\"");
boolean result = checker.isBalanced(testStrings[i]);
System.out.println("Balanced: " + result + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input: "{[()]}"
Balanced: true
Test case 2:
Input: "((}"
Balanced: false
Test case 3:
Input: ""
Balanced: true
Test case 4:
Input: "abc"
Balanced: true
Test case 5:
Input: "({[]})"
Balanced: true
Test case 6:
Input: "([)"
Balanced: false
Test case 7:
Input: "null"
Balanced: true
Test case 8:
Input: "((()))"
Balanced: true
Explanation:
- Test case 1:
"{[()]}"→ All brackets match: {→}, [→], (→) →true. - Test case 2:
"((}"→ Second '(' unmatched, '}' has no '{' →false. - Test case 3:
""→ Empty string, no brackets →true. - Test case 4:
"abc"→ No brackets, ignored →true. - Test case 5:
"({[]})"→ All brackets match in nested order →true. - Test case 6:
"([)"→ '[' does not match ')' →false. - Test case 7:
null→ Considered balanced →true. - Test case 8:
"((()))"→ Deeply nested, all match →true.
How It Works
- CharStack:
- Uses an array to store characters, with
toptracking the latest element. push: Adds an opening bracket if the stack isn’t full.pop: Removes and returns the top bracket if the stack isn’t empty.isEmpty: Checks if the stack is empty.
- Uses an array to store characters, with
- isBalanced:
- Pushes opening brackets ('(', '{', '[') onto the stack.
- For closing brackets, pops the top bracket and checks for a match.
- Returns false if the stack is empty when a closing bracket is found or if brackets don’t match.
- Returns true if the stack is empty after processing (all brackets matched).
- Example Trace (Test case 1):
- Input:
"{[()]}". - Push '{': stack = ['{'].
- Push '[': stack = ['{', '['].
- Push '(': stack = ['{', '[', '('].
- Pop for ')': Matches '(', stack = ['{', '['].
- Pop for ']': Matches '[', stack = ['{'].
- Pop for '}': Matches '{', stack = [].
- Stack empty →
true.
- Input:
- Main Method: Tests valid, invalid, empty, non-bracket, and null inputs.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Push (per char) | O(1) | O(1) |
| Pop (per char) | O(1) | O(1) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the length of the input string.
- Time complexity: O(n) for iterating through the string, with O(1) for each push/pop operation.
- Space complexity: O(n) for the stack in the worst case (all opening brackets).
- Worst case: O(n) time and O(n) space for a string like
"(((((...".
✅ Tip: Use a stack to check balanced parentheses, as its LIFO nature naturally tracks nested brackets. Test with nested, mismatched, and non-bracket inputs to ensure robustness.
⚠ Warning: Handle null inputs and empty stacks to avoid
NullPointerExceptionor incorrect results. Ensure matching logic covers all bracket types ('()', '{}', '[]').
Stack Min Function
Problem Statement
Write a Java program that extends a stack implementation to include a min() function that returns the minimum element in the stack in O(1) time. Use an additional stack to track the minimum elements, pushing the current minimum when a new element is added and popping it when an element is removed. The program should support standard stack operations (push, pop, isEmpty) and the min function, handling integers as stack elements. Test the implementation with various sequences of push and pop operations, including edge cases like empty stacks and single-element stacks. You can visualize this as maintaining a leaderboard alongside a stack of scores, where the leaderboard always shows the lowest score at a glance without recalculating.
Input: A sequence of operations (push, pop, min) on a stack of integers (e.g., push 3, push 5, min, push 2, min, pop, min). Output: Results of operations, including the minimum element when requested (e.g., min returns 3, then 2, then 3). Constraints:
- Stack size is between 0 and 10^5.
- Elements are integers in the range [-10^9, 10^9].
- The stack may be empty when
minorpopis called. Example: - Input: Operations = [push(3), push(5), min, push(2), min, pop, min]
- Output: [-, -, 3, -, 2, -, 3]
- Explanation: Push 3 (min=3), push 5 (min=3), min returns 3, push 2 (min=2), min returns 2, pop 2 (min=3), min returns 3.
- Input: Operations = [min on empty stack]
- Output: [null]
- Explanation: Min on empty stack returns null.
Pseudocode
CLASS MinStack
SET dataStack to new integer array of size 1000
SET minStack to new integer array of size 1000
SET top to -1
FUNCTION push(value)
IF top equals dataStack length - 1 THEN
RETURN false (stack full)
ENDIF
INCREMENT top
SET dataStack[top] to value
IF minStack is empty OR value is less than or equal to minStack[top] THEN
PUSH value to minStack
ENDIF
RETURN true
ENDFUNCTION
FUNCTION pop()
IF top equals -1 THEN
RETURN null (stack empty)
ENDIF
SET value to dataStack[top]
IF value equals minStack[top] THEN
POP from minStack
ENDIF
DECREMENT top
RETURN value
ENDFUNCTION
FUNCTION min()
IF top equals -1 THEN
RETURN null (stack empty)
ENDIF
RETURN minStack[top]
ENDFUNCTION
FUNCTION isEmpty()
RETURN top equals -1
ENDFUNCTION
ENDCLASS
FUNCTION testMinStack(operations)
CREATE stack as new MinStack
FOR each operation in operations
IF operation.type equals "push" THEN
CALL stack.push(operation.value)
PRINT push result
ELSE IF operation.type equals "pop" THEN
SET result to stack.pop()
PRINT popped value
ELSE IF operation.type equals "min" THEN
SET result to stack.min()
PRINT minimum value
ENDIF
ENDFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of operation sequences
FOR each testCase in testCases
PRINT test case details
CALL testMinStack(testCase)
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
MinStackclass with: a. Two arrays:dataStackfor values,minStackfor tracking minimums. b. Atopindex for both stacks. c. Methods:push(add value and update minStack),pop(remove value and update minStack),min(return top of minStack),isEmpty(check if empty). - In
push(value): a. If stack is full, return false. b. Add value todataStack. c. IfminStackis empty or value ≤ current minimum, push value tominStack. - In
pop(): a. If stack is empty, return null. b. If popped value equals current minimum, pop fromminStack. c. Return popped value. - In
min(): a. If stack is empty, return null. b. Return top ofminStack. - In the
mainmethod, test with sequences of push, pop, and min operations, including empty stacks and single-element cases.
Java Implementation
import java.util.*;
public class StackMinFunction {
// MinStack class with min() function
static class MinStack {
private int[] dataStack;
private int[] minStack;
private int top;
private static final int DEFAULT_SIZE = 1000;
public MinStack() {
dataStack = new int[DEFAULT_SIZE];
minStack = new int[DEFAULT_SIZE];
top = -1;
}
public boolean push(int value) {
if (top == dataStack.length - 1) {
return false; // Stack full
}
dataStack[++top] = value;
if (top == 0 || value <= minStack[top - 1]) {
minStack[top] = value; // Push to minStack if value is new minimum
} else {
minStack[top] = minStack[top - 1]; // Copy previous minimum
}
return true;
}
public Integer pop() {
if (top == -1) {
return null; // Stack empty
}
return dataStack[top--]; // Pop from dataStack, minStack follows
}
public Integer min() {
if (top == -1) {
return null; // Stack empty
}
return minStack[top]; // Return current minimum
}
public boolean isEmpty() {
return top == -1;
}
}
// Helper class for operations
static class Operation {
String type;
Integer value; // For push operations
Operation(String type, Integer value) {
this.type = type;
this.value = value;
}
}
// Tests the MinStack with a sequence of operations
public void testMinStack(List<Operation> operations) {
MinStack stack = new MinStack();
for (Operation op : operations) {
if (op.type.equals("push")) {
boolean success = stack.push(op.value);
System.out.println("Push " + op.value + ": " + (success ? "Success" : "Failed"));
} else if (op.type.equals("pop")) {
Integer result = stack.pop();
System.out.println("Pop: " + (result != null ? result : "null"));
} else if (op.type.equals("min")) {
Integer result = stack.min();
System.out.println("Min: " + (result != null ? result : "null"));
}
}
}
// Main method to test MinStack
public static void main(String[] args) {
StackMinFunction tester = new StackMinFunction();
// Test cases
List<List<Operation>> testCases = new ArrayList<>();
// Test case 1: Normal sequence
List<Operation> case1 = Arrays.asList(
new Operation("push", 3),
new Operation("push", 5),
new Operation("min", null),
new Operation("push", 2),
new Operation("min", null),
new Operation("pop", null),
new Operation("min", null)
);
testCases.add(case1);
// Test case 2: Empty stack
List<Operation> case2 = Arrays.asList(
new Operation("min", null),
new Operation("pop", null)
);
testCases.add(case2);
// Test case 3: Single element
List<Operation> case3 = Arrays.asList(
new Operation("push", 1),
new Operation("min", null)
);
testCases.add(case3);
// Test case 4: Large sequence
List<Operation> case4 = new ArrayList<>();
case4.add(new Operation("push", 10));
case4.add(new Operation("push", 5));
case4.add(new Operation("push", 15));
case4.add(new Operation("min", null));
case4.add(new Operation("pop", null));
case4.add(new Operation("min", null));
testCases.add(case4);
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
System.out.println("Test case " + (i + 1) + ":");
tester.testMinStack(testCases.get(i));
System.out.println();
}
}
}
Output
Running the main method produces:
Test case 1:
Push 3: Success
Push 5: Success
Min: 3
Push 2: Success
Min: 2
Pop: 2
Min: 3
Test case 2:
Min: null
Pop: null
Test case 3:
Push 1: Success
Min: 1
Test case 4:
Push 10: Success
Push 5: Success
Push 15: Success
Min: 5
Pop: 15
Min: 5
Explanation:
- Test case 1: Push 3 (min=3), push 5 (min=3), min returns 3, push 2 (min=2), min returns 2, pop 2 (min=3), min returns 3.
- Test case 2: Min and pop on empty stack return null.
- Test case 3: Push 1 (min=1), min returns 1.
- Test case 4: Push 10 (min=10), push 5 (min=5), push 15 (min=5), min returns 5, pop 15 (min=5), min returns 5.
How It Works
- MinStack:
- Uses two arrays:
dataStackfor values,minStackfor minimums. push: Adds value todataStack; pushes tominStackif value ≤ current minimum, else copies current minimum.pop: Removes value fromdataStack; adjustsminStackto maintain minimum.min: Returns top ofminStackin O(1) time.isEmpty: Checks if stack is empty.
- Uses two arrays:
- Example Trace (Test case 1):
- Push 3:
dataStack=[3],minStack=[3],top=0. - Push 5:
dataStack=[3,5],minStack=[3,3],top=1. - Min: Returns 3.
- Push 2:
dataStack=[3,5,2],minStack=[3,3,2],top=2. - Min: Returns 2.
- Pop: Removes 2,
dataStack=[3,5],minStack=[3,3],top=1. - Min: Returns 3.
- Push 3:
- Main Method: Tests normal sequences, empty stacks, single elements, and larger sequences.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Push | O(1) | O(1) |
| Pop | O(1) | O(1) |
| Min | O(1) | O(1) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the number of operations.
- Time complexity: O(1) for push, pop, and min operations.
- Space complexity: O(n) for both stacks, as each push may add to both.
- Worst case: O(n) space for all operations, O(1) time per operation.
✅ Tip: Use an additional stack to track minimums for O(1) min queries, pushing the minimum at each step. Test with sequences that include repeated minimums to verify correctness.
⚠ Warning: Ensure both stacks are synchronized during push and pop to avoid incorrect minimums. Handle empty stack cases to prevent null pointer issues.
StringBuilder Capacity Management
Problem Statement
Write a Java program that demonstrates StringBuilder’s capacity resizing by appending strings and monitoring capacity changes using the capacity() method. The program should analyze when resizing occurs and how the capacity adjusts based on the appended content. Test the implementation with different scenarios, including appending small strings, large strings, and using StringBuilder instances with varying initial capacities. You can visualize this as filling a stretchable notebook, where the notebook expands its pages (capacity) automatically when you add more notes, and you track how and when it grows to accommodate the text.
Input: A series of strings to append to StringBuilder instances with different initial configurations (e.g., default capacity, specified initial capacity). Output: The final string, a log of capacity changes after each append operation, and an analysis of when resizing occurs. Constraints:
- String lengths are between 0 and 10^5.
- Strings contain printable ASCII characters.
- Initial capacities are non-negative integers.
- The input strings may be null or empty. Example:
- Input: Append
"hello"," world"to a StringBuilder with default capacity (16). - Output:
Initial capacity: 16 After appending "hello": length=5, capacity=16 After appending " world": length=11, capacity=16 Final string: "hello world" - Explanation: No resizing occurs as the total length (11) fits within the initial capacity (16).
- Input: Append a 20-character string to a StringBuilder with default capacity (16).
- Output:
Initial capacity: 16 After appending: length=20, capacity=34 Final string: "abcdefghijklmnopqrst" - Explanation: Resizing occurs because 20 exceeds 16, new capacity = (16 * 2) + 2 = 34.
Pseudocode
FUNCTION demonstrateCapacityManagement(testCases)
FOR each testCase in testCases
SET initialCapacity to testCase.initialCapacity
SET strings to testCase.strings
CREATE stringBuilder with initialCapacity
PRINT initial capacity
FOR each string in strings
SET oldCapacity to stringBuilder.capacity()
APPEND string to stringBuilder
SET newCapacity to stringBuilder.capacity()
PRINT length, capacity, and whether resizing occurred
ENDFOR
PRINT final string
ENDFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of (initialCapacity, strings) pairs
CALL demonstrateCapacityManagement(testCases)
ENDFUNCTION
Algorithm Steps
- Define a method to demonstrate capacity management:
a. For each test case, create a StringBuilder with the specified initial capacity.
b. Print the initial capacity using
capacity(). c. For each string to append:- Record the current capacity.
- Append the string using
StringBuilder.append. - Check the new capacity and note if resizing occurred (capacity increased).
- Print the current length, capacity, and resizing status. d. Print the final string.
- Handle null or empty strings by skipping or appending empty content.
- In the
mainmethod, test with different scenarios:- Default capacity (16) with small strings.
- Default capacity with a large string that triggers resizing.
- Custom initial capacity with multiple appends.
- Empty or null strings.
- Analyze resizing: StringBuilder doubles the current capacity and adds 2 when the length exceeds the capacity.
Java Implementation
public class StringBuilderCapacityManagement {
// Demonstrates StringBuilder capacity resizing
public void demonstrateCapacityManagement(String[][] testCases, int[] initialCapacities) {
for (int i = 0; i < testCases.length; i++) {
String[] strings = testCases[i];
int initialCapacity = initialCapacities[i];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Initial capacity: " + initialCapacity);
StringBuilder sb = new StringBuilder(initialCapacity);
for (int j = 0; j < strings.length; j++) {
String str = strings[j] != null ? strings[j] : "";
int oldCapacity = sb.capacity();
sb.append(str);
int newCapacity = sb.capacity();
System.out.println("After appending \"" + (str.length() > 20 ? str.substring(0, 20) + "..." : str) + "\": " +
"length=" + sb.length() + ", capacity=" + newCapacity +
(newCapacity > oldCapacity ? " (resized)" : ""));
}
System.out.println("Final string: \"" + (sb.length() > 50 ? sb.substring(0, 50) + "..." : sb.toString()) + "\"\n");
}
}
// Main method to test capacity management
public static void main(String[] args) {
StringBuilderCapacityManagement manager = new StringBuilderCapacityManagement();
// Test cases
String[][] testCases = {
{"hello", " world"}, // Small strings, default capacity
{generateLargeString(20)}, // Large string, triggers resizing
{"a", "b", "c", "d"}, // Multiple small strings, custom capacity
{""}, // Empty string
{null} // Null string
};
int[] initialCapacities = {16, 16, 5, 16, 16}; // Corresponding initial capacities
manager.demonstrateCapacityManagement(testCases, initialCapacities);
}
// Helper method to generate a large string
private static String generateLargeString(int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append((char) ('a' + (i % 26)));
}
return sb.toString();
}
}
Output
Running the main method produces (actual capacities may vary slightly depending on JVM implementation):
Test case 1:
Initial capacity: 16
After appending "hello": length=5, capacity=16
After appending " world": length=11, capacity=16
Final string: "hello world"
Test case 2:
Initial capacity: 16
After appending "abcdefghijklmnopqrst...": length=20, capacity=34 (resized)
Final string: "abcdefghijklmnopqrst"
Test case 3:
Initial capacity: 5
After appending "a": length=1, capacity=5
After appending "b": length=2, capacity=5
After appending "c": length=3, capacity=5
After appending "d": length=4, capacity=5
Final string: "abcd"
Test case 4:
Initial capacity: 16
After appending "": length=0, capacity=16
Final string: ""
Test case 5:
Initial capacity: 16
After appending "": length=0, capacity=16
Final string: ""
Explanation:
- Test case 1: Appends
"hello"," world". Length (11) < capacity (16), no resizing. - Test case 2: Appends 20-character string. Length (20) > 16, resizes to (16 * 2) + 2 = 34.
- Test case 3: Custom capacity 5, appends four single characters. Length (4) ≤ 5, no resizing.
- Test case 4: Empty string, no change in length or capacity.
- Test case 5: Null string treated as empty, no change.
How It Works
- StringBuilder Capacity: Represents the size of the internal character array. Default is 16 if not specified.
- Resizing: When the length exceeds capacity, StringBuilder allocates a new array with capacity = (oldCapacity * 2) + 2.
- demonstrateCapacityManagement:
- Creates StringBuilder with specified initial capacity.
- Tracks capacity before and after each append.
- Reports resizing when new capacity exceeds old capacity.
- Example Trace (Test case 2):
- Initial:
capacity = 16,length = 0. - Append
"abcdefghijklmnopqrst"(20 chars):length = 20> 16, new capacity = (16 * 2) + 2 = 34.
- Initial:
- Main Method: Tests with small strings, large strings triggering resizing, custom capacity, and edge cases.
- Analysis: Resizing occurs when the total length after appending exceeds the current capacity, doubling the capacity plus 2 to accommodate future appends efficiently.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Append | O(m) | O(m) |
| Capacity Check | O(1) | O(1) |
| Resizing | O(n) | O(n) |
| Full Algorithm | O(k * m) | O(n + k * m) |
Note:
- n is the current length of the StringBuilder, m is the length of the appended string, k is the number of append operations.
- Append: O(m) for copying characters, amortized O(1) if no resizing.
- Resizing: O(n) when copying the internal array to a larger one.
- Full algorithm: O(k * m) time for k appends, O(n + k * m) space for the final StringBuilder content.
- Worst case: Frequent resizing for large strings increases time and space.
✅ Tip: Set an appropriate initial capacity for StringBuilder when the expected size is known to minimize resizing. Use
capacity()to monitor and understand resizing behavior during development.
⚠ Warning: Avoid unnecessarily small initial capacities, as frequent resizing can degrade performance. Handle null inputs to prevent unexpected behavior during append operations.
Queues Problem Solving with DSA
🛠 What you will learn
This section outlines the format used for each DSA problem statement, designed to guide students through solving exercise problems in a clear, structured learning process. Each component serves a specific purpose to enhance understanding and practical application:
- Problem Statement: Clearly defines the task, including inputs, outputs, constraints, and an example. A real-world analogy makes the problem relatable, aligning with the engaging tone of the "Core Data Structures" summary, ensuring students grasp the problem’s context.
- Pseudocode: Provides a high-level, language-agnostic outline of the solution using standardized syntax (e.g.,
FUNCTION,IF,SET,RETURN). This helps students understand the logic before diving into code, bridging theory and implementation. - Algorithm Steps: Breaks down the pseudocode into detailed, actionable steps, connecting theoretical logic to practical coding. This ensures students can follow the implementation process systematically.
- Java Implementation: Offers a complete, commented Java code solution that students can study and run. It includes a
mainmethod with at least four test cases (e.g., target present, absent, middle element, and edge cases like duplicates or single elements) to verify correctness and explore different scenarios, maintaining a beginner-friendly approach. - Output: Shows the expected results from the test cases, including indices, comparisons, and execution times (where applicable), helping students verify their code’s correctness and understand the algorithm’s behavior.
- How It Works: Traces the algorithm’s execution with a detailed example, showing step-by-step how the search narrows down the range. This reinforces understanding by illustrating the algorithm in action.
- Complexity Analysis Table: Summarizes time and space complexity for best, average, and worst cases, teaching students to evaluate efficiency and compare trade-offs across implementations.
- Tip or Warning Box: Provides practical advice (e.g., when to use DSA) and highlights pitfalls (e.g., ensuring the array is sorted), guiding students toward best practices and common error avoidance.
Each exercise includes test cases to verify correctness and performance, aligning with a learning platform featuring embedded compilers for hands-on practice and visualizations to illustrate the halving process of DSA.
Circular Queue Test
Problem Statement
Write a Java program that implements a circular queue to handle edge cases, such as enqueueing and dequeuing in a loop, and tests it with a sequence of operations to verify circular behavior. The circular queue should reuse array space by wrapping around when the rear pointer reaches the end, and handle full and empty queue conditions. Test the implementation with various sequences of enqueue and dequeue operations, including wrap-around scenarios, full queues, and empty queues. You can visualize this as a circular conveyor belt where items are added and removed, and when the belt’s end is reached, it loops back to the start to continue adding items.
Input: A sequence of operations, where each operation is either:
- Enqueue: Add an integer to the queue (e.g., enqueue 5).
- Dequeue: Remove and return the next integer from the queue. Output: For each operation, print the action performed (enqueue or dequeue) and the result, including queue state or error messages for full/empty conditions. Constraints:
- Queue capacity is fixed (e.g., 5 elements for testing).
- Elements are integers in the range [-10^9, 10^9].
- The queue may be empty or full during operations. Example:
- Input: Operations = [enqueue(1), enqueue(2), enqueue(3), dequeue, enqueue(4), dequeue, enqueue(5)]
- Output:
Enqueued 1, Queue: [1] Enqueued 2, Queue: [1, 2] Enqueued 3, Queue: [1, 2, 3] Dequeued 1, Queue: [2, 3] Enqueued 4, Queue: [2, 3, 4] Dequeued 2, Queue: [3, 4] Enqueued 5, Queue: [3, 4, 5] - Explanation: The queue operates in FIFO order, reusing space as elements are dequeued.
- Input: Operations = [enqueue(1), enqueue(2), enqueue(3), enqueue(4), enqueue(5), enqueue(6) on capacity 5]
- Output:
Enqueue 6 failed: Queue full
Pseudocode
CLASS CircularQueue
SET array to new integer array of size capacity
SET front to 0
SET rear to -1
SET size to 0
SET capacity to input capacity
FUNCTION enqueue(number)
IF size equals capacity THEN
RETURN false (queue full)
ENDIF
SET rear to (rear + 1) mod capacity
SET array[rear] to number
INCREMENT size
RETURN true
ENDFUNCTION
FUNCTION dequeue()
IF size equals 0 THEN
RETURN null (queue empty)
ENDIF
SET number to array[front]
SET front to (front + 1) mod capacity
DECREMENT size
RETURN number
ENDFUNCTION
FUNCTION isEmpty()
RETURN size equals 0
ENDFUNCTION
FUNCTION isFull()
RETURN size equals capacity
ENDFUNCTION
FUNCTION toString()
CREATE result as empty string
APPEND "["
FOR i from 0 to size - 1
SET index to (front + i) mod capacity
APPEND array[index] and ", " to result
ENDFOR
IF result ends with ", " THEN
REMOVE last ", "
ENDIF
APPEND "]" to result
RETURN result
ENDFUNCTION
ENDCLASS
FUNCTION testCircularQueue(operations, capacity)
CREATE queue as new CircularQueue with capacity
FOR each operation in operations
IF operation.type equals "enqueue" THEN
IF queue.enqueue(operation.number) THEN
PRINT enqueued number and queue state
ELSE
PRINT enqueue failed: queue full
ENDIF
ELSE IF operation.type equals "dequeue" THEN
SET number to queue.dequeue()
IF number is null THEN
PRINT queue empty message
ELSE
PRINT dequeued number and queue state
ENDIF
ENDIF
ENFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of operation sequences with fixed capacity
FOR each testCase in testCases
PRINT test case details
CALL testCircularQueue(testCase, capacity)
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
CircularQueueclass with: a. An array of fixed capacity, withfront,rear, andsizeto track state. b. Methods:enqueue(add to rear with wrap-around),dequeue(remove from front with wrap-around),isEmpty,isFull,toString. - In
enqueue: a. If queue is full, return false. b. Incrementrearmodulo capacity, add number, increment size. - In
dequeue: a. If queue is empty, return null. b. Remove number atfront, incrementfrontmodulo capacity, decrement size. - In
testCircularQueue: a. Create aCircularQueuewith given capacity. b. For each operation:- Enqueue: Add number, print queue state or "Queue full".
- Dequeue: Remove number, print result and queue state or "Queue empty".
- In the
mainmethod, test with sequences demonstrating normal operations, wrap-around, full queue, and empty queue cases.
Java Implementation
import java.util.*;
public class CircularQueueTest {
// Custom circular queue implementation for integers
static class CircularQueue {
private int[] array;
private int front;
private int rear;
private int size;
private int capacity;
public CircularQueue(int capacity) {
this.array = new int[capacity];
this.front = 0;
this.rear = -1;
this.size = 0;
this.capacity = capacity;
}
public boolean enqueue(int number) {
if (size == capacity) {
return false; // Queue full
}
rear = (rear + 1) % capacity;
array[rear] = number;
size++;
return true;
}
public Integer dequeue() {
if (size == 0) {
return null; // Queue empty
}
int number = array[front];
front = (front + 1) % capacity;
size--;
return number;
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == capacity;
}
public String toString() {
StringBuilder result = new StringBuilder("[");
for (int i = 0; i < size; i++) {
int index = (front + i) % capacity;
result.append(array[index]);
if (i < size - 1) {
result.append(", ");
}
}
result.append("]");
return result.toString();
}
}
// Helper class for operations
static class Operation {
String type;
Integer number; // For enqueue operations
Operation(String type, Integer number) {
this.type = type;
this.number = number;
}
}
// Tests circular queue with operations
public void testCircularQueue(List<Operation> operations, int capacity) {
CircularQueue queue = new CircularQueue(capacity);
for (Operation op : operations) {
if (op.type.equals("enqueue")) {
if (queue.enqueue(op.number)) {
System.out.println("Enqueued " + op.number + ", Queue: " + queue);
} else {
System.out.println("Enqueue " + op.number + " failed: Queue full");
}
} else if (op.type.equals("dequeue")) {
Integer number = queue.dequeue();
if (number == null) {
System.out.println("Queue empty, cannot dequeue");
} else {
System.out.println("Dequeued " + number + ", Queue: " + queue);
}
}
}
}
// Main method to test circular queue
public static void main(String[] args) {
CircularQueueTest tester = new CircularQueueTest();
int capacity = 5; // Fixed capacity for all test cases
// Test cases
List<List<Operation>> testCases = new ArrayList<>();
// Test case 1: Normal sequence
List<Operation> case1 = Arrays.asList(
new Operation("enqueue", 1),
new Operation("enqueue", 2),
new Operation("enqueue", 3),
new Operation("dequeue", null),
new Operation("enqueue", 4),
new Operation("dequeue", null),
new Operation("enqueue", 5)
);
testCases.add(case1);
// Test case 2: Wrap-around behavior
List<Operation> case2 = Arrays.asList(
new Operation("enqueue", 1),
new Operation("enqueue", 2),
new Operation("enqueue", 3),
new Operation("dequeue", null),
new Operation("dequeue", null),
new Operation("enqueue", 4),
new Operation("enqueue", 5),
new Operation("enqueue", 6)
);
testCases.add(case2);
// Test case 3: Full queue
List<Operation> case3 = Arrays.asList(
new Operation("enqueue", 1),
new Operation("enqueue", 2),
new Operation("enqueue", 3),
new Operation("enqueue", 4),
new Operation("enqueue", 5),
new Operation("enqueue", 6)
);
testCases.add(case3);
// Test case 4: Empty queue
List<Operation> case4 = Arrays.asList(
new Operation("dequeue", null)
);
testCases.add(case4);
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
System.out.println("Test case " + (i + 1) + " (capacity=" + capacity + "):");
tester.testCircularQueue(testCases.get(i), capacity);
System.out.println();
}
}
}
Output
Running the main method produces:
Test case 1 (capacity=5):
Enqueued 1, Queue: [1]
Enqueued 2, Queue: [1, 2]
Enqueued 3, Queue: [1, 2, 3]
Dequeued 1, Queue: [2, 3]
Enqueued 4, Queue: [2, 3, 4]
Dequeued 2, Queue: [3, 4]
Enqueued 5, Queue: [3, 4, 5]
Test case 2 (capacity=5):
Enqueued 1, Queue: [1]
Enqueued 2, Queue: [1, 2]
Enqueued 3, Queue: [1, 2, 3]
Dequeued 1, Queue: [2, 3]
Dequeued 2, Queue: [3]
Enqueued 4, Queue: [3, 4]
Enqueued 5, Queue: [3, 4, 5]
Enqueued 6, Queue: [3, 4, 5, 6]
Test case 3 (capacity=5):
Enqueued 1, Queue: [1]
Enqueued 2, Queue: [1, 2]
Enqueued 3, Queue: [1, 2, 3]
Enqueued 4, Queue: [1, 2, 3, 4]
Enqueued 5, Queue: [1, 2, 3, 4, 5]
Enqueue 6 failed: Queue full
Test case 4 (capacity=5):
Queue empty, cannot dequeue
Explanation:
- Test case 1: Normal enqueue/dequeue operations within capacity.
- Test case 2: Demonstrates wrap-around (after dequeuing, rear wraps to start for 4, 5, 6).
- Test case 3: Fills queue to capacity, fails to enqueue 6.
- Test case 4: Dequeue on empty queue returns error.
How It Works
- CircularQueue:
- Uses an array with
front,rear, andsizeto manage integers in FIFO order. enqueue: Adds torear(modulo capacity) if not full.dequeue: Removes fromfront(modulo capacity) if not empty.toString: Formats queue contents fromfronttorear.- Wrap-around:
rear = (rear + 1) % capacityandfront = (front + 1) % capacityenable circular behavior.
- Uses an array with
- testCircularQueue:
- Enqueues numbers, prints queue state or "Queue full".
- Dequeues numbers, prints result and queue state or "Queue empty".
- Example Trace (Test case 2):
- Enqueue 1: array=[1], front=0, rear=0, size=1.
- Enqueue 2: array=[1,2], front=0, rear=1, size=2.
- Enqueue 3: array=[1,2,3], front=0, rear=2, size=3.
- Dequeue 1: array=[-,2,3], front=1, rear=2, size=2.
- Dequeue 2: array=[-,-,3], front=2, rear=2, size=1.
- Enqueue 4: array=[4,-,3], front=2, rear=0, size=2 (wrap-around).
- Enqueue 5: array=[4,5,3], front=2, rear=1, size=3.
- Enqueue 6: array=[4,5,3,6], front=2, rear=2, size=4.
- Main Method: Tests normal operations, wrap-around, full queue, and empty queue.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Enqueue/Dequeue | O(1) | O(1) |
| Full Algorithm | O(n) | O(c) |
Note:
- n is the number of operations, c is the fixed capacity.
- Time complexity: O(1) for each enqueue/dequeue; O(n) for n operations.
- Space complexity: O(c) for the fixed-size array.
- Worst case: O(n) time for n operations, O(c) space for the queue.
✅ Tip: Use a circular queue to efficiently reuse array space, especially for applications with frequent enqueue and dequeue operations. Test wrap-around behavior by dequeuing some elements and then enqueuing more to verify the circular property.
⚠ Warning: Carefully manage
frontandrearindices with modulo operations to ensure correct wrap-around. Handle full and empty queue cases to avoid incorrect behavior.
Print Job Simulator
Problem Statement
Write a Java program that simulates a printer queue using a queue implementation. The program should allow users to enqueue print jobs (represented as strings) and dequeue them in order, printing each job as it is processed in a first-in, first-out (FIFO) manner. Test the implementation with various sequences of enqueue and dequeue operations, including edge cases like empty queues and null print jobs. You can visualize this as a printer handling a queue of documents, where each document is printed in the order it was submitted, and the printer processes one job at a time.
Input: A sequence of operations, where each operation is either:
- Enqueue: Add a print job (string) to the queue (e.g.,
"Document1"). - Dequeue: Remove and process the next print job, printing its details. Output: For each operation, print the action performed (enqueue or dequeue) and, for dequeue, the processed job or a message if the queue is empty. Constraints:
- Queue size is between 0 and 10^5.
- Print jobs are non-empty strings containing printable ASCII characters, or null (handled gracefully).
- The queue may be empty when dequeue is called. Example:
- Input: Operations = [enqueue("Doc1"), enqueue("Doc2"), dequeue, enqueue("Doc3"), dequeue]
- Output:
Enqueued Doc1 Enqueued Doc2 Dequeued and processed: Doc1 Enqueued Doc3 Dequeued and processed: Doc2 - Explanation: Jobs are enqueued in order and dequeued in FIFO order, printing each job as processed.
- Input: Operations = [dequeue on empty queue]
- Output:
Queue empty, cannot dequeue
Pseudocode
CLASS StringQueue
SET array to new string array of size 1000
SET front to 0
SET rear to -1
SET size to 0
FUNCTION enqueue(job)
IF size equals array length THEN
RETURN false (queue full)
ENDIF
INCREMENT rear
SET array[rear] to job
INCREMENT size
RETURN true
ENDFUNCTION
FUNCTION dequeue()
IF size equals 0 THEN
RETURN null (queue empty)
ENDIF
SET job to array[front]
INCREMENT front
DECREMENT size
RETURN job
ENDFUNCTION
FUNCTION isEmpty()
RETURN size equals 0
ENDFUNCTION
ENDCLASS
FUNCTION simulatePrinter(operations)
CREATE queue as new StringQueue
FOR each operation in operations
IF operation.type equals "enqueue" THEN
CALL queue.enqueue(operation.job)
PRINT enqueued job
ELSE IF operation.type equals "dequeue" THEN
SET job to queue.dequeue()
IF job is null THEN
PRINT queue empty message
ELSE
PRINT dequeued and processed job
ENDIF
ENDIF
ENDFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of operation sequences
FOR each testCase in testCases
PRINT test case details
CALL simulatePrinter(testCase)
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
StringQueueclass with: a. An array to store print jobs (strings), withfront,rear, andsizeto track queue state. b. Methods:enqueue(add job to rear),dequeue(remove job from front),isEmpty(check if empty). - In the
simulatePrintermethod: a. Create a newStringQueue. b. For each operation:- If "enqueue", add the job to the queue and print the action.
- If "dequeue", remove the next job, print it as processed, or print "Queue empty" if empty.
- In the
mainmethod, test with sequences of enqueue and dequeue operations, including empty queues, single jobs, and null jobs.
Java Implementation
import java.util.*;
public class PrintJobSimulator {
// Custom queue implementation for strings
static class StringQueue {
private String[] array;
private int front;
private int rear;
private int size;
private static final int DEFAULT_SIZE = 1000;
public StringQueue() {
array = new String[DEFAULT_SIZE];
front = 0;
rear = -1;
size = 0;
}
public boolean enqueue(String job) {
if (size == array.length) {
return false; // Queue full
}
array[++rear] = job;
size++;
return true;
}
public String dequeue() {
if (size == 0) {
return null; // Queue empty
}
String job = array[front++];
size--;
return job;
}
public boolean isEmpty() {
return size == 0;
}
}
// Helper class for operations
static class Operation {
String type;
String job; // For enqueue operations
Operation(String type, String job) {
this.type = type;
this.job = job;
}
}
// Simulates printer queue functionality
public void simulatePrinter(List<Operation> operations) {
StringQueue queue = new StringQueue();
for (Operation op : operations) {
if (op.type.equals("enqueue")) {
boolean success = queue.enqueue(op.job);
if (success) {
System.out.println("Enqueued " + (op.job != null ? op.job : "null"));
} else {
System.out.println("Enqueue " + (op.job != null ? op.job : "null") + " failed: Queue full");
}
} else if (op.type.equals("dequeue")) {
String job = queue.dequeue();
if (job == null) {
System.out.println("Queue empty, cannot dequeue");
} else {
System.out.println("Dequeued and processed: " + job);
}
}
}
}
// Main method to test printer queue simulation
public static void main(String[] args) {
PrintJobSimulator simulator = new PrintJobSimulator();
// Test cases
List<List<Operation>> testCases = new ArrayList<>();
// Test case 1: Normal sequence
List<Operation> case1 = Arrays.asList(
new Operation("enqueue", "Document1"),
new Operation("enqueue", "Document2"),
new Operation("dequeue", null),
new Operation("enqueue", "Document3"),
new Operation("dequeue", null)
);
testCases.add(case1);
// Test case 2: Empty queue
List<Operation> case2 = Arrays.asList(
new Operation("dequeue", null)
);
testCases.add(case2);
// Test case 3: Single job
List<Operation> case3 = Arrays.asList(
new Operation("enqueue", "SingleDoc"),
new Operation("dequeue", null)
);
testCases.add(case3);
// Test case 4: Multiple enqueues
List<Operation> case4 = Arrays.asList(
new Operation("enqueue", "Job1"),
new Operation("enqueue", "Job2"),
new Operation("enqueue", "Job3"),
new Operation("dequeue", null),
new Operation("dequeue", null)
);
testCases.add(case4);
// Test case 5: Null job
List<Operation> case5 = Arrays.asList(
new Operation("enqueue", null),
new Operation("dequeue", null)
);
testCases.add(case5);
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
System.out.println("Test case " + (i + 1) + ":");
simulator.simulatePrinter(testCases.get(i));
System.out.println();
}
}
}
Output
Running the main method produces:
Test case 1:
Enqueued Document1
Enqueued Document2
Dequeued and processed: Document1
Enqueued Document3
Dequeued and processed: Document2
Test case 2:
Queue empty, cannot dequeue
Test case 3:
Enqueued SingleDoc
Dequeued and processed: SingleDoc
Test case 4:
Enqueued Job1
Enqueued Job2
Enqueued Job3
Dequeued and processed: Job1
Dequeued and processed: Job2
Test case 5:
Enqueued null
Dequeued and processed: null
Explanation:
- Test case 1: Enqueues "Document1", "Document2", dequeues "Document1", enqueues "Document3", dequeues "Document2".
- Test case 2: Dequeue on empty queue returns "Queue empty".
- Test case 3: Enqueues single job, dequeues it.
- Test case 4: Enqueues three jobs, dequeues two in FIFO order.
- Test case 5: Enqueues null job, dequeues it.
How It Works
- StringQueue:
- Uses an array with
frontandrearindices to track the queue’s state, andsizeto track the number of jobs. enqueue: Adds a job to the rear if the queue isn’t full.dequeue: Removes and returns the front job if not empty.isEmpty: Checks if the queue is empty.
- Uses an array with
- simulatePrinter:
- Enqueues jobs, printing each action.
- Dequeues jobs, printing the processed job or "Queue empty".
- Example Trace (Test case 1):
- Enqueue "Document1": queue = ["Document1"], front=0, rear=0.
- Enqueue "Document2": queue = ["Document1", "Document2"], front=0, rear=1.
- Dequeue: Returns "Document1", queue = ["Document2"], front=1, rear=1.
- Enqueue "Document3": queue = ["Document2", "Document3"], front=1, rear=2.
- Dequeue: Returns "Document2", queue = ["Document3"], front=2, rear=2.
- Main Method: Tests normal sequences, empty queue, single job, multiple enqueues, and null job.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Enqueue/Dequeue | O(1) | O(1) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the number of operations.
- Time complexity: O(1) for each enqueue or dequeue; O(n) for processing n operations.
- Space complexity: O(n) for the queue storing up to n jobs.
- Worst case: O(n) time and O(n) space for many enqueue operations.
✅ Tip: Use a queue to simulate a printer’s job processing, as its FIFO nature ensures jobs are processed in submission order. Test with sequences that mix enqueue and dequeue operations to verify correctness.
⚠ Warning: Handle null jobs and empty queue cases to avoid unexpected behavior. Ensure the queue size is sufficient to accommodate the input sequence.
Queue-Based BFS
Problem Statement
Write a Java program that implements a Breadth-First Search (BFS) algorithm for a graph using a queue. Represent the graph as an adjacency list and print the nodes visited in BFS order, starting from a specified node. The program should handle undirected graphs, enqueue nodes to explore them level by level, and mark visited nodes to avoid cycles. Test the implementation with various graph structures, including connected, disconnected, and cyclic graphs, as well as edge cases like empty graphs. You can visualize this as exploring a network of cities, visiting all cities at the current distance from the starting point before moving farther, using a queue to keep track of the next cities to visit.
Input:
- Number of nodes
n(0 ≤ n ≤ 10^5). - List of edges as pairs of nodes (e.g., [[0,1], [1,2]] for edges 0-1, 1-2).
- Starting node for BFS. Output: A string of nodes visited in BFS order (e.g., "0 1 2"). Constraints:
- Nodes are integers from 0 to n-1.
- Edges are undirected (if u-v exists, v-u exists).
- The graph may be empty, disconnected, or cyclic. Example:
- Input: n=4, edges=[[0,1], [0,2], [1,3]], start=0
- Output: "0 1 2 3"
- Explanation: Starting from node 0, visit 0, then neighbors 1 and 2, then 1’s neighbor 3.
- Input: n=3, edges=[], start=0
- Output: "0"
- Explanation: No edges, only node 0 is visited.
Pseudocode
CLASS IntQueue
SET array to new integer array of size 1000
SET front to 0
SET rear to -1
SET size to 0
FUNCTION enqueue(number)
IF size equals array length THEN
RETURN false
ENDIF
INCREMENT rear
SET array[rear] to number
INCREMENT size
RETURN true
ENDFUNCTION
FUNCTION dequeue()
IF size equals 0 THEN
RETURN null
ENDIF
SET number to array[front]
INCREMENT front
DECREMENT size
RETURN number
ENDFUNCTION
FUNCTION isEmpty()
RETURN size equals 0
ENDFUNCTION
ENDCLASS
FUNCTION bfs(n, edges, start)
IF n equals 0 THEN
RETURN empty string
ENDIF
CREATE adjList as new list of lists for n nodes
FOR each edge in edges
ADD edge[1] to adjList[edge[0]]
ADD edge[0] to adjList[edge[1]] (undirected)
ENDFOR
CREATE queue as new IntQueue
CREATE visited as boolean array of size n, initialized to false
CREATE result as new StringBuilder
ENQUEUE start to queue
SET visited[start] to true
APPEND start to result
WHILE queue is not empty
SET node to queue.dequeue()
FOR each neighbor in adjList[node]
IF not visited[neighbor] THEN
ENQUEUE neighbor to queue
SET visited[neighbor] to true
APPEND " " and neighbor to result
ENDIF
ENDFOR
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of graphs (n, edges, start)
FOR each testCase in testCases
PRINT test case details
CALL bfs(testCase.n, testCase.edges, testCase.start)
PRINT BFS order
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define an
IntQueueclass with: a. An array to store integers, withfront,rear, andsize. b. Methods:enqueue(add to rear),dequeue(remove from front),isEmpty. - In the
bfsmethod: a. If n=0, return empty string. b. Build an adjacency list from edges (add u-v and v-u for undirected). c. Create a queue, a boolean array for visited nodes, and a StringBuilder for output. d. Enqueue the start node, mark it visited, append to result. e. While queue is not empty:- Dequeue a node.
- For each unvisited neighbor, enqueue it, mark it visited, append to result. f. Return the result string.
- In the
mainmethod, test with graphs of varying structures (connected, disconnected, cyclic, empty).
Java Implementation
import java.util.*;
public class QueueBasedBFS {
// Custom queue implementation for integers
static class IntQueue {
private int[] array;
private int front;
private int rear;
private int size;
private static final int DEFAULT_SIZE = 1000;
public IntQueue() {
array = new int[DEFAULT_SIZE];
front = 0;
rear = -1;
size = 0;
}
public boolean enqueue(int number) {
if (size == array.length) {
return false; // Queue full
}
array[++rear] = number;
size++;
return true;
}
public Integer dequeue() {
if (size == 0) {
return null; // Queue empty
}
int number = array[front++];
size--;
return number;
}
public boolean isEmpty() {
return size == 0;
}
}
// Performs BFS and returns nodes in visit order
public String bfs(int n, int[][] edges, int start) {
if (n == 0) {
return "";
}
// Build adjacency list
List<List<Integer>> adjList = new ArrayList<>();
for (int i = 0; i < n; i++) {
adjList.add(new ArrayList<>());
}
for (int[] edge : edges) {
adjList.get(edge[0]).add(edge[1]);
adjList.get(edge[1]).add(edge[0]); // Undirected
}
// BFS
IntQueue queue = new IntQueue();
boolean[] visited = new boolean[n];
StringBuilder result = new StringBuilder();
queue.enqueue(start);
visited[start] = true;
result.append(start);
while (!queue.isEmpty()) {
Integer node = queue.dequeue();
if (node == null) break;
for (int neighbor : adjList.get(node)) {
if (!visited[neighbor]) {
queue.enqueue(neighbor);
visited[neighbor] = true;
result.append(" ").append(neighbor);
}
}
}
return result.toString();
}
// Helper class for test cases
static class GraphTestCase {
int n;
int[][] edges;
int start;
GraphTestCase(int n, int[][] edges, int start) {
this.n = n;
this.edges = edges;
this.start = start;
}
}
// Main method to test BFS
public static void main(String[] args) {
QueueBasedBFS bfs = new QueueBasedBFS();
// Test cases
List<GraphTestCase> testCases = new ArrayList<>();
// Test case 1: Connected graph
testCases.add(new GraphTestCase(
4,
new int[][]{{0,1}, {0,2}, {1,3}},
0
));
// Test case 2: Cyclic graph
testCases.add(new GraphTestCase(
4,
new int[][]{{0,1}, {1,2}, {2,3}, {3,0}},
1
));
// Test case 3: Disconnected graph
testCases.add(new GraphTestCase(
5,
new int[][]{{0,1}, {2,3}},
0
));
// Test case 4: Empty graph
testCases.add(new GraphTestCase(
0,
new int[][]{},
0
));
// Test case 5: Single node
testCases.add(new GraphTestCase(
1,
new int[][]{},
0
));
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
GraphTestCase test = testCases.get(i);
System.out.println("Test case " + (i + 1) + ": n=" + test.n + ", start=" + test.start);
System.out.println("Edges: " + Arrays.deepToString(test.edges));
String result = bfs.bfs(test.n, test.edges, test.start);
System.out.println("BFS order: " + (result.isEmpty() ? "[]" : result) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1: n=4, start=0
Edges: [[0, 1], [0, 2], [1, 3]]
BFS order: 0 1 2 3
Test case 2: n=4, start=1
Edges: [[0, 1], [1, 2], [2, 3], [3, 0]]
BFS order: 1 0 2 3
Test case 3: n=5, start=0
Edges: [[0, 1], [2, 3]]
BFS order: 0 1
Test case 4: n=0, start=0
Edges: []
BFS order: []
Test case 5: n=1, start=0
Edges: []
BFS order: 0
Explanation:
- Test case 1: From node 0, visits 0, then 1 and 2, then 3 (via 1).
- Test case 2: From node 1 in a cycle (0-1-2-3-0), visits 1, then 0 and 2, then 3.
- Test case 3: From node 0, visits 0 and 1; nodes 2, 3, 4 are disconnected.
- Test case 4: Empty graph returns empty string.
- Test case 5: Single node 0, no edges, visits only 0.
How It Works
- IntQueue:
- Uses an array with
front,rear, andsizefor FIFO operations. enqueue: Adds to rear if not full.dequeue: Removes from front if not empty.
- Uses an array with
- bfs:
- Builds an adjacency list from edges (undirected: add u-v and v-u).
- Uses a queue to explore nodes level by level, a boolean array to track visited nodes, and a StringBuilder for output.
- Enqueues start node, marks visited, appends to result.
- Dequeues nodes, enqueues unvisited neighbors, appends them to result.
- Example Trace (Test case 1):
- n=4, edges=[[0,1], [0,2], [1,3]], start=0.
- Adjacency list: [0: [1,2], 1: [0,3], 2: [0], 3: [1]].
- Enqueue 0: queue=[0], visited=[T,F,F,F], result="0".
- Dequeue 0, enqueue 1,2: queue=[1,2], visited=[T,T,T,F], result="0 1 2".
- Dequeue 1, enqueue 3: queue=[2,3], visited=[T,T,T,T], result="0 1 2 3".
- Dequeue 2, 3: queue=[], result="0 1 2 3".
- Main Method: Tests connected, cyclic, disconnected, empty, and single-node graphs.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Enqueue/Dequeue | O(1) | O(1) |
| BFS Algorithm | O(V + E) | O(V + E) |
Note:
- V is the number of vertices, E is the number of edges.
- Time complexity: O(V + E) for visiting all nodes and edges via adjacency list.
- Space complexity: O(V) for queue and visited array, O(E) for adjacency list.
- Worst case: O(V + E) time and space for dense graphs.
✅ Tip: Use a queue for BFS to ensure level-by-level traversal, and an adjacency list for efficient neighbor access. Test with cyclic and disconnected graphs to verify correct handling of visited nodes.
⚠ Warning: Always mark nodes as visited before enqueuing to avoid infinite loops in cyclic graphs. Ensure the queue size is sufficient for large graphs.
Queue Reversal
Problem Statement
Write a Java program that reverses the elements of a queue using a stack. The program should enqueue a set of numbers into a queue, reverse the order of the elements using a stack, and print the reversed queue. The reversal should maintain the queue’s FIFO (first-in, first-out) property in the final output. Test the implementation with various sets of numbers, including empty queues and single-element queues. You can visualize this as unloading a queue of items onto a stack, which flips their order, and then reloading them into a new queue to get the reversed sequence.
Input: A set of integers to enqueue (e.g., [1, 2, 3]). Output: The reversed queue as a string (e.g., "[3, 2, 1]"). Constraints:
- Queue size is between 0 and 10^5.
- Elements are integers in the range [-10^9, 10^9].
- The queue may be empty. Example:
- Input: Enqueue [1, 2, 3]
- Output: [3, 2, 1]
- Explanation: Enqueue 1, 2, 3; dequeue to stack (3, 2, 1 top); pop to new queue → [3, 2, 1].
- Input: Enqueue []
- Output: []
- Explanation: Empty queue remains empty after reversal.
Pseudocode
CLASS IntQueue
SET array to new integer array of size 1000
SET front to 0
SET rear to -1
SET size to 0
FUNCTION enqueue(number)
IF size equals array length THEN
RETURN false (queue full)
ENDIF
INCREMENT rear
SET array[rear] to number
INCREMENT size
RETURN true
ENDFUNCTION
FUNCTION dequeue()
IF size equals 0 THEN
RETURN null (queue empty)
ENDIF
SET number to array[front]
INCREMENT front
DECREMENT size
RETURN number
ENDFUNCTION
FUNCTION isEmpty()
RETURN size equals 0
ENDFUNCTION
FUNCTION toString()
CREATE result as empty string
APPEND "["
FOR i from front to rear
APPEND array[i] and ", " to result
ENDFOR
IF result ends with ", " THEN
REMOVE last ", "
ENDIF
APPEND "]" to result
RETURN result
ENDFUNCTION
ENDCLASS
CLASS IntStack
SET array to new integer array of size 1000
SET top to -1
FUNCTION push(number)
IF top equals array length - 1 THEN
RETURN false (stack full)
ENDIF
INCREMENT top
SET array[top] to number
RETURN true
ENDFUNCTION
FUNCTION pop()
IF top equals -1 THEN
RETURN null (stack empty)
ENDIF
SET number to array[top]
DECREMENT top
RETURN number
ENDFUNCTION
ENDCLASS
FUNCTION reverseQueue(queue)
CREATE stack as new IntStack
CREATE newQueue as new IntQueue
WHILE queue is not empty
SET number to queue.dequeue()
PUSH number to stack
ENDWHILE
WHILE stack is not empty
SET number to stack.pop()
ENQUEUE number to newQueue
ENDWHILE
RETURN newQueue
ENDFUNCTION
FUNCTION main()
SET testCases to array of integer arrays
FOR each numbers in testCases
CREATE queue as new IntQueue
FOR each number in numbers
ENQUEUE number to queue
ENDFOR
PRINT original queue
SET reversed to reverseQueue(queue)
PRINT reversed queue
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define an
IntQueueclass with: a. An array to store integers, withfront,rear, andsizeto track queue state. b. Methods:enqueue(add to rear),dequeue(remove from front),isEmpty,toString. - Define an
IntStackclass with: a. An array to store integers, withtopindex. b. Methods:push(add to top),pop(remove from top). - In the
reverseQueuemethod: a. Create a stack and a new queue. b. Dequeue all elements from the input queue and push them onto the stack. c. Pop all elements from the stack and enqueue them into the new queue. d. Return the new queue. - In the
mainmethod, test with different sets of numbers, including empty and single-element queues, printing the original and reversed queues.
Java Implementation
import java.util.*;
public class QueueReversal {
// Custom queue implementation for integers
static class IntQueue {
private int[] array;
private int front;
private int rear;
private int size;
private static final int DEFAULT_SIZE = 1000;
public IntQueue() {
array = new int[DEFAULT_SIZE];
front = 0;
rear = -1;
size = 0;
}
public boolean enqueue(int number) {
if (size == array.length) {
return false; // Queue full
}
array[++rear] = number;
size++;
return true;
}
public Integer dequeue() {
if (size == 0) {
return null; // Queue empty
}
int number = array[front++];
size--;
return number;
}
public boolean isEmpty() {
return size == 0;
}
public String toString() {
StringBuilder result = new StringBuilder("[");
for (int i = front; i <= rear; i++) {
result.append(array[i]);
if (i < rear) {
result.append(", ");
}
}
result.append("]");
return result.toString();
}
}
// Custom stack implementation for integers
static class IntStack {
private int[] array;
private int top;
private static final int DEFAULT_SIZE = 1000;
public IntStack() {
array = new int[DEFAULT_SIZE];
top = -1;
}
public boolean push(int number) {
if (top == array.length - 1) {
return false; // Stack full
}
array[++top] = number;
return true;
}
public Integer pop() {
if (top == -1) {
return null; // Stack empty
}
return array[top--];
}
}
// Reverses a queue using a stack
public IntQueue reverseQueue(IntQueue queue) {
IntStack stack = new IntStack();
IntQueue newQueue = new IntQueue();
// Dequeue all elements and push to stack
while (!queue.isEmpty()) {
Integer number = queue.dequeue();
if (number != null) {
stack.push(number);
}
}
// Pop from stack and enqueue to new queue
while (true) {
Integer number = stack.pop();
if (number == null) {
break;
}
newQueue.enqueue(number);
}
return newQueue;
}
// Main method to test queue reversal
public static void main(String[] args) {
QueueReversal reverser = new QueueReversal();
// Test cases
int[][] testCases = {
{1, 2, 3}, // Normal queue
{}, // Empty queue
{42}, // Single element
{10, 20, 30, 40, 50} // Larger queue
};
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
IntQueue queue = new IntQueue();
for (int num : testCases[i]) {
queue.enqueue(num);
}
System.out.println("Original queue: " + queue);
IntQueue reversed = reverser.reverseQueue(queue);
System.out.println("Reversed queue: " + reversed + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Original queue: [1, 2, 3]
Reversed queue: [3, 2, 1]
Test case 2:
Original queue: []
Reversed queue: []
Test case 3:
Original queue: [42]
Reversed queue: [42]
Test case 4:
Original queue: [10, 20, 30, 40, 50]
Reversed queue: [50, 40, 30, 20, 10]
Explanation:
- Test case 1: Enqueue [1, 2, 3] → stack [3, 2, 1 (top)] → new queue [3, 2, 1].
- Test case 2: Empty queue → empty stack → empty queue.
- Test case 3: Enqueue [42] → stack [42] → new queue [42].
- Test case 4: Enqueue [10, 20, 30, 40, 50] → stack [50, 40, 30, 20, 10 (top)] → new queue [50, 40, 30, 20, 10].
How It Works
- IntQueue:
- Uses an array with
front,rear, andsizeto manage integers in FIFO order. enqueue: Adds to rear if not full.dequeue: Removes from front if not empty.toString: Formats queue as a string for output.
- Uses an array with
- IntStack:
- Uses an array with
topto manage integers in LIFO order. push: Adds to top if not full.pop: Removes from top if not empty.
- Uses an array with
- reverseQueue:
- Dequeues all elements from the input queue to a stack (reversing order due to LIFO).
- Pops all elements from the stack to a new queue (restoring FIFO order, now reversed).
- Example Trace (Test case 1):
- Enqueue 1, 2, 3: queue = [1, 2, 3], front=0, rear=2.
- Dequeue to stack: stack = [1, 2, 3 (top)].
- Pop to new queue: newQueue = [3, 2, 1].
- Main Method: Tests normal, empty, single-element, and larger queues.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Enqueue/Dequeue | O(1) | O(1) |
| Push/Pop | O(1) | O(1) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the number of elements in the queue.
- Time complexity: O(n) for n dequeues to stack and n pops to new queue.
- Space complexity: O(n) for the stack and new queue.
- Worst case: O(n) time and O(n) space for large queues.
✅ Tip: Use a stack to reverse a queue, as its LIFO nature naturally flips the order of elements. Test with various queue sizes to ensure the reversal works correctly.
⚠ Warning: Ensure the stack and queue sizes are sufficient to handle the input. Handle empty queues to avoid null pointer issues during reversal.
Ticket Counter Simulation
Problem Statement
Write a Java program that simulates a ticket counter system where customers, represented by names (strings), join a queue and are served in first-in, first-out (FIFO) order. The program should allow users to enqueue customers, dequeue them to simulate serving, and display the current queue size after each operation. Test the implementation with various sequences of enqueue and dequeue operations, including edge cases like empty queues and null customer names. You can visualize this as a ticket counter at a movie theater, where customers line up, are served one by one, and the staff tracks how many people are still waiting.
Input: A sequence of operations, where each operation is either:
- Enqueue: Add a customer name (string) to the queue (e.g., "Alice").
- Dequeue: Remove and serve the next customer from the queue.
- Size: Display the current number of customers in the queue. Output: For each operation, print the action performed (enqueue, dequeue, or size) and the result, such as the served customer, queue size, or error messages for empty queues. Constraints:
- Queue size is between 0 and 10^5.
- Customer names are non-empty strings containing printable ASCII characters, or null (handled gracefully).
- The queue may be empty when dequeue or size is called. Example:
- Input: Operations = [enqueue("Alice"), enqueue("Bob"), size, dequeue, size, enqueue("Charlie"), dequeue]
- Output:
Enqueued Alice Enqueued Bob Queue size: 2 Served customer: Alice Queue size: 1 Enqueued Charlie Served customer: Bob - Explanation: Customers join and are served in order, with queue size reported as requested.
- Input: Operations = [dequeue on empty queue]
- Output:
Queue empty, cannot dequeue
Pseudocode
CLASS StringQueue
SET array to new string array of size 1000
SET front to 0
SET rear to -1
SET size to 0
FUNCTION enqueue(name)
IF size equals array length THEN
RETURN false (queue full)
ENDIF
INCREMENT rear
SET array[rear] to name
INCREMENT size
RETURN true
ENDFUNCTION
FUNCTION dequeue()
IF size equals 0 THEN
RETURN null (queue empty)
ENDIF
SET name to array[front]
INCREMENT front
DECREMENT size
RETURN name
ENDFUNCTION
FUNCTION size()
RETURN size
ENDFUNCTION
FUNCTION isEmpty()
RETURN size equals 0
ENDFUNCTION
ENDCLASS
FUNCTION simulateTicketCounter(operations)
CREATE queue as new StringQueue
FOR each operation in operations
IF operation.type equals "enqueue" THEN
CALL queue.enqueue(operation.name)
PRINT enqueued name
ELSE IF operation.type equals "dequeue" THEN
SET name to queue.dequeue()
IF name is null THEN
PRINT queue empty message
ELSE
PRINT served customer name
ENDIF
ELSE IF operation.type equals "size" THEN
PRINT queue size
ENDIF
ENDFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of operation sequences
FOR each testCase in testCases
PRINT test case details
CALL simulateTicketCounter(testCase)
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
StringQueueclass with: a. An array to store customer names, withfront,rear, andsizeto track queue state. b. Methods:enqueue(add to rear),dequeue(remove from front),size(return current size),isEmpty(check if empty). - In the
simulateTicketCountermethod: a. Create a newStringQueue. b. For each operation:- If "enqueue", add the customer name and print the action.
- If "dequeue", remove the next customer, print the served customer or "Queue empty".
- If "size", print the current queue size.
- In the
mainmethod, test with sequences of enqueue, dequeue, and size operations, including empty queues, single customers, and null names.
Java Implementation
import java.util.*;
public class TicketCounterSimulation {
// Custom queue implementation for strings
static class StringQueue {
private String[] array;
private int front;
private int rear;
private int size;
private static final int DEFAULT_SIZE = 1000;
public StringQueue() {
array = new String[DEFAULT_SIZE];
front = 0;
rear = -1;
size = 0;
}
public boolean enqueue(String name) {
if (size == array.length) {
return false; // Queue full
}
array[++rear] = name;
size++;
return true;
}
public String dequeue() {
if (size == 0) {
return null; // Queue empty
}
String name = array[front++];
size--;
return name;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
}
// Helper class for operations
static class Operation {
String type;
String name; // For enqueue operations
Operation(String type, String name) {
this.type = type;
this.name = name;
}
}
// Simulates ticket counter system
public void simulateTicketCounter(List<Operation> operations) {
StringQueue queue = new StringQueue();
for (Operation op : operations) {
if (op.type.equals("enqueue")) {
boolean success = queue.enqueue(op.name);
if (success) {
System.out.println("Enqueued " + (op.name != null ? op.name : "null"));
} else {
System.out.println("Enqueue " + (op.name != null ? op.name : "null") + " failed: Queue full");
}
} else if (op.type.equals("dequeue")) {
String name = queue.dequeue();
if (name == null) {
System.out.println("Queue empty, cannot dequeue");
} else {
System.out.println("Served customer: " + name);
}
} else if (op.type.equals("size")) {
System.out.println("Queue size: " + queue.size());
}
}
}
// Main method to test ticket counter simulation
public static void main(String[] args) {
TicketCounterSimulation simulator = new TicketCounterSimulation();
// Test cases
List<List<Operation>> testCases = new ArrayList<>();
// Test case 1: Normal sequence
List<Operation> case1 = Arrays.asList(
new Operation("enqueue", "Alice"),
new Operation("enqueue", "Bob"),
new Operation("size", null),
new Operation("dequeue", null),
new Operation("size", null),
new Operation("enqueue", "Charlie"),
new Operation("dequeue", null)
);
testCases.add(case1);
// Test case 2: Empty queue
List<Operation> case2 = Arrays.asList(
new Operation("dequeue", null),
new Operation("size", null)
);
testCases.add(case2);
// Test case 3: Single customer
List<Operation> case3 = Arrays.asList(
new Operation("enqueue", "David"),
new Operation("size", null),
new Operation("dequeue", null)
);
testCases.add(case3);
// Test case 4: Multiple enqueues and dequeues
List<Operation> case4 = Arrays.asList(
new Operation("enqueue", "Eve"),
new Operation("enqueue", "Frank"),
new Operation("enqueue", "Grace"),
new Operation("size", null),
new Operation("dequeue", null),
new Operation("dequeue", null),
new Operation("size", null)
);
testCases.add(case4);
// Test case 5: Null customer name
List<Operation> case5 = Arrays.asList(
new Operation("enqueue", null),
new Operation("size", null),
new Operation("dequeue", null)
);
testCases.add(case5);
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
System.out.println("Test case " + (i + 1) + ":");
simulator.simulateTicketCounter(testCases.get(i));
System.out.println();
}
}
}
Output
Running the main method produces:
Test case 1:
Enqueued Alice
Enqueued Bob
Queue size: 2
Served customer: Alice
Queue size: 1
Enqueued Charlie
Served customer: Bob
Test case 2:
Queue empty, cannot dequeue
Queue size: 0
Test case 3:
Enqueued David
Queue size: 1
Served customer: David
Test case 4:
Enqueued Eve
Enqueued Frank
Enqueued Grace
Queue size: 3
Served customer: Eve
Served customer: Frank
Queue size: 1
Test case 5:
Enqueued null
Queue size: 1
Served customer: null
Explanation:
- Test case 1: Enqueues Alice, Bob; reports size 2; dequeues Alice; reports size 1; enqueues Charlie; dequeues Bob.
- Test case 2: Dequeue and size on empty queue return error and 0.
- Test case 3: Enqueues David, reports size 1, dequeues David.
- Test case 4: Enqueues three customers, reports size 3, dequeues two, reports size 1.
- Test case 5: Enqueues null name, reports size 1, dequeues null.
How It Works
- StringQueue:
- Uses an array with
front,rear, andsizeto manage customer names in FIFO order. enqueue: Adds to rear if not full.dequeue: Removes from front if not empty.size: Returns current number of customers.
- Uses an array with
- simulateTicketCounter:
- Enqueues customers, printing the action.
- Dequeues customers, printing the served customer or "Queue empty".
- Reports queue size when requested.
- Example Trace (Test case 1):
- Enqueue "Alice": array=["Alice"], front=0, rear=0, size=1.
- Enqueue "Bob": array=["Alice","Bob"], front=0, rear=1, size=2.
- Size: Returns 2.
- Dequeue: Returns "Alice", array=["-","Bob"], front=1, rear=1, size=1.
- Size: Returns 1.
- Enqueue "Charlie": array=["-","Bob","Charlie"], front=1, rear=2, size=2.
- Dequeue: Returns "Bob", array=["-","-","Charlie"], front=2, rear=2, size=1.
- Main Method: Tests normal sequences, empty queue, single customer, multiple operations, and null name.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Enqueue/Dequeue | O(1) | O(1) |
| Size | O(1) | O(1) |
| Full Algorithm | O(n) | O(n) |
Note:
- n is the number of operations.
- Time complexity: O(1) for each enqueue, dequeue, or size; O(n) for n operations.
- Space complexity: O(n) for the queue storing up to n customers.
- Worst case: O(n) time and O(n) space for many enqueue operations.
✅ Tip: Use a queue for a ticket counter simulation, as its FIFO nature ensures customers are served in arrival order. Test with size queries to verify queue state after operations.
⚠ Warning: Handle null names and empty queue cases to avoid unexpected behavior. Ensure the queue size is sufficient to accommodate the input sequence.
Dijkstra’s Algorithm
Problem Statement
Write a Java program that implements Dijkstra’s algorithm for a weighted graph using a priority queue. Represent the graph as an adjacency list, where each edge has a non-negative weight, and compute the shortest path distances from a source node to all other nodes. The program should handle directed graphs and print the shortest distances for each reachable node. Test the implementation with various graph structures, including connected, sparse, and dense graphs, as well as edge cases like single nodes or unreachable nodes. You can visualize this as finding the shortest driving routes from a starting city to all other cities, using a priority queue to always explore the closest unvisited city next.
Input:
- Number of nodes
n(1 ≤ n ≤ 10^5). - List of edges as triples [u, v, w] (node u to node v with weight w, w ≥ 0).
- Source node for shortest paths (0 ≤ source < n). Output: A string of node indices and their shortest distances from the source (e.g., "0:0 1:3 2:5"), or "INF" for unreachable nodes. Constraints:
- Nodes are integers from 0 to n-1.
- Edge weights are non-negative integers (0 ≤ w ≤ 10^9).
- The graph may be disconnected; unreachable nodes have distance "INF".
- Assume the input graph is valid (no negative weights). Example:
- Input: n=4, edges=[[0,1,1], [0,2,4], [1,2,2], [1,3,6]], source=0
- Output: "0:0 1:1 2:3 3:7"
- Explanation: Shortest paths from node 0: 0→1 (1), 0→1→2 (1+2=3), 0→1→3 (1+6=7).
- Input: n=3, edges=[[0,1,2]], source=2
- Output: "0:INF 1:INF 2:0"
- Explanation: Node 2 cannot reach 0 or 1; only 2 is reachable (distance 0).
Pseudocode
CLASS NodeDistance
SET node to integer
SET distance to long
ENDCLASS
CLASS MinPriorityQueue
SET array to new NodeDistance array of size 1000
SET size to 0
FUNCTION enqueue(node, distance)
IF size equals array length THEN
RETURN false
ENDIF
SET array[size] to new NodeDistance(node, distance)
CALL siftUp(size)
INCREMENT size
RETURN true
ENDFUNCTION
FUNCTION siftUp(index)
WHILE index > 0
SET parent to (index - 1) / 2
IF array[index].distance >= array[parent].distance THEN
BREAK
ENDIF
SWAP array[index] and array[parent]
SET index to parent
ENDWHILE
ENDFUNCTION
FUNCTION dequeue()
IF size equals 0 THEN
RETURN null
ENDIF
SET result to array[0]
SET array[0] to array[size - 1]
DECREMENT size
IF size > 0 THEN
CALL siftDown(0)
ENDIF
RETURN result
ENDFUNCTION
FUNCTION siftDown(index)
SET minIndex to index
WHILE true
SET left to 2 * index + 1
SET right to 2 * index + 2
IF left < size AND array[left].distance < array[minIndex].distance THEN
SET minIndex to left
ENDIF
IF right < size AND array[right].distance < array[minIndex].distance THEN
SET minIndex to right
ENDIF
IF minIndex equals index THEN
BREAK
ENDIF
SWAP array[index] and array[minIndex]
SET index to minIndex
ENDWHILE
ENDFUNCTION
ENDCLASS
FUNCTION dijkstra(n, edges, source)
CREATE adjList as new list of lists of (node, weight) pairs for n nodes
FOR each edge [u, v, w] in edges
ADD (v, w) to adjList[u]
ENDFOR
CREATE distances as array of size n, initialized to INF
SET distances[source] to 0
CREATE queue as new MinPriorityQueue
ENQUEUE (source, 0) to queue
CREATE visited as boolean array of size n, initialized to false
WHILE queue is not empty
SET nodeDist to queue.dequeue()
SET node to nodeDist.node
SET dist to nodeDist.distance
IF visited[node] THEN
CONTINUE
ENDIF
SET visited[node] to true
FOR each (neighbor, weight) in adjList[node]
IF dist + weight < distances[neighbor] THEN
SET distances[neighbor] to dist + weight
ENQUEUE (neighbor, dist + weight) to queue
ENDIF
ENDFOR
ENDWHILE
CREATE result as new StringBuilder
FOR i from 0 to n-1
IF distances[i] equals INF THEN
APPEND i + ":INF" to result
ELSE
APPEND i + ":" + distances[i] to result
ENDIF
IF i < n-1 THEN
APPEND " " to result
ENDIF
ENDFOR
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of graphs (n, edges, source)
FOR each testCase in testCases
PRINT test case details
CALL dijkstra(testCase.n, testCase.edges, testCase.source)
PRINT shortest distances
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
NodeDistanceclass to store a node index and its tentative distance. - Define a
MinPriorityQueueclass using a min-heap with: a. An array to storeNodeDistanceobjects, prioritized by distance. b.enqueue: Add node-distance pair, sift up to maintain min-heap. c.siftUp: Move smaller distance up. d.dequeue: Remove root (smallest distance), sift down. e.siftDown: Move larger distance down. - In
dijkstra: a. Build adjacency list from edges (directed: add u→v,w). b. Initialize distances to INF, except source (0). c. Enqueue source with distance 0. d. While queue is not empty:- Dequeue node with smallest distance.
- Skip if visited.
- Mark node visited.
- Update neighbors’ distances if shorter path found, enqueue them. e. Format distances as string (INF for unreachable).
- In
main, test with different graph structures and edge cases.
Java Implementation
import java.util.*;
public class DijkstrasAlgorithm {
// Class to store node and distance pair
static class NodeDistance {
int node;
long distance;
NodeDistance(int node, long distance) {
this.node = node;
this.distance = distance;
}
}
// Custom min-heap priority queue for node-distance pairs
static class MinPriorityQueue {
private NodeDistance[] array;
private int size;
private static final int DEFAULT_SIZE = 1000;
public MinPriorityQueue() {
array = new NodeDistance[DEFAULT_SIZE];
size = 0;
}
public boolean enqueue(int node, long distance) {
if (size == array.length) {
return false; // Queue full
}
array[size] = new NodeDistance(node, distance);
siftUp(size);
size++;
return true;
}
private void siftUp(int index) {
while (index > 0) {
int parent = (index - 1) / 2;
if (array[index].distance >= array[parent].distance) {
break;
}
// Swap
NodeDistance temp = array[index];
array[index] = array[parent];
array[parent] = temp;
index = parent;
}
}
public NodeDistance dequeue() {
if (size == 0) {
return null; // Queue empty
}
NodeDistance result = array[0];
array[0] = array[size - 1];
size--;
if (size > 0) {
siftDown(0);
}
return result;
}
private void siftDown(int index) {
int minIndex = index;
while (true) {
int left = 2 * index + 1;
int right = 2 * index + 2;
if (left < size && array[left].distance < array[minIndex].distance) {
minIndex = left;
}
if (right < size && array[right].distance < array[minIndex].distance) {
minIndex = right;
}
if (minIndex == index) {
break;
}
// Swap
NodeDistance temp = array[index];
array[index] = array[minIndex];
array[minIndex] = temp;
index = minIndex;
}
}
}
// Dijkstra’s algorithm to find shortest paths
public String dijkstra(int n, int[][] edges, int source) {
// Build adjacency list
List<List<int[]>> adjList = new ArrayList<>();
for (int i = 0; i < n; i++) {
adjList.add(new ArrayList<>());
}
for (int[] edge : edges) {
adjList.get(edge[0]).add(new int[]{edge[1], edge[2]}); // Directed: u -> v, weight
}
// Initialize distances and priority queue
long[] distances = new long[n];
Arrays.fill(distances, Long.MAX_VALUE);
distances[source] = 0;
MinPriorityQueue queue = new MinPriorityQueue();
queue.enqueue(source, 0);
boolean[] visited = new boolean[n];
// Process nodes
while (true) {
NodeDistance nodeDist = queue.dequeue();
if (nodeDist == null) break;
int node = nodeDist.node;
long dist = nodeDist.distance;
if (visited[node]) continue;
visited[node] = true;
for (int[] neighbor : adjList.get(node)) {
int v = neighbor[0];
long w = neighbor[1];
if (dist + w < distances[v]) {
distances[v] = dist + w;
queue.enqueue(v, dist + w);
}
}
}
// Format output
StringBuilder result = new StringBuilder();
for (int i = 0; i < n; i++) {
if (distances[i] == Long.MAX_VALUE) {
result.append(i).append(":INF");
} else {
result.append(i).append(":").append(distances[i]);
}
if (i < n - 1) result.append(" ");
}
return result.toString();
}
// Helper class for test cases
static class GraphTestCase {
int n;
int[][] edges;
int source;
GraphTestCase(int n, int[][] edges, int source) {
this.n = n;
this.edges = edges;
this.source = source;
}
}
// Main method to test Dijkstra’s algorithm
public static void main(String[] args) {
DijkstrasAlgorithm dijkstra = new DijkstrasAlgorithm();
// Test cases
List<GraphTestCase> testCases = new ArrayList<>();
// Test case 1: Connected graph
testCases.add(new GraphTestCase(
4,
new int[][]{{0,1,1}, {0,2,4}, {1,2,2}, {1,3,6}},
0
));
// Test case 2: Unreachable nodes
testCases.add(new GraphTestCase(
3,
new int[][]{{0,1,2}},
2
));
// Test case 3: Single node
testCases.add(new GraphTestCase(
1,
new int[][]{},
0
));
// Test case 4: Dense graph
testCases.add(new GraphTestCase(
4,
new int[][]{{0,1,1}, {0,2,5}, {0,3,10}, {1,2,3}, {1,3,3}, {2,3,2}},
0
));
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
GraphTestCase test = testCases.get(i);
System.out.println("Test case " + (i + 1) + ": n=" + test.n + ", source=" + test.source);
System.out.println("Edges: " + Arrays.deepToString(test.edges));
String result = dijkstra.dijkstra(test.n, test.edges, test.source);
System.out.println("Shortest distances: " + result + "\n");
}
}
}
Output
Running the main method produces:
Test case 1: n=4, source=0
Edges: [[0, 1, 1], [0, 2, 4], [1, 2, 2], [1, 3, 6]]
Shortest distances: 0:0 1:1 2:3 3:7
Test case 2: n=3, source=2
Edges: [[0, 1, 2]]
Shortest distances: 0:INF 1:INF 2:0
Test case 3: n=1, source=0
Edges: []
Shortest distances: 0:0
Test case 4: n=4, source=0
Edges: [[0, 1, 1], [0, 2, 5], [0, 3, 10], [1, 2, 3], [1, 3, 3], [2, 3, 2]]
Shortest distances: 0:0 1:1 2:4 3:4
Explanation:
- Test case 1: From source 0, shortest paths: 0→1 (1), 0→1→2 (1+2=3), 0→1→3 (1+6=7).
- Test case 2: From source 2, no outgoing edges, so only 2 has distance 0; others are INF.
- Test case 3: Single node 0 has distance 0.
- Test case 4: From source 0, shortest paths: 0→1 (1), 0→1→2 (1+3=4), 0→1→3 (1+3=4).
How It Works
- NodeDistance: Stores a node and its tentative distance.
- MinPriorityQueue:
- Uses a min-heap to prioritize nodes with smaller distances.
enqueue: Adds node-distance pair, sifts up.dequeue: Removes smallest-distance node, sifts down.
- dijkstra:
- Builds adjacency list from edges (directed).
- Initializes distances to INF, source to 0.
- Enqueues source with distance 0.
- Processes nodes in order of increasing distance, updating neighbors’ distances if shorter.
- Uses visited array to avoid reprocessing nodes.
- Formats distances (INF for unreachable).
- Example Trace (Test case 1):
- n=4, edges=[[0,1,1], [0,2,4], [1,2,2], [1,3,6]], source=0.
- Adjacency list: [0: [(1,1),(2,4)], 1: [(2,2),(3,6)], 2: [], 3: []].
- Initialize: distances=[0,INF,INF,INF], enqueue (0,0).
- Dequeue (0,0): Update 1:1, 2:4, enqueue (1,1),(2,4).
- Dequeue (1,1): Update 2:3 (1+2), 3:7 (1+6), enqueue (2,3),(3,7).
- Dequeue (2,3): No updates.
- Dequeue (3,7): No updates.
- Result: "0:0 1:1 2:3 3:7".
- Main Method: Tests connected graph, unreachable nodes, single node, and dense graph.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Enqueue/Dequeue | O(log V) | O(1) |
| Dijkstra | O((V + E) log V) | O(V + E) |
Note:
- V is the number of vertices, E is the number of edges.
- Time complexity: O(log V) for enqueue/dequeue; O((V + E) log V) for processing each node and edge with heap operations.
- Space complexity: O(V) for queue, distances, and visited array; O(E) for adjacency list.
- Worst case: O((V + E) log V) time, O(V + E) space for dense graphs.
✅ Tip: Use a min-heap priority queue in Dijkstra’s algorithm to efficiently select the node with the smallest tentative distance. Test with sparse and dense graphs to verify correctness.
⚠ Warning: Ensure edge weights are non-negative, as Dijkstra’s algorithm does not handle negative weights. Use a visited array to avoid reprocessing nodes in cyclic graphs.
Kth Largest Element
Problem Statement
Write a Java program that uses a priority queue to find the kth largest element in an array of integers. The program should enqueue elements into a min-heap of size k, maintaining the k largest elements, and return the root (smallest of the k largest) as the kth largest element. Test the implementation with at least three different arrays and k values, including edge cases like k=1, k equal to the array length, and arrays with duplicate elements. You can visualize this as sorting through a list of exam scores to find the kth highest score, keeping only the top k scores in a priority queue to efficiently track the kth largest.
Input:
- An array of integers (e.g., [3, 1, 4, 1, 5]).
- An integer k (1 ≤ k ≤ array length). Output: The kth largest element in the array (e.g., for k=2 in [3, 1, 4, 1, 5], output 4). Constraints:
- Array length is between 1 and 10^5.
- Elements are integers in the range [-10^9, 10^9].
- 1 ≤ k ≤ array length.
- Assume k is valid for the input array. Example:
- Input: arr=[3, 1, 4, 1, 5], k=2
- Output: 4
- Explanation: The 2nd largest element is 4 (largest is 5, 2nd is 4).
- Input: arr=[1], k=1
- Output: 1
- Explanation: Only one element, so 1st largest is 1.
- Input: arr=[4, 4, 4], k=2
- Output: 4
- Explanation: All elements are 4, so 2nd largest is 4.
Pseudocode
CLASS MinPriorityQueue
SET array to new integer array of size capacity
SET size to 0
SET capacity to input capacity
FUNCTION enqueue(number)
IF size equals capacity THEN
IF number <= array[0] THEN
RETURN false
ENDIF
SET array[0] to number
CALL siftDown(0)
RETURN true
ENDIF
SET array[size] to number
CALL siftUp(size)
INCREMENT size
RETURN true
ENDFUNCTION
FUNCTION siftUp(index)
WHILE index > 0
SET parent to (index - 1) / 2
IF array[index] >= array[parent] THEN
BREAK
ENDIF
SWAP array[index] and array[parent]
SET index to parent
ENDWHILE
ENDFUNCTION
FUNCTION siftDown(index)
SET minIndex to index
WHILE true
SET left to 2 * index + 1
SET right to 2 * index + 2
IF left < size AND array[left] < array[minIndex] THEN
SET minIndex to left
ENDIF
IF right < size AND array[right] < array[minIndex] THEN
SET minIndex to right
ENDIF
IF minIndex equals index THEN
BREAK
ENDIF
SWAP array[index] and array[minIndex]
SET index to minIndex
ENDWHILE
ENDFUNCTION
FUNCTION peek()
IF size equals 0 THEN
RETURN null
ENDIF
RETURN array[0]
ENDFUNCTION
ENDCLASS
FUNCTION findKthLargest(arr, k)
CREATE queue as new MinPriorityQueue with capacity k
FOR each number in arr
CALL queue.enqueue(number)
ENDFOR
RETURN queue.peek()
ENDFUNCTION
FUNCTION main()
SET testCases to array of (array, k) pairs
FOR each testCase in testCases
PRINT test case details
CALL findKthLargest(testCase.arr, testCase.k)
PRINT kth largest element
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
MinPriorityQueueclass using a min-heap with: a. An array of fixed capacity k, withsizeto track elements. b.enqueue: If size < k, add to end and sift up; if size = k and number > root, replace root and sift down. c.siftUp: Move smaller number up to maintain min-heap property. d.siftDown: Move larger number down to maintain min-heap property. e.peek: Return root (smallest of k largest elements). - In
findKthLargest: a. Create a min-heap with capacity k. b. For each number in the array:- Enqueue to heap, maintaining k largest elements. c. Return the root (kth largest).
- In
main, test with at least three arrays and k values, including edge cases like k=1, k=length, and duplicates.
Java Implementation
import java.util.*;
public class KthLargestElement {
// Custom min-heap priority queue implementation
static class MinPriorityQueue {
private int[] array;
private int size;
private int capacity;
public MinPriorityQueue(int capacity) {
this.array = new int[capacity];
this.size = 0;
this.capacity = capacity;
}
public boolean enqueue(int number) {
if (size == capacity) {
if (number <= array[0]) {
return false; // Smaller than kth largest
}
array[0] = number; // Replace smallest
siftDown(0);
return true;
}
array[size] = number;
siftUp(size);
size++;
return true;
}
private void siftUp(int index) {
while (index > 0) {
int parent = (index - 1) / 2;
if (array[index] >= array[parent]) {
break;
}
// Swap
int temp = array[index];
array[index] = array[parent];
array[parent] = temp;
index = parent;
}
}
private void siftDown(int index) {
int minIndex = index;
while (true) {
int left = 2 * index + 1;
int right = 2 * index + 2;
if (left < size && array[left] < array[minIndex]) {
minIndex = left;
}
if (right < size && array[right] < array[minIndex]) {
minIndex = right;
}
if (minIndex == index) {
break;
}
// Swap
int temp = array[index];
array[index] = array[minIndex];
array[minIndex] = temp;
index = minIndex;
}
}
public Integer peek() {
if (size == 0) {
return null;
}
return array[0];
}
}
// Finds the kth largest element in the array
public int findKthLargest(int[] arr, int k) {
MinPriorityQueue queue = new MinPriorityQueue(k);
for (int num : arr) {
queue.enqueue(num);
}
return queue.peek();
}
// Helper class for test cases
static class TestCase {
int[] arr;
int k;
TestCase(int[] arr, int k) {
this.arr = arr;
this.k = k;
}
}
// Main method to test kth largest element
public static void main(String[] args) {
KthLargestElement finder = new KthLargestElement();
// Test cases
List<TestCase> testCases = new ArrayList<>();
// Test case 1: Normal array
testCases.add(new TestCase(new int[]{3, 1, 4, 1, 5}, 2));
// Test case 2: Single element
testCases.add(new TestCase(new int[]{1}, 1));
// Test case 3: Duplicates
testCases.add(new TestCase(new int[]{4, 4, 4}, 2));
// Test case 4: k equals array length
testCases.add(new TestCase(new int[]{7, 2, 9, 4, 6}, 5));
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
TestCase test = testCases.get(i);
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Array: " + Arrays.toString(test.arr));
System.out.println("k: " + test.k);
int result = finder.findKthLargest(test.arr, test.k);
System.out.println("Kth largest element: " + result + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Array: [3, 1, 4, 1, 5]
k: 2
Kth largest element: 4
Test case 2:
Array: [1]
k: 1
Kth largest element: 1
Test case 3:
Array: [4, 4, 4]
k: 2
Kth largest element: 4
Test case 4:
Array: [7, 2, 9, 4, 6]
k: 5
Kth largest element: 2
Explanation:
- Test case 1: In [3, 1, 4, 1, 5], k=2, the 2nd largest is 4 (largest is 5).
- Test case 2: In [1], k=1, the 1st largest is 1.
- Test case 3: In [4, 4, 4], k=2, the 2nd largest is 4 (all elements equal).
- Test case 4: In [7, 2, 9, 4, 6], k=5, the 5th largest is 2 (smallest element).
How It Works
- MinPriorityQueue:
- Uses a min-heap array to maintain the k largest elements, with smallest at root.
enqueue: If size < k, adds number and sifts up; if size = k and number > root, replaces root and sifts down.siftUp: Moves smaller number up to maintain min-heap.siftDown: Moves larger number down to maintain min-heap.peek: Returns root (kth largest).
- findKthLargest:
- Creates a min-heap of size k.
- Enqueues each array element, keeping only the k largest.
- Returns the root (kth largest).
- Example Trace (Test case 1):
- arr=[3, 1, 4, 1, 5], k=2.
- Enqueue 3: heap=[3], size=1.
- Enqueue 1: heap=[1,3], size=2.
- Enqueue 4: heap=[1,3], replace 1 with 4, heap=[3,4], size=2.
- Enqueue 1: heap=[3,4] (1 < 3, ignored).
- Enqueue 5: heap=[3,4], replace 3 with 5, heap=[4,5], size=2.
- Peek: Returns 4 (2nd largest).
- Main Method: Tests normal array, single element, duplicates, and k=length.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Enqueue | O(log k) | O(1) |
| Find Kth Largest | O(n log k) | O(k) |
Note:
- n is the array length, k is the input k.
- Time complexity: O(log k) for each enqueue (heap operations); O(n log k) for n elements.
- Space complexity: O(k) for the min-heap of size k.
- Worst case: O(n log k) time for large n, O(k) space.
✅ Tip: Use a min-heap of size k to efficiently find the kth largest element, as it discards smaller elements early. Test with edge cases like k=1 or k=length to ensure correctness.
⚠ Warning: Ensure k is valid (1 ≤ k ≤ array length) to avoid errors. Handle duplicates carefully, as they may affect the heap’s behavior.
Merge K Sorted Lists
Problem Statement
Write a Java program that merges k sorted arrays into a single sorted array using a priority queue. The program should enqueue the first element of each array along with its array index and value, and repeatedly dequeue the smallest element and enqueue the next element from the same array, continuing until all elements are processed. Test the implementation with various cases, including multiple arrays, single arrays, empty arrays, and arrays with duplicate elements. You can visualize this as combining k sorted lists of exam scores into one sorted list, using a priority queue to always pick the smallest score available from the lists.
Input:
- A list of k sorted arrays, where each array contains integers (e.g., [[1,4,5], [1,3,4], [2,6]]).
- k is the number of arrays (0 ≤ k ≤ 10^3). Output: A single sorted array containing all elements from the input arrays (e.g., [1,1,2,3,4,4,5,6]). Constraints:
- Each array is sorted in non-decreasing order.
- Array lengths are between 0 and 10^4.
- Elements are integers in the range [-10^9, 10^9].
- Total number of elements across all arrays is at most 10^4. Example:
- Input: arrays=[[1,4,5], [1,3,4], [2,6]]
- Output: [1,1,2,3,4,4,5,6]
- Explanation: Merges three sorted arrays into one sorted array.
- Input: arrays=[[]]
- Output: []
- Explanation: Single empty array results in an empty output.
Pseudocode
CLASS Element
SET value to integer
SET arrayIndex to integer
SET elementIndex to integer
ENDCLASS
CLASS MinPriorityQueue
SET array to new Element array of size 1000
SET size to 0
FUNCTION enqueue(value, arrayIndex, elementIndex)
IF size equals array length THEN
RETURN false
ENDIF
SET array[size] to new Element(value, arrayIndex, elementIndex)
CALL siftUp(size)
INCREMENT size
RETURN true
ENDFUNCTION
FUNCTION siftUp(index)
WHILE index > 0
SET parent to (index - 1) / 2
IF array[index].value >= array[parent].value THEN
BREAK
ENDIF
SWAP array[index] and array[parent]
SET index to parent
ENDWHILE
ENDFUNCTION
FUNCTION dequeue()
IF size equals 0 THEN
RETURN null
ENDIF
SET result to array[0]
SET array[0] to array[size - 1]
DECREMENT size
IF size > 0 THEN
CALL siftDown(0)
ENDIF
RETURN result
ENDFUNCTION
FUNCTION siftDown(index)
SET minIndex to index
WHILE true
SET left to 2 * index + 1
SET right to 2 * index + 2
IF left < size AND array[left].value < array[minIndex].value THEN
SET minIndex to left
ENDIF
IF right < size AND array[right].value < array[minIndex].value THEN
SET minIndex to right
ENDIF
IF minIndex equals index THEN
BREAK
ENDIF
SWAP array[index] and array[minIndex]
SET index to minIndex
ENDWHILE
ENDFUNCTION
ENDCLASS
FUNCTION mergeKSortedLists(arrays)
IF arrays is empty THEN
RETURN empty array
ENDIF
CREATE queue as new MinPriorityQueue
CREATE result as new list
FOR i from 0 to arrays length - 1
IF arrays[i] is not empty THEN
ENQUEUE (arrays[i][0], i, 0) to queue
ENDIF
ENDFOR
WHILE queue is not empty
SET element to queue.dequeue()
ADD element.value to result
SET arrayIndex to element.arrayIndex
SET elementIndex to element.elementIndex + 1
IF elementIndex < arrays[arrayIndex] length THEN
ENQUEUE (arrays[arrayIndex][elementIndex], arrayIndex, elementIndex) to queue
ENDIF
ENDWHILE
RETURN result as array
ENDFUNCTION
FUNCTION main()
SET testCases to array of lists of sorted arrays
FOR each testCase in testCases
PRINT test case details
CALL mergeKSortedLists(testCase)
PRINT merged sorted array
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define an
Elementclass to store a value, its array index, and its position in that array. - Define a
MinPriorityQueueclass using a min-heap with: a. An array to storeElementobjects, prioritized by value. b.enqueue: Add element, sift up to maintain min-heap. c.siftUp: Move smaller value up. d.dequeue: Remove smallest value, sift down. e.siftDown: Move larger value down. - In
mergeKSortedLists: a. If input is empty, return empty array. b. Create a min-heap and result list. c. Enqueue first element of each non-empty array with its array index and position. d. While queue is not empty:- Dequeue smallest element, add to result.
- If more elements exist in its array, enqueue the next element. e. Return result as array.
- In
main, test with multiple arrays, single arrays, empty arrays, and duplicates.
Java Implementation
import java.util.*;
public class MergeKSortedLists {
// Class to store element value, array index, and element index
static class Element {
int value;
int arrayIndex;
int elementIndex;
Element(int value, int arrayIndex, int elementIndex) {
this.value = value;
this.arrayIndex = arrayIndex;
this.elementIndex = elementIndex;
}
}
// Custom min-heap priority queue for elements
static class MinPriorityQueue {
private Element[] array;
private int size;
private static final int DEFAULT_SIZE = 1000;
public MinPriorityQueue() {
array = new Element[DEFAULT_SIZE];
size = 0;
}
public boolean enqueue(int value, int arrayIndex, int elementIndex) {
if (size == array.length) {
return false; // Queue full
}
array[size] = new Element(value, arrayIndex, elementIndex);
siftUp(size);
size++;
return true;
}
private void siftUp(int index) {
while (index > 0) {
int parent = (index - 1) / 2;
if (array[index].value >= array[parent].value) {
break;
}
// Swap
Element temp = array[index];
array[index] = array[parent];
array[parent] = temp;
index = parent;
}
}
public Element dequeue() {
if (size == 0) {
return null; // Queue empty
}
Element result = array[0];
array[0] = array[size - 1];
size--;
if (size > 0) {
siftDown(0);
}
return result;
}
private void siftDown(int index) {
int minIndex = index;
while (true) {
int left = 2 * index + 1;
int right = 2 * index + 2;
if (left < size && array[left].value < array[minIndex].value) {
minIndex = left;
}
if (right < size && array[right].value < array[minIndex].value) {
minIndex = right;
}
if (minIndex == index) {
break;
}
// Swap
Element temp = array[index];
array[index] = array[minIndex];
array[minIndex] = temp;
index = minIndex;
}
}
}
// Merges k sorted arrays into one sorted array
public int[] mergeKSortedLists(int[][] arrays) {
if (arrays == null || arrays.length == 0) {
return new int[0];
}
MinPriorityQueue queue = new MinPriorityQueue();
List<Integer> result = new ArrayList<>();
// Enqueue first element of each non-empty array
for (int i = 0; i < arrays.length; i++) {
if (arrays[i] != null && arrays[i].length > 0) {
queue.enqueue(arrays[i][0], i, 0);
}
}
// Process queue
while (true) {
Element element = queue.dequeue();
if (element == null) break;
result.add(element.value);
int arrayIndex = element.arrayIndex;
int elementIndex = element.elementIndex + 1;
if (elementIndex < arrays[arrayIndex].length) {
queue.enqueue(arrays[arrayIndex][elementIndex], arrayIndex, elementIndex);
}
}
// Convert result to array
int[] merged = new int[result.size()];
for (int i = 0; i < result.size(); i++) {
merged[i] = result.get(i);
}
return merged;
}
// Helper class for test cases
static class TestCase {
int[][] arrays;
TestCase(int[][] arrays) {
this.arrays = arrays;
}
}
// Main method to test merge k sorted lists
public static void main(String[] args) {
MergeKSortedLists merger = new MergeKSortedLists();
// Test cases
List<TestCase> testCases = new ArrayList<>();
// Test case 1: Multiple arrays
testCases.add(new TestCase(
new int[][]{{1,4,5}, {1,3,4}, {2,6}}
));
// Test case 2: Single array
testCases.add(new TestCase(
new int[][]{{1,2,3}}
));
// Test case 3: Empty arrays
testCases.add(new TestCase(
new int[][]{{}}
));
// Test case 4: Arrays with duplicates
testCases.add(new TestCase(
new int[][]{{1,1,1}, {2,2}, {1,3}}
));
// Test case 5: Mixed empty and non-empty
testCases.add(new TestCase(
new int[][]{{}, {1,2}, {3}}
));
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
TestCase test = testCases.get(i);
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input arrays: " + Arrays.deepToString(test.arrays));
int[] result = merger.mergeKSortedLists(test.arrays);
System.out.println("Merged sorted array: " + Arrays.toString(result) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input arrays: [[1, 4, 5], [1, 3, 4], [2, 6]]
Merged sorted array: [1, 1, 2, 3, 4, 4, 5, 6]
Test case 2:
Input arrays: [[1, 2, 3]]
Merged sorted array: [1, 2, 3]
Test case 3:
Input arrays: [[]]
Merged sorted array: []
Test case 4:
Input arrays: [[1, 1, 1], [2, 2], [1, 3]]
Merged sorted array: [1, 1, 1, 1, 2, 2, 3]
Test case 5:
Input arrays: [[], [1, 2], [3]]
Merged sorted array: [1, 2, 3]
Explanation:
- Test case 1: Merges three arrays, producing sorted output [1,1,2,3,4,4,5,6].
- Test case 2: Single array [1,2,3] remains unchanged.
- Test case 3: Single empty array results in empty output.
- Test case 4: Arrays with duplicates merge into [1,1,1,1,2,2,3].
- Test case 5: Empty and non-empty arrays merge into [1,2,3].
How It Works
- Element: Stores a value, its array index, and position in that array.
- MinPriorityQueue:
- Uses a min-heap to prioritize smallest values.
enqueue: Adds element, sifts up.dequeue: Removes smallest element, sifts down.
- mergeKSortedLists:
- Handles empty input by returning empty array.
- Enqueues first element of each non-empty array.
- Dequeues smallest element, adds to result, enqueues next element from same array.
- Converts result list to array.
- Example Trace (Test case 1):
- arrays=[[1,4,5], [1,3,4], [2,6]].
- Enqueue: (1,0,0), (1,1,0), (2,2,0) → heap=[(1,0,0),(1,1,0),(2,2,0)].
- Dequeue: (1,0,0), add 1, enqueue (4,0,1) → heap=[(1,1,0),(4,0,1),(2,2,0)].
- Dequeue: (1,1,0), add 1, enqueue (3,1,1) → heap=[(2,2,0),(4,0,1),(3,1,1)].
- Continue until heap empty: result=[1,1,2,3,4,4,5,6].
- Main Method: Tests multiple arrays, single array, empty arrays, duplicates, and mixed cases.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Enqueue/Dequeue | O(log k) | O(1) |
| Merge K Lists | O(N log k) | O(k + N) |
Note:
- k is the number of arrays, N is the total number of elements.
- Time complexity: O(log k) for enqueue/dequeue; O(N log k) for N elements with heap of size k.
- Space complexity: O(k) for heap, O(N) for result array.
- Worst case: O(N log k) time, O(k + N) space for large N.
✅ Tip: Use a min-heap with size k to efficiently merge sorted arrays by always selecting the smallest available element. Track array indices to enqueue the next element.
⚠ Warning: Handle empty arrays and null inputs to avoid errors. Ensure the priority queue size is sufficient for k arrays.
Min-Heap to Max-Heap
Problem Statement
Write a Java program that modifies a min-heap-based priority queue implementation to support a max-heap, where larger values have higher priority. The program should enqueue integers into the max-heap and dequeue the largest element, printing each operation’s result. Test the modified implementation with various sequences of enqueue and dequeue operations, including edge cases like empty queues, single elements, and duplicate values. You can visualize this as a priority task list where the most urgent task (highest number) is always processed first, with the max-heap ensuring the largest value is at the root.
Input: A sequence of operations, where each operation is either:
- Enqueue: Add an integer to the max-heap (e.g., enqueue 5).
- Dequeue: Remove and return the largest element from the max-heap. Output: For each operation, print the action performed (enqueue or dequeue) and the result, such as the dequeued value or a message if the queue is empty. Constraints:
- Queue size is between 0 and 10^5.
- Elements are integers in the range [-10^9, 10^9].
- The queue may be empty when dequeue is called. Example:
- Input: Operations = [enqueue(3), enqueue(1), enqueue(4), dequeue, enqueue(2), dequeue]
- Output:
Enqueued 3 Enqueued 1 Enqueued 4 Dequeued: 4 Enqueued 2 Dequeued: 3 - Explanation: The max-heap ensures the largest element (4, then 3) is dequeued first.
- Input: Operations = [dequeue on empty queue]
- Output:
Queue empty, cannot dequeue
Pseudocode
CLASS MaxPriorityQueue
SET array to new integer array of size 1000
SET size to 0
FUNCTION enqueue(number)
IF size equals array length THEN
RETURN false (queue full)
ENDIF
SET array[size] to number
CALL siftUp(size)
INCREMENT size
RETURN true
ENDFUNCTION
FUNCTION siftUp(index)
WHILE index > 0
SET parent to (index - 1) / 2
IF array[index] <= array[parent] THEN
BREAK
ENDIF
SWAP array[index] and array[parent]
SET index to parent
ENDWHILE
ENDFUNCTION
FUNCTION dequeue()
IF size equals 0 THEN
RETURN null (queue empty)
ENDIF
SET result to array[0]
SET array[0] to array[size - 1]
DECREMENT size
IF size > 0 THEN
CALL siftDown(0)
ENDIF
RETURN result
ENDFUNCTION
FUNCTION siftDown(index)
SET maxIndex to index
WHILE true
SET left to 2 * index + 1
SET right to 2 * index + 2
IF left < size AND array[left] > array[maxIndex] THEN
SET maxIndex to left
ENDIF
IF right < size AND array[right] > array[maxIndex] THEN
SET maxIndex to right
ENDIF
IF maxIndex equals index THEN
BREAK
ENDIF
SWAP array[index] and array[maxIndex]
SET index to maxIndex
ENDWHILE
ENDFUNCTION
FUNCTION isEmpty()
RETURN size equals 0
ENDFUNCTION
ENDCLASS
FUNCTION testMaxPriorityQueue(operations)
CREATE queue as new MaxPriorityQueue
FOR each operation in operations
IF operation.type equals "enqueue" THEN
IF queue.enqueue(operation.number) THEN
PRINT enqueued number
ELSE
PRINT enqueue failed: queue full
ENDIF
ELSE IF operation.type equals "dequeue" THEN
SET number to queue.dequeue()
IF number is null THEN
PRINT queue empty message
ELSE
PRINT dequeued number
ENDIF
ENDIF
ENDFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of operation sequences
FOR each testCase in testCases
PRINT test case details
CALL testMaxPriorityQueue(testCase)
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
MaxPriorityQueueclass using a max-heap with: a. An array to store integers, withsizeto track elements. b.enqueue: Add number to end, sift up to maintain max-heap property (larger values up). c.siftUp: Move larger number up if greater than parent. d.dequeue: Remove root (largest), move last element to root, sift down. e.siftDown: Move smaller number down to maintain max-heap property. f.isEmpty: Check if queue is empty. - In
testMaxPriorityQueue: a. Create aMaxPriorityQueue. b. For each operation:- If "enqueue", add number, print action or "Queue full".
- If "dequeue", remove largest number, print result or "Queue empty".
- In
main, test with sequences of enqueue and dequeue operations, including empty queues, single elements, and duplicates.
Java Implementation
import java.util.*;
public class MinHeapToMaxHeap {
// Custom max-heap priority queue implementation
static class MaxPriorityQueue {
private int[] array;
private int size;
private static final int DEFAULT_SIZE = 1000;
public MaxPriorityQueue() {
array = new int[DEFAULT_SIZE];
size = 0;
}
public boolean enqueue(int number) {
if (size == array.length) {
return false; // Queue full
}
array[size] = number;
siftUp(size);
size++;
return true;
}
private void siftUp(int index) {
while (index > 0) {
int parent = (index - 1) / 2;
if (array[index] <= array[parent]) {
break;
}
// Swap
int temp = array[index];
array[index] = array[parent];
array[parent] = temp;
index = parent;
}
}
public Integer dequeue() {
if (size == 0) {
return null; // Queue empty
}
int result = array[0];
array[0] = array[size - 1];
size--;
if (size > 0) {
siftDown(0);
}
return result;
}
private void siftDown(int index) {
int maxIndex = index;
while (true) {
int left = 2 * index + 1;
int right = 2 * index + 2;
if (left < size && array[left] > array[maxIndex]) {
maxIndex = left;
}
if (right < size && array[right] > array[maxIndex]) {
maxIndex = right;
}
if (maxIndex == index) {
break;
}
// Swap
int temp = array[index];
array[index] = array[maxIndex];
array[maxIndex] = temp;
index = maxIndex;
}
}
public boolean isEmpty() {
return size == 0;
}
}
// Helper class for operations
static class Operation {
String type;
Integer number;
Operation(String type, Integer number) {
this.type = type;
this.number = number;
}
}
// Tests max priority queue with operations
public void testMaxPriorityQueue(List<Operation> operations) {
MaxPriorityQueue queue = new MaxPriorityQueue();
for (Operation op : operations) {
if (op.type.equals("enqueue")) {
if (queue.enqueue(op.number)) {
System.out.println("Enqueued " + op.number);
} else {
System.out.println("Enqueue " + op.number + " failed: Queue full");
}
} else if (op.type.equals("dequeue")) {
Integer number = queue.dequeue();
if (number == null) {
System.out.println("Queue empty, cannot dequeue");
} else {
System.out.println("Dequeued: " + number);
}
}
}
}
// Main method to test max priority queue
public static void main(String[] args) {
MinHeapToMaxHeap tester = new MinHeapToMaxHeap();
// Test cases
List<List<Operation>> testCases = new ArrayList<>();
// Test case 1: Normal sequence
List<Operation> case1 = Arrays.asList(
new Operation("enqueue", 3),
new Operation("enqueue", 1),
new Operation("enqueue", 4),
new Operation("dequeue", null),
new Operation("enqueue", 2),
new Operation("dequeue", null)
);
testCases.add(case1);
// Test case 2: Empty queue
List<Operation> case2 = Arrays.asList(
new Operation("dequeue", null)
);
testCases.add(case2);
// Test case 3: Single element
List<Operation> case3 = Arrays.asList(
new Operation("enqueue", 5),
new Operation("dequeue", null)
);
testCases.add(case3);
// Test case 4: Duplicate values
List<Operation> case4 = Arrays.asList(
new Operation("enqueue", 2),
new Operation("enqueue", 2),
new Operation("enqueue", 2),
new Operation("dequeue", null),
new Operation("dequeue", null)
);
testCases.add(case4);
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
System.out.println("Test case " + (i + 1) + ":");
tester.testMaxPriorityQueue(testCases.get(i));
System.out.println();
}
}
}
Output
Running the main method produces:
Test case 1:
Enqueued 3
Enqueued 1
Enqueued 4
Dequeued: 4
Enqueued 2
Dequeued: 3
Test case 2:
Queue empty, cannot dequeue
Test case 3:
Enqueued 5
Dequeued: 5
Test case 4:
Enqueued 2
Enqueued 2
Enqueued 2
Dequeued: 2
Dequeued: 2
Explanation:
- Test case 1: Enqueues 3, 1, 4 (heap: [4,1,3]); dequeues 4; enqueues 2 (heap: [3,1,2]); dequeues 3.
- Test case 2: Dequeue on empty queue returns error.
- Test case 3: Enqueues 5, dequeues 5.
- Test case 4: Enqueues three 2’s, dequeues two 2’s in FIFO order.
How It Works
- MaxPriorityQueue:
- Uses a max-heap array to maintain largest element at root.
enqueue: Adds to end, sifts up if larger than parent.siftUp: Moves larger number up to maintain max-heap.dequeue: Removes root, moves last element to root, sifts down.siftDown: Moves smaller number down to maintain max-heap.
- testMaxPriorityQueue:
- Enqueues numbers, printing action or "Queue full".
- Dequeues largest number, printing result or "Queue empty".
- Example Trace (Test case 1):
- Enqueue 3: array=[3], size=1.
- Enqueue 1: array=[3,1], size=2.
- Enqueue 4: array=[4,1,3], size=3 (after sift-up).
- Dequeue: Returns 4, array=[3,1], size=2 (after sift-down).
- Enqueue 2: array=[3,1,2], size=3.
- Dequeue: Returns 3, array=[2,1], size=2.
- Main Method: Tests normal sequence, empty queue, single element, and duplicates.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Enqueue | O(log n) | O(1) |
| Dequeue | O(log n) | O(1) |
| Full Algorithm | O(n log n) | O(n) |
Note:
- n is the number of operations.
- Time complexity: O(log n) for enqueue/dequeue due to heap operations; O(n log n) for n operations.
- Space complexity: O(n) for storing up to n elements.
- Worst case: O(n log n) time, O(n) space for many operations.
✅ Tip: Convert a min-heap to a max-heap by reversing comparison logic to prioritize larger values. Test with duplicate values to verify FIFO behavior for equal priorities.
⚠ Warning: Ensure heap properties are maintained after each operation. Handle empty queue cases to avoid null pointer issues.
Task Scheduler
Problem Statement
Write a Java program that simulates a task scheduler using a priority queue. The program should enqueue tasks, each with a description (string) and priority (integer), and dequeue them in order of highest priority, printing each task as it is processed. The priority queue should ensure that tasks with higher priority numbers are dequeued first, and tasks with equal priorities are processed in FIFO order. Test the implementation with various sequences of enqueue and dequeue operations, including edge cases like empty queues and tasks with equal priorities. You can visualize this as a manager prioritizing urgent tasks in a to-do list, ensuring the most critical tasks are handled first.
Input: A sequence of operations, where each operation is either:
- Enqueue: Add a task with a description (string) and priority (integer) to the priority queue (e.g., "Write report", 3).
- Dequeue: Remove and process the task with the highest priority. Output: For each operation, print the action performed (enqueue or dequeue) and, for dequeue, the processed task’s description or a message if the queue is empty. Constraints:
- Queue size is between 0 and 10^5.
- Task descriptions are non-empty strings containing printable ASCII characters, or null (handled gracefully).
- Priorities are integers in the range [-10^9, 10^9].
- The queue may be empty when dequeue is called. Example:
- Input: Operations = [enqueue("Task1", 3), enqueue("Task2", 1), enqueue("Task3", 3), dequeue, dequeue]
- Output:
Enqueued Task1 (priority 3) Enqueued Task2 (priority 1) Enqueued Task3 (priority 3) Processed task: Task1 Processed task: Task3 - Explanation: Task1 and Task3 (priority 3) are processed before Task2 (priority 1) due to higher priority.
- Input: Operations = [dequeue on empty queue]
- Output:
Queue empty, cannot dequeue
Pseudocode
CLASS Task
SET description to string
SET priority to integer
ENDCLASS
CLASS PriorityQueue
SET array to new Task array of size 1000
SET size to 0
FUNCTION enqueue(description, priority)
IF size equals array length THEN
RETURN false (queue full)
ENDIF
SET newTask to new Task(description, priority)
SET array[size] to newTask
CALL siftUp(size)
INCREMENT size
RETURN true
ENDFUNCTION
FUNCTION siftUp(index)
WHILE index > 0
SET parent to (index - 1) / 2
IF array[index].priority <= array[parent].priority THEN
BREAK
ENDIF
SWAP array[index] and array[parent]
SET index to parent
ENDWHILE
ENDFUNCTION
FUNCTION dequeue()
IF size equals 0 THEN
RETURN null (queue empty)
ENDIF
SET result to array[0]
SET array[0] to array[size - 1]
DECREMENT size
IF size > 0 THEN
CALL siftDown(0)
ENDIF
RETURN result
ENDFUNCTION
FUNCTION siftDown(index)
SET maxIndex to index
WHILE true
SET left to 2 * index + 1
SET right to 2 * index + 2
IF left < size AND array[left].priority > array[maxIndex].priority THEN
SET maxIndex to left
ENDIF
IF right < size AND array[right].priority > array[maxIndex].priority THEN
SET maxIndex to right
ENDIF
IF maxIndex equals index THEN
BREAK
ENDIF
SWAP array[index] and array[maxIndex]
SET index to maxIndex
ENDWHILE
ENDFUNCTION
FUNCTION isEmpty()
RETURN size equals 0
ENDFUNCTION
ENDCLASS
FUNCTION simulateTaskScheduler(operations)
CREATE queue as new PriorityQueue
FOR each operation in operations
IF operation.type equals "enqueue" THEN
CALL queue.enqueue(operation.description, operation.priority)
PRINT enqueued task and priority
ELSE IF operation.type equals "dequeue" THEN
SET task to queue.dequeue()
IF task is null THEN
PRINT queue empty message
ELSE
PRINT processed task description
ENDIF
ENDIF
ENFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of operation sequences
FOR each testCase in testCases
PRINT test case details
CALL simulateTaskScheduler(testCase)
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Taskclass to store task description (string) and priority (integer). - Define a
PriorityQueueclass using a max-heap with: a. An array to store tasks, withsizeto track elements. b.enqueue: Add task to end, sift up to maintain heap property. c.dequeue: Remove root (highest priority), move last task to root, sift down. d.siftUp: Move task up if its priority is higher than parent’s. e.siftDown: Move task down to maintain max-heap property. f.isEmpty: Check if queue is empty. - In
simulateTaskScheduler: a. Create aPriorityQueue. b. For each operation:- If "enqueue", add task with priority, print action.
- If "dequeue", remove highest-priority task, print task or "Queue empty".
- In
main, test with sequences of enqueue and dequeue operations, including empty queues, single tasks, and equal priorities.
Java Implementation
import java.util.*;
public class TaskScheduler {
// Task class to store description and priority
static class Task {
String description;
int priority;
Task(String description, int priority) {
this.description = description;
this.priority = priority;
}
}
// Custom priority queue implementation (max-heap)
static class PriorityQueue {
private Task[] array;
private int size;
private static final int DEFAULT_SIZE = 1000;
public PriorityQueue() {
array = new Task[DEFAULT_SIZE];
size = 0;
}
public boolean enqueue(String description, int priority) {
if (size == array.length) {
return false; // Queue full
}
Task newTask = new Task(description, priority);
array[size] = newTask;
siftUp(size);
size++;
return true;
}
private void siftUp(int index) {
while (index > 0) {
int parent = (index - 1) / 2;
if (array[index].priority <= array[parent].priority) {
break;
}
// Swap
Task temp = array[index];
array[index] = array[parent];
array[parent] = temp;
index = parent;
}
}
public Task dequeue() {
if (size == 0) {
return null; // Queue empty
}
Task result = array[0];
array[0] = array[size - 1];
size--;
if (size > 0) {
siftDown(0);
}
return result;
}
private void siftDown(int index) {
int maxIndex = index;
while (true) {
int left = 2 * index + 1;
int right = 2 * index + 2;
if (left < size && array[left].priority > array[maxIndex].priority) {
maxIndex = left;
}
if (right < size && array[right].priority > array[maxIndex].priority) {
maxIndex = right;
}
if (maxIndex == index) {
break;
}
// Swap
Task temp = array[index];
array[index] = array[maxIndex];
array[maxIndex] = temp;
index = maxIndex;
}
}
public boolean isEmpty() {
return size == 0;
}
}
// Helper class for operations
static class Operation {
String type;
String description;
Integer priority;
Operation(String type, String description, Integer priority) {
this.type = type;
this.description = description;
this.priority = priority;
}
}
// Simulates task scheduler
public void simulateTaskScheduler(List<Operation> operations) {
PriorityQueue queue = new PriorityQueue();
for (Operation op : operations) {
if (op.type.equals("enqueue")) {
boolean success = queue.enqueue(op.description, op.priority);
if (success) {
System.out.println("Enqueued " + (op.description != null ? op.description : "null") +
" (priority " + op.priority + ")");
} else {
System.out.println("Enqueue " + (op.description != null ? op.description : "null") +
" failed: Queue full");
}
} else if (op.type.equals("dequeue")) {
Task task = queue.dequeue();
if (task == null) {
System.out.println("Queue empty, cannot dequeue");
} else {
System.out.println("Processed task: " + task.description);
}
}
}
}
// Main method to test task scheduler
public static void main(String[] args) {
TaskScheduler scheduler = new TaskScheduler();
// Test cases
List<List<Operation>> testCases = new ArrayList<>();
// Test case 1: Normal sequence
List<Operation> case1 = Arrays.asList(
new Operation("enqueue", "Write report", 3),
new Operation("enqueue", "Send email", 1),
new Operation("enqueue", "Attend meeting", 3),
new Operation("dequeue", null, null),
new Operation("dequeue", null, null)
);
testCases.add(case1);
// Test case 2: Empty queue
List<Operation> case2 = Arrays.asList(
new Operation("dequeue", null, null)
);
testCases.add(case2);
// Test case 3: Single task
List<Operation> case3 = Arrays.asList(
new Operation("enqueue", "Debug code", 5),
new Operation("dequeue", null, null)
);
testCases.add(case3);
// Test case 4: Equal priorities
List<Operation> case4 = Arrays.asList(
new Operation("enqueue", "Task A", 2),
new Operation("enqueue", "Task B", 2),
new Operation("enqueue", "Task C", 2),
new Operation("dequeue", null, null),
new Operation("dequeue", null, null)
);
testCases.add(case4);
// Test case 5: Null task description
List<Operation> case5 = Arrays.asList(
new Operation("enqueue", null, 4),
new Operation("dequeue", null, null)
);
testCases.add(case5);
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
System.out.println("Test case " + (i + 1) + ":");
scheduler.simulateTaskScheduler(testCases.get(i));
System.out.println();
}
}
}
Output
Running the main method produces:
Test case 1:
Enqueued Write report (priority 3)
Enqueued Send email (priority 1)
Enqueued Attend meeting (priority 3)
Processed task: Write report
Processed task: Attend meeting
Test case 2:
Queue empty, cannot dequeue
Test case 3:
Enqueued Debug code (priority 5)
Processed task: Debug code
Test case 4:
Enqueued Task A (priority 2)
Enqueued Task B (priority 2)
Enqueued Task C (priority 2)
Processed task: Task A
Processed task: Task B
Test case 5:
Enqueued null (priority 4)
Processed task: null
Explanation:
- Test case 1: Enqueues tasks with priorities 3, 1, 3; dequeues highest-priority tasks (Write report, Attend meeting) before Send email.
- Test case 2: Dequeue on empty queue returns error.
- Test case 3: Enqueues and dequeues a single task.
- Test case 4: Enqueues three tasks with equal priority (2), processes in FIFO order (Task A, Task B).
- Test case 5: Enqueues and dequeues a task with null description.
How It Works
- Task: Stores a task’s description and priority.
- PriorityQueue:
- Uses a max-heap array to store tasks, ensuring highest-priority task is at root.
enqueue: Adds task to end, sifts up to maintain heap property.dequeue: Removes root, moves last task to root, sifts down.- Equal priorities maintain FIFO order due to heap insertion order.
- simulateTaskScheduler:
- Enqueues tasks, printing action and priority.
- Dequeues highest-priority task, printing description or "Queue empty".
- Example Trace (Test case 1):
- Enqueue "Write report" (3): array=[(Write report,3)], size=1.
- Enqueue "Send email" (1): array=[(Write report,3),(Send email,1)], size=2.
- Enqueue "Attend meeting" (3): array=[(Attend meeting,3),(Send email,1),(Write report,3)], size=3 (after sift-up).
- Dequeue: Returns "Attend meeting", array=[(Write report,3),(Send email,1)], size=2 (after sift-down).
- Dequeue: Returns "Write report", array=[(Send email,1)], size=1.
- Main Method: Tests normal sequences, empty queue, single task, equal priorities, and null description.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Enqueue | O(log n) | O(1) |
| Dequeue | O(log n) | O(1) |
| Full Algorithm | O(n log n) | O(n) |
Note:
- n is the number of operations.
- Time complexity: O(log n) for enqueue/dequeue due to heap operations; O(n log n) for n operations.
- Space complexity: O(n) for storing up to n tasks.
- Worst case: O(n log n) time for n enqueue/dequeue operations, O(n) space.
✅ Tip: Use a max-heap for a priority queue to ensure highest-priority tasks are processed first. Test with equal priorities to verify FIFO behavior for same-priority tasks.
⚠ Warning: Handle null descriptions and empty queue cases to avoid errors. Ensure the heap maintains its property after each enqueue and dequeue operation.
Linked Lists Problem Solving with DSA
🛠 What you will learn
This section outlines the format used for each DSA problem statement, designed to guide students through solving exercise problems in a clear, structured learning process. Each component serves a specific purpose to enhance understanding and practical application:
- Problem Statement: Clearly defines the task, including inputs, outputs, constraints, and an example. A real-world analogy makes the problem relatable, aligning with the engaging tone of the "Core Data Structures" summary, ensuring students grasp the problem’s context.
- Pseudocode: Provides a high-level, language-agnostic outline of the solution using standardized syntax (e.g.,
FUNCTION,IF,SET,RETURN). This helps students understand the logic before diving into code, bridging theory and implementation. - Algorithm Steps: Breaks down the pseudocode into detailed, actionable steps, connecting theoretical logic to practical coding. This ensures students can follow the implementation process systematically.
- Java Implementation: Offers a complete, commented Java code solution that students can study and run. It includes a
mainmethod with at least four test cases (e.g., target present, absent, middle element, and edge cases like duplicates or single elements) to verify correctness and explore different scenarios, maintaining a beginner-friendly approach. - Output: Shows the expected results from the test cases, including indices, comparisons, and execution times (where applicable), helping students verify their code’s correctness and understand the algorithm’s behavior.
- How It Works: Traces the algorithm’s execution with a detailed example, showing step-by-step how the search narrows down the range. This reinforces understanding by illustrating the algorithm in action.
- Complexity Analysis Table: Summarizes time and space complexity for best, average, and worst cases, teaching students to evaluate efficiency and compare trade-offs across implementations.
- Tip or Warning Box: Provides practical advice (e.g., when to use DSA) and highlights pitfalls (e.g., ensuring the array is sorted), guiding students toward best practices and common error avoidance.
Each exercise includes test cases to verify correctness and performance, aligning with a learning platform featuring embedded compilers for hands-on practice and visualizations to illustrate the halving process of DSA.
Cycle Detection
Problem Statement
Write a Java program that extends a singly linked list implementation to detect if the list contains a cycle using Floyd’s Tortoise and Hare algorithm. The linked list consists of nodes, each containing an integer value and a reference to the next node. A cycle exists if a node’s next pointer points to an earlier node in the list. The program should return true if a cycle is detected and false otherwise, and test the implementation with both cyclic and acyclic lists of varying sizes, including empty lists, single-node lists, and multi-node lists with or without cycles. You can visualize this as checking if a chain of numbered cards loops back to an earlier card, using two pointers moving at different speeds to detect the loop.
Input:
- A singly linked list of integers (e.g., 1→2→3→4→2, where 4 points back to 2, forming a cycle). Output: A boolean indicating whether the list contains a cycle (true for cyclic, false for acyclic), along with a string representation of the list for clarity. Constraints:
- The list size is between 0 and 10^5.
- Node values are integers in the range [-10^9, 10^9].
- The list may be empty or contain a cycle at any position. Example:
- Input: 1→2→3→4→2 (cycle: 4→2)
- Output: true
- Explanation: A cycle exists as 4 points back to 2.
- Input: 1→2→3→null
- Output: false
- Explanation: No cycle, as the list terminates at null.
- Input: []
- Output: false
- Explanation: An empty list has no cycle.
Pseudocode
CLASS Node
SET value to integer
SET next to Node (null by default)
ENDCLASS
FUNCTION hasCycle(head)
IF head is null OR head.next is null THEN
RETURN false
ENDIF
SET slow to head
SET fast to head
WHILE fast is not null AND fast.next is not null
SET slow to slow.next
SET fast to fast.next.next
IF slow equals fast THEN
RETURN true
ENDIF
ENDWHILE
RETURN false
ENDFUNCTION
FUNCTION toString(head, limit)
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
SET count to 0
CREATE visited as new set
WHILE current is not null AND count < limit
IF visited contains current THEN
APPEND "->" + current.value + " (cycle)" to result
BREAK
ENDIF
ADD current to visited
APPEND current.value to result
IF current.next is not null THEN
APPEND " " to result
ENDIF
SET current to current.next
INCREMENT count
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of linked lists (some cyclic, some acyclic)
FOR each testCase in testCases
PRINT test case details
CALL hasCycle(testCase.head)
PRINT whether cycle exists
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Nodeclass with: a. An integervalue. b. Anextpointer to the next node. - In
hasCycle(Floyd’s Tortoise and Hare): a. If head or head.next is null, return false (no cycle possible). b. Initializeslowandfastpointers to head. c. Whilefastandfast.nextare not null:- Move
slowone step,fasttwo steps. - If
slowequalsfast, a cycle exists (return true). d. Iffastreaches null, no cycle exists (return false).
- Move
- In
toString: a. If head is null, return "[]". b. Traverse the list with a limit to avoid infinite loops in cyclic lists. c. Use a set to detect cycles and indicate them in the output. d. Return space-separated values with cycle annotation if present. - In
main, test with empty, single-node, multi-node acyclic, and cyclic lists.
Java Implementation
import java.util.*;
public class CycleDetection {
// Node class for the linked list
static class Node {
int value;
Node next;
Node(int value) {
this.value = value;
this.next = null;
}
}
// Detects if the linked list has a cycle using Floyd’s algorithm
public boolean hasCycle(Node head) {
if (head == null || head.next == null) {
return false;
}
Node slow = head;
Node fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return true;
}
}
return false;
}
// Converts linked list to string for output, handling cycles
public String toString(Node head) {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
Node current = head;
int count = 0;
int limit = 100; // Prevent infinite loop
Set<Node> visited = new HashSet<>();
while (current != null && count < limit) {
if (visited.contains(current)) {
result.append("->").append(current.value).append(" (cycle)");
break;
}
visited.add(current);
result.append(current.value);
if (current.next != null) {
result.append(" ");
}
current = current.next;
count++;
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
Node head;
boolean hasCycle;
// Creates acyclic list from values
TestCase(int[] values) {
this.hasCycle = false;
if (values.length == 0) {
head = null;
return;
}
head = new Node(values[0]);
Node current = head;
for (int i = 1; i < values.length; i++) {
current.next = new Node(values[i]);
current = current.next;
}
}
// Creates cyclic list with cycle at specified position
TestCase(int[] values, int cyclePos) {
this.hasCycle = true;
if (values.length == 0) {
head = null;
return;
}
head = new Node(values[0]);
Node current = head;
Node cycleNode = null;
for (int i = 1; i < values.length; i++) {
current.next = new Node(values[i]);
current = current.next;
if (i == cyclePos) {
cycleNode = current;
}
}
if (cyclePos >= 0 && cyclePos < values.length) {
current.next = cycleNode; // Create cycle
}
}
}
// Main method to test cycle detection
public static void main(String[] args) {
CycleDetection detector = new CycleDetection();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{1, 2, 3, 4}, 2), // Cyclic: 1→2→3→4→3
new TestCase(new int[]{1, 2, 3}), // Acyclic: 1→2→3
new TestCase(new int[]{}), // Empty
new TestCase(new int[]{5}), // Single node
new TestCase(new int[]{1, 2, 3, 4, 5}, 0) // Cyclic: 1→2→3→4→5→1
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
TestCase test = testCases[i];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input list: " + detector.toString(test.head));
boolean result = detector.hasCycle(test.head);
System.out.println("Has cycle: " + result + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input list: 1 2 3 4->3 (cycle)
Has cycle: true
Test case 2:
Input list: 1 2 3
Has cycle: false
Test case 3:
Input list: []
Has cycle: false
Test case 4:
Input list: 5
Has cycle: false
Test case 5:
Input list: 1 2 3 4 5->1 (cycle)
Has cycle: true
Explanation:
- Test case 1: List 1→2→3→4 with 4→3 forms a cycle, returns true.
- Test case 2: Acyclic list 1→2→3, returns false.
- Test case 3: Empty list, returns false.
- Test case 4: Single node 5, returns false.
- Test case 5: List 1→2→3→4→5 with 5→1 forms a cycle, returns true.
How It Works
- Node: Stores an integer value and a next pointer.
- hasCycle (Floyd’s Tortoise and Hare):
- Returns false if list is empty or has one node.
- Uses
slow(moves one step) andfast(moves two steps) pointers. - If
slowmeetsfast, a cycle exists. - If
fastreaches null, no cycle exists.
- toString:
- Handles cycles by tracking visited nodes and limiting traversal.
- Outputs values with spaces, appending cycle annotation if detected.
- Example Trace (Test case 1):
- Input: 1→2→3→4→3 (cycle at 3).
- slow=1, fast=1.
- Step 1: slow=2, fast=3.
- Step 2: slow=3, fast=4.
- Step 3: slow=4, fast=3.
- Step 4: slow=3, fast=3 (meet, cycle detected).
- Return: true.
- Main Method: Tests cyclic (cycle at different positions), acyclic, empty, and single-node lists.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Has Cycle | O(n) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of nodes in the list.
- Time complexity: O(n) for hasCycle (Floyd’s algorithm traverses list); O(n) for toString (traverse with cycle detection).
- Space complexity: O(1) for hasCycle (two pointers); O(n) for toString (visited set and StringBuilder).
- Worst case: O(n) time, O(n) space for large lists in output.
✅ Tip: Use Floyd’s Tortoise and Hare algorithm for efficient cycle detection with O(1) space. Test with cycles at different positions to ensure robustness.
⚠ Warning: Ensure pointers are checked for null to avoid null pointer exceptions. Be cautious when printing cyclic lists to avoid infinite loops.
Merge Two Sorted Lists
Problem Statement
Write a Java program that merges two sorted singly linked lists into a single sorted linked list. Each linked list consists of nodes containing integer values in non-decreasing order. The program should merge the lists by comparing node values and constructing a new sorted list, preserving the sorted order. Test the implementation with different inputs, including empty lists, single-node lists, lists of varying lengths, and lists with duplicate values. You can visualize this as combining two sorted stacks of numbered cards into one sorted stack, picking the smaller card from the top of either stack each time.
Input:
- Two sorted singly linked lists of integers (e.g., 1→3→5 and 2→4→6). Output: A single sorted linked list as a string (e.g., "1 2 3 4 5 6"). Constraints:
- The list sizes are between 0 and 10^5.
- Node values are integers in the range [-10^9, 10^9].
- Both lists are sorted in non-decreasing order.
- Either or both lists may be empty. Example:
- Input: list1 = 1→3→5, list2 = 2→4→6
- Output: "1 2 3 4 5 6"
- Explanation: Merges into a sorted list 1→2→3→4→5→6.
- Input: list1 = [], list2 = []
- Output: "[]"
- Explanation: Merging two empty lists results in an empty list.
- Input: list1 = 1→1, list2 = 1→2
- Output: "1 1 1 2"
- Explanation: Merges lists with duplicates into a sorted list.
Pseudocode
CLASS Node
SET value to integer
SET next to Node (null by default)
ENDCLASS
FUNCTION mergeTwoLists(list1, list2)
CREATE dummy as new Node(0)
SET tail to dummy
SET current1 to list1
SET current2 to list2
WHILE current1 is not null AND current2 is not null
IF current1.value <= current2.value THEN
SET tail.next to current1
SET current1 to current1.next
ELSE
SET tail.next to current2
SET current2 to current2.next
ENDIF
SET tail to tail.next
ENDWHILE
IF current1 is not null THEN
SET tail.next to current1
ELSE IF current2 is not null THEN
SET tail.next to current2
ENDIF
RETURN dummy.next
ENDFUNCTION
FUNCTION toString(head)
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
WHILE current is not null
APPEND current.value to result
IF current.next is not null THEN
APPEND " " to result
ENDIF
SET current to current.next
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of pairs of linked lists
FOR each testCase in testCases
PRINT test case details
CALL mergeTwoLists(testCase.list1, testCase.list2)
PRINT merged list using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Nodeclass with: a. An integervalue. b. Anextpointer to the next node. - In
mergeTwoLists: a. Create a dummy node to simplify list construction. b. Initializetailto dummy,current1to list1,current2to list2. c. While both lists have nodes:- Compare
current1.valueandcurrent2.value. - Append smaller node to
tail.next, advance corresponding pointer. - Move
tailto appended node. d. Append remaining nodes from list1 or list2, if any. e. Returndummy.nextas the merged list head.
- Compare
- In
toString: a. If head is null, return "[]". b. Traverse the list, append each value to a StringBuilder with spaces. c. Return the string representation. - In
main, test with non-empty lists, empty lists, single-node lists, and lists with duplicates.
Java Implementation
public class MergeTwoSortedLists {
// Node class for the linked list
static class Node {
int value;
Node next;
Node(int value) {
this.value = value;
this.next = null;
}
}
// Merges two sorted linked lists
public Node mergeTwoLists(Node list1, Node list2) {
Node dummy = new Node(0);
Node tail = dummy;
Node current1 = list1;
Node current2 = list2;
while (current1 != null && current2 != null) {
if (current1.value <= current2.value) {
tail.next = current1;
current1 = current1.next;
} else {
tail.next = current2;
current2 = current2.next;
}
tail = tail.next;
}
if (current1 != null) {
tail.next = current1;
} else if (current2 != null) {
tail.next = current2;
}
return dummy.next;
}
// Converts linked list to string for output
public String toString(Node head) {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
Node current = head;
while (current != null) {
result.append(current.value);
if (current.next != null) {
result.append(" ");
}
current = current.next;
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
Node list1;
Node list2;
TestCase(int[] values1, int[] values2) {
// Create list1
if (values1.length == 0) {
list1 = null;
} else {
list1 = new Node(values1[0]);
Node current = list1;
for (int i = 1; i < values1.length; i++) {
current.next = new Node(values1[i]);
current = current.next;
}
}
// Create list2
if (values2.length == 0) {
list2 = null;
} else {
list2 = new Node(values2[0]);
Node current = list2;
for (int i = 1; i < values2.length; i++) {
current.next = new Node(values2[i]);
current = current.next;
}
}
}
}
// Main method to test merging sorted lists
public static void main(String[] args) {
MergeTwoSortedLists merger = new MergeTwoSortedLists();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{1, 3, 5}, new int[]{2, 4, 6}), // Both non-empty
new TestCase(new int[]{}, new int[]{}), // Both empty
new TestCase(new int[]{1}, new int[]{}), // One empty
new TestCase(new int[]{1, 1}, new int[]{1, 2}), // Duplicates
new TestCase(new int[]{1, 2, 3}, new int[]{4}) // Different lengths
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
TestCase test = testCases[i];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("List 1: " + merger.toString(test.list1));
System.out.println("List 2: " + merger.toString(test.list2));
Node merged = merger.mergeTwoLists(test.list1, test.list2);
System.out.println("Merged list: " + merger.toString(merged) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
List 1: 1 3 5
List 2: 2 4 6
Merged list: 1 2 3 4 5 6
Test case 2:
List 1: []
List 2: []
Merged list: []
Test case 3:
List 1: 1
List 2: []
Merged list: 1
Test case 4:
List 1: 1 1
List 2: 1 2
Merged list: 1 1 1 2
Test case 5:
List 1: 1 2 3
List 2: 4
Merged list: 1 2 3 4
Explanation:
- Test case 1: Merges 1→3→5 and 2→4→6 into 1→2→3→4→5→6.
- Test case 2: Both empty lists result in an empty list.
- Test case 3: Merges 1 with empty list, resulting in 1.
- Test case 4: Merges lists with duplicates 1→1 and 1→2 into 1→1→1→2.
- Test case 5: Merges 1→2→3 and 4 into 1→2→3→4.
How It Works
- Node: Stores an integer value and a next pointer.
- mergeTwoLists:
- Uses a dummy node to simplify list construction.
- Compares nodes from both lists, appending the smaller to the result.
- Advances the corresponding list’s pointer and moves the tail.
- Appends any remaining nodes from either list.
- toString: Converts the list to a space-separated string, returning "[]" for empty lists.
- Example Trace (Test case 1):
- list1: 1→3→5, list2: 2→4→6.
- dummy→null, tail=dummy, current1=1, current2=2.
- Step 1: 1≤2, dummy→1, current1=3, tail=1.
- Step 2: 3>2, dummy→1→2, current2=4, tail=2.
- Step 3: 3≤4, dummy→1→2→3, current1=5, tail=3.
- Step 4: 5>4, dummy→1→2→3→4, current2=6, tail=4.
- Step 5: 5≤6, dummy→1→2→3→4→5, current1=null, tail=5.
- Step 6: Append 6, dummy→1→2→3→4→5→6.
- Return: 1→2→3→4→5→6.
- Main Method: Tests non-empty lists, empty lists, one empty list, duplicates, and different lengths.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Merge Two Lists | O(n + m) | O(1) |
| To String | O(n) | O(n) |
Note:
- n and m are the lengths of the two input lists.
- Time complexity: O(n + m) for merging (single pass through both lists); O(n) for toString (traverse list).
- Space complexity: O(1) for merging (constant pointers); O(n) for toString (StringBuilder).
- Worst case: O(n + m) time, O(n + m) space for output with large lists.
✅ Tip: Use a dummy node to simplify merging by avoiding special cases for the head. Compare values iteratively to maintain sorted order.
⚠ Warning: Ensure pointers are updated correctly to avoid losing list segments. Handle empty list cases to prevent null pointer exceptions.
Middle Element Finder
Problem Statement
Write a Java program that finds the middle element of a singly linked list in a single pass using the fast-and-slow pointer technique. The linked list consists of nodes, each containing an integer value and a reference to the next node. For lists with an odd number of nodes, return the middle node’s value; for even-length lists, return the value of the second middle node (e.g., in 1→2→3→4, return 3). The program should handle edge cases like empty lists and single-node lists. Test the implementation with linked lists of varying sizes, including empty lists, single-node lists, and lists with odd and even lengths. You can visualize this as finding the middle card in a chain of numbered cards, using two pointers where one moves twice as fast to reach the middle efficiently.
Input:
- A singly linked list of integers (e.g., 1→2→3→4→5). Output: The value of the middle node as an integer, or -1 if the list is empty (e.g., 3 for 1→2→3→4→5). Constraints:
- The list size is between 0 and 10^5.
- Node values are integers in the range [-10^9, 10^9].
- The list may be empty. Example:
- Input: 1→2→3→4→5
- Output: 3
- Explanation: Middle node of 5 nodes is the 3rd node (value 3).
- Input: 1→2→3→4
- Output: 3
- Explanation: Second middle node of 4 nodes is the 3rd node (value 3).
- Input: []
- Output: -1
- Explanation: Empty list returns -1.
Pseudocode
CLASS Node
SET value to integer
SET next to Node (null by default)
ENDCLASS
FUNCTION findMiddle(head)
IF head is null THEN
RETURN -1
ENDIF
SET slow to head
SET fast to head
WHILE fast is not null AND fast.next is not null
SET slow to slow.next
SET fast to fast.next.next
ENDWHILE
RETURN slow.value
ENDFUNCTION
FUNCTION toString(head)
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
WHILE current is not null
APPEND current.value to result
IF current.next is not null THEN
APPEND " " to result
ENDIF
SET current to current.next
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of linked lists
FOR each testCase in testCases
PRINT test case details
CALL findMiddle(testCase.head)
PRINT middle element
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Nodeclass with: a. An integervalue. b. Anextpointer to the next node. - In
findMiddle: a. If head is null, return -1 (empty list). b. Initializeslowandfastpointers to head. c. Whilefastandfast.nextare not null:- Move
slowone step,fasttwo steps. d. Whenfastreaches the end,slowis at the middle node. e. Returnslow.value.
- Move
- In
toString: a. If head is null, return "[]". b. Traverse the list, append each value to a StringBuilder with spaces. c. Return the string representation. - In
main, test with empty, single-node, odd-length, and even-length lists.
Java Implementation
public class MiddleElementFinder {
// Node class for the linked list
static class Node {
int value;
Node next;
Node(int value) {
this.value = value;
this.next = null;
}
}
// Finds the middle element using fast-and-slow pointer technique
public int findMiddle(Node head) {
if (head == null) {
return -1;
}
Node slow = head;
Node fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow.value;
}
// Converts linked list to string for output
public String toString(Node head) {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
Node current = head;
while (current != null) {
result.append(current.value);
if (current.next != null) {
result.append(" ");
}
current = current.next;
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
Node head;
TestCase(int[] values) {
if (values.length == 0) {
head = null;
return;
}
head = new Node(values[0]);
Node current = head;
for (int i = 1; i < values.length; i++) {
current.next = new Node(values[i]);
current = current.next;
}
}
}
// Main method to test middle element finder
public static void main(String[] args) {
MiddleElementFinder finder = new MiddleElementFinder();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{1, 2, 3, 4, 5}), // Odd length
new TestCase(new int[]{1, 2, 3, 4}), // Even length
new TestCase(new int[]{}), // Empty list
new TestCase(new int[]{5}), // Single node
new TestCase(new int[]{1, 1, 1, 1, 1, 1}) // Even length with duplicates
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
TestCase test = testCases[i];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input list: " + finder.toString(test.head));
int middle = finder.findMiddle(test.head);
System.out.println("Middle element: " + middle + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input list: 1 2 3 4 5
Middle element: 3
Test case 2:
Input list: 1 2 3 4
Middle element: 3
Test case 3:
Input list: []
Middle element: -1
Test case 4:
Input list: 5
Middle element: 5
Test case 5:
Input list: 1 1 1 1 1 1
Middle element: 1
Explanation:
- Test case 1: Odd-length list 1→2→3→4→5, middle is 3rd node (3).
- Test case 2: Even-length list 1→2→3→4, second middle is 3rd node (3).
- Test case 3: Empty list, returns -1.
- Test case 4: Single node 5, returns 5.
- Test case 5: Even-length list 1→1→1→1→1→1, second middle is 4th node (1).
How It Works
- Node: Stores an integer value and a next pointer.
- findMiddle:
- Returns -1 for empty lists.
- Uses
slow(moves one step) andfast(moves two steps) pointers. - When
fastreaches the end,slowis at the middle (or second middle for even length).
- toString: Converts the list to a space-separated string, returning "[]" for empty lists.
- Example Trace (Test case 1):
- Input: 1→2→3→4→5.
- Initial: slow=1, fast=1.
- Step 1: slow=2, fast=3.
- Step 2: slow=3, fast=5.
- Step 3: fast=null, slow=3.
- Return: 3.
- Main Method: Tests odd-length, even-length, empty, single-node, and duplicate lists.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Find Middle | O(n) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of nodes in the list.
- Time complexity: O(n) for findMiddle (single pass, fast pointer covers list in n/2 steps); O(n) for toString (traverse list).
- Space complexity: O(1) for findMiddle (two pointers); O(n) for toString (StringBuilder).
- Worst case: O(n) time, O(n) space for output with large lists.
✅ Tip: Use the fast-and-slow pointer technique to find the middle element efficiently in one pass. For even-length lists, choose whether to return the first or second middle node based on problem requirements.
⚠ Warning: Ensure null checks for
fastandfast.nextto avoid null pointer exceptions. Handle empty lists by returning a sentinel value like -1.
Playlist Manager
Problem Statement
Write a Java program that simulates a music playlist using a singly linked list. Each node in the list represents a song with a string name. The program should allow users to add songs (insert at the head or tail of the list), remove songs (delete by value, removing the first occurrence), and print the playlist. Test the implementation with various sequences of operations, including adding and removing songs, handling empty playlists, and dealing with duplicate song names. You can visualize this as managing a playlist where songs are added to the beginning or end, removed by name, and displayed as a sequence of song titles.
Input:
- A sequence of operations, where each operation is:
- Add at head: Insert a song name at the start of the playlist.
- Add at tail: Insert a song name at the end of the playlist.
- Remove: Delete the first occurrence of a song by name.
- Print: Display the current playlist. Output: For each operation, print the action performed (e.g., "Added song at head", "Removed song", or the playlist as a string). If a song to remove is not found, print an appropriate message. Constraints:
- The playlist size is between 0 and 10^5.
- Song names are non-empty strings of length up to 100 characters.
- The playlist may be empty or contain duplicate song names. Example:
- Input: Operations = [addHead("Song1"), addTail("Song2"), addHead("Song3"), print, remove("Song2"), print]
- Output:
Added Song1 at head Added Song2 at tail Added Song3 at head Playlist: Song3 Song1 Song2 Removed Song2 Playlist: Song3 Song1 - Input: Operations = [remove("Song1"), print]
- Output:
Song Song1 not found Playlist: []
Pseudocode
CLASS Node
SET song to string
SET next to Node (null by default)
ENDCLASS
CLASS PlaylistManager
SET head to null
FUNCTION addHead(song)
CREATE newNode as new Node(song)
SET newNode.next to head
SET head to newNode
ENDFUNCTION
FUNCTION addTail(song)
CREATE newNode as new Node(song)
IF head is null THEN
SET head to newNode
RETURN
ENDIF
SET current to head
WHILE current.next is not null
SET current to current.next
ENDWHILE
SET current.next to newNode
ENDFUNCTION
FUNCTION removeSong(song)
IF head is null THEN
RETURN false
ENDIF
IF head.song equals song THEN
SET head to head.next
RETURN true
ENDIF
SET current to head
WHILE current.next is not null
IF current.next.song equals song THEN
SET current.next to current.next.next
RETURN true
ENDIF
SET current to current.next
ENDWHILE
RETURN false
ENDFUNCTION
FUNCTION toString()
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
WHILE current is not null
APPEND current.song to result
IF current.next is not null THEN
APPEND " " to result
ENDIF
SET current to current.next
ENDWHILE
RETURN result as string
ENDFUNCTION
ENDCLASS
FUNCTION testPlaylist(operations)
CREATE playlist as new PlaylistManager
FOR each operation in operations
IF operation.type equals "addHead" THEN
CALL playlist.addHead(operation.song)
PRINT added song at head
ELSE IF operation.type equals "addTail" THEN
CALL playlist.addTail(operation.song)
PRINT added song at tail
ELSE IF operation.type equals "remove" THEN
IF playlist.removeSong(operation.song) THEN
PRINT removed song
ELSE
PRINT song not found
ENDIF
ELSE IF operation.type equals "print" THEN
PRINT playlist using toString
ENDIF
ENDFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of operation sequences
FOR each testCase in testCases
PRINT test case details
CALL testPlaylist(testCase)
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Nodeclass with: a. A stringsongfor the song name. b. Anextpointer to the next node. - Define a
PlaylistManagerclass with: a.addHead: Insert a new node with the song at the start. b.addTail: Traverse to the end and append a new node. c.removeSong: Remove the first node with the given song name, return true if found. d.toString: Convert the playlist to a space-separated string. - In
testPlaylist: a. Create aPlaylistManager. b. For each operation:- Add at head: Call
addHead, print action. - Add at tail: Call
addTail, print action. - Remove: Call
removeSong, print success or failure. - Print: Call
toString, print playlist.
- Add at head: Call
- In
main, test with sequences including adds, removes, empty playlists, and duplicates.
Java Implementation
import java.util.*;
public class PlaylistManager {
// Node class for the linked list
static class Node {
String song;
Node next;
Node(String song) {
this.song = song;
this.next = null;
}
}
// PlaylistManager class to manage songs
static class Playlist {
private Node head;
public void addHead(String song) {
Node newNode = new Node(song);
newNode.next = head;
head = newNode;
}
public void addTail(String song) {
Node newNode = new Node(song);
if (head == null) {
head = newNode;
return;
}
Node current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}
public boolean removeSong(String song) {
if (head == null) {
return false;
}
if (head.song.equals(song)) {
head = head.next;
return true;
}
Node current = head;
while (current.next != null) {
if (current.next.song.equals(song)) {
current.next = current.next.next;
return true;
}
current = current.next;
}
return false;
}
public String toString() {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
Node current = head;
while (current != null) {
result.append(current.song);
if (current.next != null) {
result.append(" ");
}
current = current.next;
}
return result.toString();
}
}
// Helper class for operations
static class Operation {
String type;
String song;
Operation(String type, String song) {
this.type = type;
this.song = song;
}
}
// Tests playlist operations
public void testPlaylist(List<Operation> operations) {
Playlist playlist = new Playlist();
for (Operation op : operations) {
if (op.type.equals("addHead")) {
playlist.addHead(op.song);
System.out.println("Added " + op.song + " at head");
} else if (op.type.equals("addTail")) {
playlist.addTail(op.song);
System.out.println("Added " + op.song + " at tail");
} else if (op.type.equals("remove")) {
if (playlist.removeSong(op.song)) {
System.out.println("Removed " + op.song);
} else {
System.out.println("Song " + op.song + " not found");
}
} else if (op.type.equals("print")) {
System.out.println("Playlist: " + playlist.toString());
}
}
}
// Main method to test playlist manager
public static void main(String[] args) {
PlaylistManager manager = new PlaylistManager();
// Test cases
List<List<Operation>> testCases = new ArrayList<>();
// Test case 1: Normal operations
testCases.add(Arrays.asList(
new Operation("addHead", "Song1"),
new Operation("addTail", "Song2"),
new Operation("addHead", "Song3"),
new Operation("print", null),
new Operation("remove", "Song2"),
new Operation("print", null)
));
// Test case 2: Empty playlist
testCases.add(Arrays.asList(
new Operation("remove", "Song1"),
new Operation("print", null)
));
// Test case 3: Single song
testCases.add(Arrays.asList(
new Operation("addHead", "Song1"),
new Operation("print", null),
new Operation("remove", "Song1"),
new Operation("print", null)
));
// Test case 4: Duplicates
testCases.add(Arrays.asList(
new Operation("addHead", "Song1"),
new Operation("addTail", "Song1"),
new Operation("addHead", "Song2"),
new Operation("print", null),
new Operation("remove", "Song1"),
new Operation("print", null)
));
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
System.out.println("Test case " + (i + 1) + ":");
manager.testPlaylist(testCases.get(i));
System.out.println();
}
}
}
Output
Running the main method produces:
Test case 1:
Added Song1 at head
Added Song2 at tail
Added Song3 at head
Playlist: Song3 Song1 Song2
Removed Song2
Playlist: Song3 Song1
Test case 2:
Song Song1 not found
Playlist: []
Test case 3:
Added Song1 at head
Playlist: Song1
Removed Song1
Playlist: []
Test case 4:
Added Song1 at head
Added Song1 at tail
Added Song2 at head
Playlist: Song2 Song1 Song1
Removed Song1
Playlist: Song2 Song1
Explanation:
- Test case 1: Adds Song1 (head), Song2 (tail), Song3 (head), prints "Song3 Song1 Song2", removes Song2, prints "Song3 Song1".
- Test case 2: Attempts to remove Song1 from empty playlist, prints not found, prints "[]".
- Test case 3: Adds Song1, prints "Song1", removes Song1, prints "[]".
- Test case 4: Adds Song1 (head), Song1 (tail), Song2 (head), prints "Song2 Song1 Song1", removes first Song1, prints "Song2 Song1".
How It Works
- Node: Stores a string song name and a next pointer.
- Playlist:
addHead: Inserts new node at the start in O(1).addTail: Traverses to end, appends node in O(n).removeSong: Removes first occurrence of song, returns true if found, O(n).toString: Returns space-separated song names, "[]" for empty.
- testPlaylist: Executes operations, printing actions and results.
- Example Trace (Test case 1):
- addHead("Song1"): head=Song1→null.
- addTail("Song2"): head=Song1→Song2.
- addHead("Song3"): head=Song3→Song1→Song2.
- print: "Song3 Song1 Song2".
- remove("Song2"): head=Song3→Song1.
- print: "Song3 Song1".
- Main Method: Tests normal operations, empty playlist, single song, and duplicates.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Add Head | O(1) | O(1) |
| Add Tail | O(n) | O(1) |
| Remove Song | O(n) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of nodes in the playlist.
- Time complexity: O(1) for addHead; O(n) for addTail and removeSong (traverse list); O(n) for toString (traverse list).
- Space complexity: O(1) for operations (constant pointers); O(n) for toString (StringBuilder).
- Worst case: O(n) time, O(n) space for output with large playlists.
✅ Tip: Use a linked list for flexible playlist management, with head insertion for quick additions and tail insertion for appending. Test with duplicates to ensure correct removal of the first occurrence.
⚠ Warning: Handle empty playlist cases to avoid null pointer exceptions. Ensure removal checks all nodes to find the target song.
Reverse a Linked List
Problem Statement
Write a Java program that reverses a singly linked list. The linked list consists of nodes, each containing an integer value and a reference to the next node. The program should reverse the order of the nodes (e.g., 1→2→3 becomes 3→2→1) using an iterative approach and print the reversed list. Test the implementation with linked lists of varying sizes, including empty lists, single-node lists, and multi-node lists. You can visualize this as rearranging a chain of numbered cards, flipping their order so the last card becomes the first.
Input:
- A singly linked list of integers (e.g., 1→2→3→4). Output: The reversed linked list as a string (e.g., "4 3 2 1"). Constraints:
- The list size is between 0 and 10^5.
- Node values are integers in the range [-10^9, 10^9].
- The list may be empty. Example:
- Input: 1→2→3→4
- Output: "4 3 2 1"
- Explanation: The list 1→2→3→4 is reversed to 4→3→2→1.
- Input: []
- Output: "[]"
- Explanation: An empty list remains empty.
- Input: 5
- Output: "5"
- Explanation: A single-node list is unchanged.
Pseudocode
CLASS Node
SET value to integer
SET next to Node (null by default)
ENDCLASS
FUNCTION reverseList(head)
SET prev to null
SET current to head
WHILE current is not null
SET nextNode to current.next
SET current.next to prev
SET prev to current
SET current to nextNode
ENDWHILE
RETURN prev
ENDFUNCTION
FUNCTION toString(head)
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
WHILE current is not null
APPEND current.value to result
IF current.next is not null THEN
APPEND " " to result
ENDIF
SET current to current.next
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of linked lists
FOR each testCase in testCases
PRINT test case details
CALL reverseList(testCase.head)
PRINT reversed list using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Nodeclass with: a. An integervalue. b. Anextpointer to the next node. - In
reverseList: a. Initializeprevas null,currentas head. b. Whilecurrentis not null:- Save
current.nextasnextNode. - Set
current.nexttoprev(reverse the link). - Move
prevtocurrent,currenttonextNode. c. Returnprevas the new head.
- Save
- In
toString: a. If head is null, return "[]". b. Traverse the list, append each value to a StringBuilder, add spaces between values. c. Return the string representation. - In
main, test with empty, single-node, and multi-node lists.
Java Implementation
public class ReverseLinkedList {
// Node class for the linked list
static class Node {
int value;
Node next;
Node(int value) {
this.value = value;
this.next = null;
}
}
// Reverses the linked list
public Node reverseList(Node head) {
Node prev = null;
Node current = head;
while (current != null) {
Node nextNode = current.next;
current.next = prev;
prev = current;
current = nextNode;
}
return prev;
}
// Converts linked list to string for output
public String toString(Node head) {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
Node current = head;
while (current != null) {
result.append(current.value);
if (current.next != null) {
result.append(" ");
}
current = current.next;
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
Node head;
TestCase(int[] values) {
if (values.length == 0) {
head = null;
return;
}
head = new Node(values[0]);
Node current = head;
for (int i = 1; i < values.length; i++) {
current.next = new Node(values[i]);
current = current.next;
}
}
}
// Main method to test linked list reversal
public static void main(String[] args) {
ReverseLinkedList reverser = new ReverseLinkedList();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{1, 2, 3, 4}), // Multi-node list
new TestCase(new int[]{}), // Empty list
new TestCase(new int[]{5}), // Single node
new TestCase(new int[]{1, 1, 1}), // List with duplicates
new TestCase(new int[]{10, 20}) // Two nodes
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
TestCase test = testCases[i];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input list: " + reverser.toString(test.head));
Node reversed = reverser.reverseList(test.head);
System.out.println("Reversed list: " + reverser.toString(reversed) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input list: 1 2 3 4
Reversed list: 4 3 2 1
Test case 2:
Input list: []
Reversed list: []
Test case 3:
Input list: 5
Reversed list: 5
Test case 4:
Input list: 1 1 1
Reversed list: 1 1 1
Test case 5:
Input list: 10 20
Reversed list: 20 10
Explanation:
- Test case 1: Reverses 1→2→3→4 to 4→3→2→1.
- Test case 2: Empty list remains empty.
- Test case 3: Single node 5 is unchanged.
- Test case 4: List with duplicates 1→1→1 reverses to 1→1→1.
- Test case 5: Two nodes 10→20 reverse to 20→10.
How It Works
- Node: Stores an integer value and a reference to the next node.
- reverseList:
- Iteratively reverses links by adjusting
nextpointers. - Uses
prev,current, andnextNodeto track nodes. - Each step reverses one link, moving pointers forward.
- Iteratively reverses links by adjusting
- toString: Converts the list to a space-separated string, returning "[]" for empty lists.
- Example Trace (Test case 1):
- Input: 1→2→3→4.
- Initial: prev=null, current=1, nextNode=2.
- Step 1: 1→null, prev=1, current=2, nextNode=3.
- Step 2: 2→1→null, prev=2, current=3, nextNode=4.
- Step 3: 3→2→1→null, prev=3, current=4, nextNode=null.
- Step 4: 4→3→2→1→null, prev=4, current=null.
- Return: 4→3→2→1.
- Main Method: Tests multi-node, empty, single-node, duplicate, and two-node lists.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Reverse List | O(n) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of nodes in the list.
- Time complexity: O(n) for reversing (single pass) and toString (traverse list).
- Space complexity: O(1) for reversal (constant pointers); O(n) for toString (StringBuilder).
- Worst case: O(n) time, O(n) space for large lists in output.
✅ Tip: Use an iterative approach to reverse a linked list to save space, adjusting pointers in a single pass. Test with empty and single-node lists to ensure edge cases are handled.
⚠ Warning: Be cautious when updating
nextpointers to avoid losing references to the rest of the list. Always save the next node before reversing the link.
Bidirectional Traversal
Problem Statement
Write a Java program that extends a doubly linked list implementation to include methods for printing the list in both forward (head to tail) and backward (tail to head) directions. The doubly linked list consists of nodes, each containing an integer value, a reference to the next node, and a reference to the previous node. The program should provide methods to traverse and print the list using the next pointers for forward direction and the prev pointers for backward direction. Test the implementation with sample doubly linked lists of varying sizes, including empty lists, single-node lists, and multi-node lists. You can visualize this as displaying a chain of numbered cards in order from the first to the last card, and then from the last to the first card, using the bidirectional links.
Input:
- A doubly linked list of integers (e.g., 1↔2↔3↔4). Output: Two strings representing the list in forward order (e.g., "1 2 3 4") and backward order (e.g., "4 3 2 1"). Constraints:
- The list size is between 0 and 10^5.
- Node values are integers in the range [-10^9, 10^9].
- The list may be empty. Example:
- Input: 1↔2↔3↔4
- Output:
Forward: 1 2 3 4 Backward: 4 3 2 1 - Input: []
- Output:
Forward: [] Backward: [] - Input: 5
- Output:
Forward: 5 Backward: 5
Pseudocode
CLASS DoublyNode
SET value to integer
SET next to DoublyNode (null by default)
SET prev to DoublyNode (null by default)
ENDCLASS
FUNCTION printForward(head)
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
WHILE current is not null
APPEND current.value to result
IF current.next is not null THEN
APPEND " " to result
ENDIF
SET current to current.next
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION printBackward(head)
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
WHILE current.next is not null
SET current to current.next
ENDWHILE
WHILE current is not null
APPEND current.value to result
IF current.prev is not null THEN
APPEND " " to result
ENDIF
SET current to current.prev
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of doubly linked lists
FOR each testCase in testCases
PRINT test case details
CALL printForward(testCase.head)
CALL printBackward(testCase.head)
PRINT forward and backward results
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
DoublyNodeclass with: a. An integervalue. b. Anextpointer to the next node. c. Aprevpointer to the previous node. - In
printForward: a. If head is null, return "[]". b. Traverse the list usingnextpointers, appending each value to a StringBuilder with spaces. c. Return the string representation. - In
printBackward: a. If head is null, return "[]". b. Traverse to the tail usingnextpointers. c. Traverse back usingprevpointers, appending each value with spaces. d. Return the string representation. - In
main, test with empty, single-node, and multi-node doubly linked lists, printing both forward and backward.
Java Implementation
public class BidirectionalTraversal {
// Node class for the doubly linked list
static class DoublyNode {
int value;
DoublyNode next;
DoublyNode prev;
DoublyNode(int value) {
this.value = value;
this.next = null;
this.prev = null;
}
}
// Prints the list in forward direction (head to tail)
public String printForward(DoublyNode head) {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
DoublyNode current = head;
while (current != null) {
result.append(current.value);
if (current.next != null) {
result.append(" ");
}
current = current.next;
}
return result.toString();
}
// Prints the list in backward direction (tail to head)
public String printBackward(DoublyNode head) {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
DoublyNode current = head;
// Move to tail
while (current.next != null) {
current = current.next;
}
// Traverse backward
while (current != null) {
result.append(current.value);
if (current.prev != null) {
result.append(" ");
}
current = current.prev;
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
DoublyNode head;
TestCase(int[] values) {
if (values.length == 0) {
head = null;
return;
}
head = new DoublyNode(values[0]);
DoublyNode current = head;
for (int i = 1; i < values.length; i++) {
DoublyNode newNode = new DoublyNode(values[i]);
newNode.prev = current;
current.next = newNode;
current = newNode;
}
}
}
// Main method to test bidirectional traversal
public static void main(String[] args) {
BidirectionalTraversal traverser = new BidirectionalTraversal();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{1, 2, 3, 4}), // Multi-node list
new TestCase(new int[]{}), // Empty list
new TestCase(new int[]{5}), // Single node
new TestCase(new int[]{1, 1, 1}), // List with duplicates
new TestCase(new int[]{10, 20}) // Two nodes
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
TestCase test = testCases[i];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Forward: " + traverser.printForward(test.head));
System.out.println("Backward: " + traverser.printBackward(test.head) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Forward: 1 2 3 4
Backward: 4 3 2 1
Test case 2:
Forward: []
Backward: []
Test case 3:
Forward: 5
Backward: 5
Test case 4:
Forward: 1 1 1
Backward: 1 1 1
Test case 5:
Forward: 10 20
Backward: 20 10
Explanation:
- Test case 1: Forward prints 1→2→3→4, backward prints 4→3→2→1.
- Test case 2: Empty list prints "[]" for both directions.
- Test case 3: Single node 5 prints "5" for both directions.
- Test case 4: List with duplicates 1→1→1 prints "1 1 1" for both directions.
- Test case 5: Two nodes 10→20 print "10 20" forward, "20 10" backward.
How It Works
- DoublyNode: Stores an integer value, a
nextpointer, and aprevpointer. - printForward:
- Traverses from head to tail using
nextpointers. - Builds a space-separated string, returning "[]" for empty lists.
- Traverses from head to tail using
- printBackward:
- Traverses to tail using
nextpointers. - Traverses back to head using
prevpointers, building a space-separated string.
- Traverses to tail using
- Example Trace (Test case 1):
- Input: 1↔2↔3↔4.
- Forward: Traverse head=1, append 1,2,3,4 → "1 2 3 4".
- Backward: Move to tail=4, append 4,3,2,1 → "4 3 2 1".
- Main Method: Tests multi-node, empty, single-node, duplicate, and two-node lists.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Print Forward | O(n) | O(n) |
| Print Backward | O(n) | O(n) |
Note:
- n is the number of nodes in the list.
- Time complexity: O(n) for printForward (single pass); O(n) for printBackward (two passes: to tail and back).
- Space complexity: O(n) for both (StringBuilder for output).
- Worst case: O(n) time, O(n) space for output with large lists.
✅ Tip: Use the
nextandprevpointers in a doubly linked list to enable efficient bidirectional traversal. Test both directions to ensureprevpointers are correctly set.
⚠ Warning: Ensure null checks for
nextandprevpointers to avoid null pointer exceptions, especially for empty or single-node lists.
Browser History Simulator
Problem Statement
Write a Java program that simulates a browser’s navigation history using a doubly linked list. Each node in the list represents a webpage with a string URL. The program should support adding new pages (insert at tail), navigating back (delete the current tail and move to the previous page), and navigating forward (re-insert a previously deleted page). A current pointer tracks the current page, and forward navigation is only possible if pages were previously removed via back navigation. Test the implementation with sequences of operations, including adding pages, navigating back and forward, and handling edge cases like empty history or navigating beyond available pages. You can visualize this as managing a browser’s history where new pages are added to the end, going back removes the current page, and going forward restores a previously visited page.
Input:
- A sequence of operations, where each operation is:
visit(url): Add a new page (URL) at the tail, clear forward history.back(): Move to the previous page, remove current page, return URL or "null" if not possible.forward(): Move to the next page (re-insert removed page), return URL or "null" if not possible.printHistory(): Print the current history from head to current page. Output: For each operation, print the action performed (e.g., "Visited URL", "Navigated back to URL", "Navigated forward to URL", or the current history). Return "null" for invalid back/forward navigations. Constraints:
- The history size is between 0 and 10^5.
- URLs are non-empty strings of length up to 100 characters.
- The history may be empty or have no forward/backward pages. Example:
- Input: Operations = [visit("page1"), visit("page2"), visit("page3"), printHistory, back, printHistory, forward, printHistory]
- Output:
Visited page1 Visited page2 Visited page3 History: page1 page2 page3 Navigated back to page2 History: page1 page2 Navigated forward to page3 History: page1 page2 page3 - Input: Operations = [back, printHistory]
- Output:
Navigated back to null History: []
Pseudocode
CLASS DoublyNode
SET url to string
SET next to DoublyNode (null by default)
SET prev to DoublyNode (null by default)
ENDCLASS
CLASS BrowserHistory
SET head to null
SET current to null
FUNCTION visit(url)
CREATE newNode as new DoublyNode(url)
IF head is null THEN
SET head to newNode
SET current to newNode
ELSE
SET current.next to newNode
SET newNode.prev to current
SET current to newNode
ENDIF
ENDFUNCTION
FUNCTION back()
IF current is null OR current.prev is null THEN
RETURN "null"
ENDIF
SET url to current.url
SET current to current.prev
SET current.next to null
RETURN url
ENDFUNCTION
FUNCTION forward(forwardList)
IF forwardList is empty THEN
RETURN "null"
ENDIF
SET url to forwardList.removeLast()
CREATE newNode as new DoublyNode(url)
SET newNode.prev to current
IF current is not null THEN
SET current.next to newNode
ELSE
SET head to newNode
ENDIF
SET current to newNode
RETURN url
ENDFUNCTION
FUNCTION toString()
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET temp to head
WHILE temp is not null AND temp is not current.next
APPEND temp.url to result
IF temp.next is not null AND temp.next is not current.next THEN
APPEND " " to result
ENDIF
SET temp to temp.next
ENDWHILE
RETURN result as string
ENDFUNCTION
ENDCLASS
FUNCTION testBrowserHistory(operations)
CREATE browser as new BrowserHistory
CREATE forwardList as new list
FOR each operation in operations
IF operation.type equals "visit" THEN
CLEAR forwardList
CALL browser.visit(operation.url)
PRINT visited url
ELSE IF operation.type equals "back" THEN
SET url to browser.back()
IF url is not "null" THEN
ADD url to forwardList
ENDIF
PRINT navigated back to url
ELSE IF operation.type equals "forward" THEN
SET url to browser.forward(forwardList)
PRINT navigated forward to url
ELSE IF operation.type equals "print" THEN
PRINT history using browser.toString
ENDIF
ENDFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of operation sequences
FOR each testCase in testCases
PRINT test case details
CALL testBrowserHistory(testCase)
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
DoublyNodeclass with: a. A stringurlfor the webpage. b. Anextpointer to the next node. c. Aprevpointer to the previous node. - Define a
BrowserHistoryclass with: a.headandcurrentpointers to track the list and current page. b.visit: Add new node at tail, clear forward history, updatecurrent. c.back: Movecurrentto previous node, remove current tail, store URL for forward. d.forward: Re-insert last removed URL from forward list, updatecurrent. e.toString: Print history from head to current page. - In
testBrowserHistory: a. Create aBrowserHistoryand aforwardListto store back-navigated URLs. b. For each operation:visit: Clear forward list, callvisit, print action.back: Callback, store URL in forward list, print URL.forward: Callforwardwith forward list, print URL.print: CalltoString, print history.
- In
main, test with sequences including visits, back/forward navigations, and edge cases.
Java Implementation
import java.util.*;
public class BrowserHistorySimulator {
// Node class for the doubly linked list
static class DoublyNode {
String url;
DoublyNode next;
DoublyNode prev;
DoublyNode(String url) {
this.url = url;
this.next = null;
this.prev = null;
}
}
// BrowserHistory class to simulate navigation
static class BrowserHistory {
private DoublyNode head;
private DoublyNode current;
public void visit(String url) {
DoublyNode newNode = new DoublyNode(url);
if (head == null) {
head = newNode;
current = newNode;
} else {
current.next = newNode;
newNode.prev = current;
current = newNode;
}
}
public String back() {
if (current == null || current.prev == null) {
return "null";
}
String url = current.url;
current = current.prev;
current.next = null;
return url;
}
public String forward(List<String> forwardList) {
if (forwardList.isEmpty()) {
return "null";
}
String url = forwardList.remove(forwardList.size() - 1);
DoublyNode newNode = new DoublyNode(url);
newNode.prev = current;
if (current != null) {
current.next = newNode;
} else {
head = newNode;
}
current = newNode;
return url;
}
public String toString() {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
DoublyNode temp = head;
while (temp != null && temp != current.next) {
result.append(temp.url);
if (temp.next != null && temp.next != current.next) {
result.append(" ");
}
temp = temp.next;
}
return result.toString();
}
}
// Helper class for operations
static class Operation {
String type;
String url;
Operation(String type, String url) {
this.type = type;
this.url = url;
}
}
// Tests browser history operations
public void testBrowserHistory(List<Operation> operations) {
BrowserHistory browser = new BrowserHistory();
List<String> forwardList = new ArrayList<>();
for (Operation op : operations) {
if (op.type.equals("visit")) {
forwardList.clear();
browser.visit(op.url);
System.out.println("Visited " + op.url);
} else if (op.type.equals("back")) {
String url = browser.back();
if (!url.equals("null")) {
forwardList.add(url);
}
System.out.println("Navigated back to " + url);
} else if (op.type.equals("forward")) {
String url = browser.forward(forwardList);
System.out.println("Navigated forward to " + url);
} else if (op.type.equals("print")) {
System.out.println("History: " + browser.toString());
}
}
}
// Main method to test browser history simulator
public static void main(String[] args) {
BrowserHistorySimulator simulator = new BrowserHistorySimulator();
// Test cases
List<List<Operation>> testCases = new ArrayList<>();
// Test case 1: Normal navigation
testCases.add(Arrays.asList(
new Operation("visit", "page1"),
new Operation("visit", "page2"),
new Operation("visit", "page3"),
new Operation("print", null),
new Operation("back", null),
new Operation("print", null),
new Operation("forward", null),
new Operation("print", null)
));
// Test case 2: Empty history
testCases.add(Arrays.asList(
new Operation("back", null),
new Operation("forward", null),
new Operation("print", null)
));
// Test case 3: Single page
testCases.add(Arrays.asList(
new Operation("visit", "page1"),
new Operation("print", null),
new Operation("back", null),
new Operation("print", null),
new Operation("forward", null),
new Operation("print", null)
));
// Test case 4: Multiple back/forward
testCases.add(Arrays.asList(
new Operation("visit", "page1"),
new Operation("visit", "page2"),
new Operation("visit", "page3"),
new Operation("back", null),
new Operation("back", null),
new Operation("forward", null),
new Operation("forward", null),
new Operation("print", null)
));
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
System.out.println("Test case " + (i + 1) + ":");
simulator.testBrowserHistory(testCases.get(i));
System.out.println();
}
}
}
Output
Running the main method produces:
Test case 1:
Visited page1
Visited page2
Visited page3
History: page1 page2 page3
Navigated back to page3
History: page1 page2
Navigated forward to page3
History: page1 page2 page3
Test case 2:
Navigated back to null
Navigated forward to null
History: []
Test case 3:
Visited page1
History: page1
Navigated back to null
History: []
Navigated forward to page1
History: page1
Test case 4:
Visited page1
Visited page2
Visited page3
Navigated back to page3
Navigated back to page2
Navigated forward to page3
Navigated forward to page2
History: page1 page2
Explanation:
- Test case 1: Visits page1, page2, page3, prints "page1 page2 page3", goes back (removes page3), prints "page1 page2", goes forward (restores page3), prints "page1 page2 page3".
- Test case 2: Attempts back/forward on empty history, prints "null" and "[]".
- Test case 3: Visits page1, prints "page1", goes back (removes page1), prints "[]", goes forward (restores page1), prints "page1".
- Test case 4: Visits page1, page2, page3, goes back twice (removes page3, page2), goes forward twice (restores page3, page2), prints "page1 page2".
How It Works
- DoublyNode: Stores a string URL, a
nextpointer, and aprevpointer. - BrowserHistory:
visit: Adds new node at tail, clears forward history, updatescurrent.back: Movescurrentto previous node, removes tail, stores URL for forward.forward: Re-inserts last removed URL from forward list, updatescurrent.toString: Prints history from head tocurrent, returns "[]" if empty.
- testBrowserHistory: Manages operations, uses
forwardListto store back-navigated URLs. - Example Trace (Test case 1):
- visit("page1"): head=page1, current=page1.
- visit("page2"): head=page1↔page2, current=page2.
- visit("page3"): head=page1↔page2↔page3, current=page3.
- print: "page1 page2 page3".
- back: current=page2, forwardList=[page3], returns "page3".
- print: "page1 page2".
- forward: Re-inserts page3, current=page3, returns "page3".
- print: "page1 page2 page3".
- Main Method: Tests normal navigation, empty history, single page, and multiple back/forward operations.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Visit | O(1) | O(1) |
| Back | O(1) | O(1) |
| Forward | O(1) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of nodes in the history.
- Time complexity: O(1) for visit, back, forward (constant-time operations); O(n) for toString (traverse to current).
- Space complexity: O(1) for visit, back, forward (constant pointers); O(n) for toString (StringBuilder) and forwardList storage.
- Worst case: O(n) time, O(n) space for output or forward history with large lists.
✅ Tip: Use a doubly linked list with a
currentpointer to efficiently manage browser history. Clear the forward history on new visits to simulate browser behavior accurately.
⚠ Warning: Maintain the
currentpointer and forward list correctly to avoid losing navigation history. Handle edge cases like empty history or no forward pages.
Deque Implementation
Problem Statement
Write a Java program that implements a double-ended queue (deque) using a doubly linked list. The doubly linked list consists of nodes, each containing an integer value, a reference to the next node, and a reference to the previous node. The deque should support methods to add elements at the front (head) and back (tail), and remove elements from the front and back, maintaining the list’s bidirectional structure. Test the implementation with a sequence of operations, including adding and removing elements at both ends, and handling edge cases like empty deques and single-element deques. You can visualize this as managing a queue of numbered cards where you can add or remove cards from either the front or the back of the deck.
Input:
- A sequence of operations, where each operation is:
addFront(value): Add a value to the front of the deque.addBack(value): Add a value to the back of the deque.removeFront(): Remove and return the front value, or -1 if empty.removeBack(): Remove and return the back value, or -1 if empty.printDeque(): Print the deque in forward order. Output: For each operation, print the action performed (e.g., "Added 5 at front", "Removed 3 from back", or the deque as a string). Return -1 for remove operations on an empty deque. Constraints:
- The deque size is between 0 and 10^5.
- Values are integers in the range [-10^9, 10^9].
- The deque may be empty. Example:
- Input: Operations = [addFront(1), addBack(2), addFront(3), printDeque, removeFront, printDeque, removeBack, printDeque]
- Output:
Added 1 at front Added 2 at back Added 3 at front Deque: 3 1 2 Removed 3 from front Deque: 1 2 Removed 2 from back Deque: 1 - Input: Operations = [removeFront, printDeque]
- Output:
Removed -1 from front Deque: []
Pseudocode
CLASS DoublyNode
SET value to integer
SET next to DoublyNode (null by default)
SET prev to DoublyNode (null by default)
ENDCLASS
CLASS Deque
SET head to null
SET tail to null
FUNCTION addFront(value)
CREATE newNode as new DoublyNode(value)
IF head is null THEN
SET head to newNode
SET tail to newNode
ELSE
SET newNode.next to head
SET head.prev to newNode
SET head to newNode
ENDIF
ENDFUNCTION
FUNCTION addBack(value)
CREATE newNode as new DoublyNode(value)
IF tail is null THEN
SET head to newNode
SET tail to newNode
ELSE
SET newNode.prev to tail
SET tail.next to newNode
SET tail to newNode
ENDIF
ENDFUNCTION
FUNCTION removeFront()
IF head is null THEN
RETURN -1
ENDIF
SET value to head.value
SET head to head.next
IF head is null THEN
SET tail to null
ELSE
SET head.prev to null
ENDIF
RETURN value
ENDFUNCTION
FUNCTION removeBack()
IF tail is null THEN
RETURN -1
ENDIF
SET value to tail.value
SET tail to tail.prev
IF tail is null THEN
SET head to null
ELSE
SET tail.next to null
ENDIF
RETURN value
ENDFUNCTION
FUNCTION toString()
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
WHILE current is not null
APPEND current.value to result
IF current.next is not null THEN
APPEND " " to result
ENDIF
SET current to current.next
ENDWHILE
RETURN result as string
ENDFUNCTION
ENDCLASS
FUNCTION testDeque(operations)
CREATE deque as new Deque
FOR each operation in operations
IF operation.type equals "addFront" THEN
CALL deque.addFront(operation.value)
PRINT added value at front
ELSE IF operation.type equals "addBack" THEN
CALL deque.addBack(operation.value)
PRINT added value at back
ELSE IF operation.type equals "removeFront" THEN
SET value to deque.removeFront()
PRINT removed value from front
ELSE IF operation.type equals "removeBack" THEN
SET value to deque.removeBack()
PRINT removed value from back
ELSE IF operation.type equals "print" THEN
PRINT deque using toString
ENDIF
ENDFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of operation sequences
FOR each testCase in testCases
PRINT test case details
CALL testDeque(testCase)
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
DoublyNodeclass with: a. An integervalue. b. Anextpointer to the next node. c. Aprevpointer to the previous node. - Define a
Dequeclass with: a.headandtailpointers to track both ends. b.addFront: Add node at head, updateprevandtailif needed. c.addBack: Add node at tail, updatenextandheadif needed. d.removeFront: Remove head, updateheadandtail, return value or -1. e.removeBack: Remove tail, updatetailandhead, return value or -1. f.toString: Print list forward, return "[]" if empty. - In
testDeque: a. Create aDeque. b. For each operation:addFront: CalladdFront, print action.addBack: CalladdBack, print action.removeFront: CallremoveFront, print value.removeBack: CallremoveBack, print value.print: CalltoString, print deque.
- In
main, test with sequences including adds/removes at both ends and edge cases.
Java Implementation
import java.util.*;
public class DequeImplementation {
// Node class for the doubly linked list
static class DoublyNode {
int value;
DoublyNode next;
DoublyNode prev;
DoublyNode(int value) {
this.value = value;
this.next = null;
this.prev = null;
}
}
// Deque class using doubly linked list
static class Deque {
private DoublyNode head;
private DoublyNode tail;
public void addFront(int value) {
DoublyNode newNode = new DoublyNode(value);
if (head == null) {
head = newNode;
tail = newNode;
} else {
newNode.next = head;
head.prev = newNode;
head = newNode;
}
}
public void addBack(int value) {
DoublyNode newNode = new DoublyNode(value);
if (tail == null) {
head = newNode;
tail = newNode;
} else {
newNode.prev = tail;
tail.next = newNode;
tail = newNode;
}
}
public int removeFront() {
if (head == null) {
return -1;
}
int value = head.value;
head = head.next;
if (head == null) {
tail = null;
} else {
head.prev = null;
}
return value;
}
public int removeBack() {
if (tail == null) {
return -1;
}
int value = tail.value;
tail = tail.prev;
if (tail == null) {
head = null;
} else {
tail.next = null;
}
return value;
}
public String toString() {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
DoublyNode current = head;
while (current != null) {
result.append(current.value);
if (current.next != null) {
result.append(" ");
}
current = current.next;
}
return result.toString();
}
}
// Helper class for operations
static class Operation {
String type;
Integer value;
Operation(String type, Integer value) {
this.type = type;
this.value = value;
}
}
// Tests deque operations
public void testDeque(List<Operation> operations) {
Deque deque = new Deque();
for (Operation op : operations) {
if (op.type.equals("addFront")) {
deque.addFront(op.value);
System.out.println("Added " + op.value + " at front");
} else if (op.type.equals("addBack")) {
deque.addBack(op.value);
System.out.println("Added " + op.value + " at back");
} else if (op.type.equals("removeFront")) {
int value = deque.removeFront();
System.out.println("Removed " + value + " from front");
} else if (op.type.equals("removeBack")) {
int value = deque.removeBack();
System.out.println("Removed " + value + " from back");
} else if (op.type.equals("print")) {
System.out.println("Deque: " + deque.toString());
}
}
}
// Main method to test deque implementation
public static void main(String[] args) {
DequeImplementation manager = new DequeImplementation();
// Test cases
List<List<Operation>> testCases = new ArrayList<>();
// Test case 1: Normal operations
testCases.add(Arrays.asList(
new Operation("addFront", 1),
new Operation("addBack", 2),
new Operation("addFront", 3),
new Operation("print", null),
new Operation("removeFront", null),
new Operation("print", null),
new Operation("removeBack", null),
new Operation("print", null)
));
// Test case 2: Empty deque
testCases.add(Arrays.asList(
new Operation("removeFront", null),
new Operation("removeBack", null),
new Operation("print", null)
));
// Test case 3: Single element
testCases.add(Arrays.asList(
new Operation("addFront", 5),
new Operation("print", null),
new Operation("removeFront", null),
new Operation("print", null)
));
// Test case 4: Mixed operations with duplicates
testCases.add(Arrays.asList(
new Operation("addFront", 1),
new Operation("addBack", 1),
new Operation("addFront", 2),
new Operation("print", null),
new Operation("removeFront", null),
new Operation("removeBack", null),
new Operation("print", null)
));
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
System.out.println("Test case " + (i + 1) + ":");
manager.testDeque(testCases.get(i));
System.out.println();
}
}
}
Output
Running the main method produces:
Test case 1:
Added 1 at front
Added 2 at back
Added 3 at front
Deque: 3 1 2
Removed 3 from front
Deque: 1 2
Removed 2 from back
Deque: 1
Test case 2:
Removed -1 from front
Removed -1 from back
Deque: []
Test case 3:
Added 5 at front
Deque: 5
Removed 5 from front
Deque: []
Test case 4:
Added 1 at front
Added 1 at back
Added 2 at front
Deque: 2 1 1
Removed 2 from front
Removed 1 from back
Deque: 1
Explanation:
- Test case 1: Adds 1 (front), 2 (back), 3 (front), prints "3 1 2", removes 3 (front), prints "1 2", removes 2 (back), prints "1".
- Test case 2: Removes from empty deque (returns -1), prints "[]".
- Test case 3: Adds 5 (front), prints "5", removes 5 (front), prints "[]".
- Test case 4: Adds 1 (front), 1 (back), 2 (front), prints "2 1 1", removes 2 (front), 1 (back), prints "1".
How It Works
- DoublyNode: Stores an integer value, a
nextpointer, and aprevpointer. - Deque:
addFront: Adds node at head, updatesprevandtail, O(1).addBack: Adds node at tail, updatesnextandhead, O(1).removeFront: Removes head, updatesheadandtail, returns value or -1, O(1).removeBack: Removes tail, updatestailandhead, returns value or -1, O(1).toString: Traverses forward, returns space-separated string or "[]".
- testDeque: Executes operations, printing actions and results.
- Example Trace (Test case 1):
- addFront(1): head=1↔null, tail=1.
- addBack(2): head=1↔2, tail=2.
- addFront(3): head=3↔1↔2, tail=2.
- print: "3 1 2".
- removeFront: Removes 3, head=1↔2, tail=2, returns 3.
- print: "1 2".
- removeBack: Removes 2, head=1, tail=1, returns 2.
- print: "1".
- Main Method: Tests normal operations, empty deque, single element, and duplicates.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Add Front | O(1) | O(1) |
| Add Back | O(1) | O(1) |
| Remove Front | O(1) | O(1) |
| Remove Back | O(1) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of nodes in the deque.
- Time complexity: O(1) for addFront, addBack, removeFront, removeBack (constant-time operations); O(n) for toString (traverse list).
- Space complexity: O(1) for add/remove operations (constant pointers); O(n) for toString (StringBuilder).
- Worst case: O(n) time, O(n) space for output with large deques.
✅ Tip: Use a doubly linked list for a deque to enable O(1) operations at both ends by maintaining
headandtailpointers. Test edge cases like empty deques and single-element deques to ensure robustness.
⚠ Warning: Always update both
headandtailpointers correctly during add and remove operations to maintain deque integrity. Check for null pointers to handle empty deque cases.
Insert After Value
Problem Statement
Write a Java program that extends a doubly linked list implementation to include a method that inserts a new node with a given value after the first occurrence of a specified target value. The doubly linked list consists of nodes, each containing an integer value, a reference to the next node, and a reference to the previous node. The program should insert the new node by updating the next and prev pointers appropriately and return true if the insertion is successful (target value found) or false if the target value is not found. Test the implementation with cases where the target value exists in the list and where it does not, including empty lists and single-node lists. You can visualize this as adding a new card to a chain of numbered cards, placing it right after the first card with a specific number.
Input:
- A doubly linked list of integers (e.g., 1↔2↔3), a target value (e.g., 2), and a new value to insert (e.g., 4). Output: A boolean indicating whether the insertion was successful, and the updated list printed in forward order (e.g., "1 2 4 3"). Constraints:
- The list size is between 0 and 10^5.
- Node values and the new value are integers in the range [-10^9, 10^9].
- The list may be empty or not contain the target value. Example:
- Input: List = 1↔2↔3, target = 2, newValue = 4
- Output: true, "1 2 4 3"
- Explanation: Inserts 4 after the first 2, resulting in 1↔2↔4↔3.
- Input: List = 1↔2↔3, target = 5, newValue = 4
- Output: false, "1 2 3"
- Explanation: Target 5 not found, list unchanged.
- Input: List = [], target = 1, newValue = 2
- Output: false, "[]"
- Explanation: Empty list, no insertion possible.
Pseudocode
CLASS DoublyNode
SET value to integer
SET next to DoublyNode (null by default)
SET prev to DoublyNode (null by default)
ENDCLASS
FUNCTION insertAfterValue(head, target, newValue)
IF head is null THEN
RETURN false
ENDIF
SET current to head
WHILE current is not null
IF current.value equals target THEN
CREATE newNode as new DoublyNode(newValue)
SET newNode.next to current.next
SET newNode.prev to current
IF current.next is not null THEN
SET current.next.prev to newNode
ENDIF
SET current.next to newNode
RETURN true
ENDIF
SET current to current.next
ENDWHILE
RETURN false
ENDFUNCTION
FUNCTION printForward(head)
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
WHILE current is not null
APPEND current.value to result
IF current.next is not null THEN
APPEND " " to result
ENDIF
SET current to current.next
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of test cases (list, target, newValue)
FOR each testCase in testCases
PRINT test case details
CALL insertAfterValue(testCase.head, testCase.target, testCase.newValue)
PRINT insertion result and updated list using printForward
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
DoublyNodeclass with: a. An integervalue. b. Anextpointer to the next node. c. Aprevpointer to the previous node. - In
insertAfterValue: a. If head is null, return false (empty list). b. Traverse the list usingcurrentuntil the target value is found or the list ends. c. If target is found:- Create a new node with
newValue. - Set
newNode.nexttocurrent.nextandnewNode.prevtocurrent. - Update
current.next.prevtonewNodeifcurrent.nextexists. - Set
current.nexttonewNode. - Return true. d. If target is not found, return false.
- Create a new node with
- In
printForward: a. If head is null, return "[]". b. Traverse the list usingnextpointers, appending each value with spaces. c. Return the string representation. - In
main, test with cases where the target exists, does not exist, and edge cases like empty or single-node lists.
Java Implementation
public class InsertAfterValue {
// Node class for the doubly linked list
static class DoublyNode {
int value;
DoublyNode next;
DoublyNode prev;
DoublyNode(int value) {
this.value = value;
this.next = null;
this.prev = null;
}
}
// Inserts a new node after the first occurrence of target
public boolean insertAfterValue(DoublyNode head, int target, int newValue) {
if (head == null) {
return false;
}
DoublyNode current = head;
while (current != null) {
if (current.value == target) {
DoublyNode newNode = new DoublyNode(newValue);
newNode.next = current.next;
newNode.prev = current;
if (current.next != null) {
current.next.prev = newNode;
}
current.next = newNode;
return true;
}
current = current.next;
}
return false;
}
// Prints the list in forward direction
public String printForward(DoublyNode head) {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
DoublyNode current = head;
while (current != null) {
result.append(current.value);
if (current.next != null) {
result.append(" ");
}
current = current.next;
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
DoublyNode head;
int target;
int newValue;
TestCase(int[] values, int target, int newValue) {
this.target = target;
this.newValue = newValue;
if (values.length == 0) {
head = null;
return;
}
head = new DoublyNode(values[0]);
DoublyNode current = head;
for (int i = 1; i < values.length; i++) {
DoublyNode newNode = new DoublyNode(values[i]);
newNode.prev = current;
current.next = newNode;
current = newNode;
}
}
}
// Main method to test insert after value
public static void main(String[] args) {
InsertAfterValue inserter = new InsertAfterValue();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{1, 2, 3}, 2, 4), // Target exists
new TestCase(new int[]{1, 2, 3}, 5, 4), // Target does not exist
new TestCase(new int[]{}, 1, 2), // Empty list
new TestCase(new int[]{5}, 5, 6), // Single node, target exists
new TestCase(new int[]{1, 1, 1}, 1, 2) // Duplicates, insert after first
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
TestCase test = testCases[i];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input list: " + inserter.printForward(test.head));
System.out.println("Insert " + test.newValue + " after " + test.target);
boolean result = inserter.insertAfterValue(test.head, test.target, test.newValue);
System.out.println("Insertion successful: " + result);
System.out.println("Updated list: " + inserter.printForward(test.head) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input list: 1 2 3
Insert 4 after 2
Insertion successful: true
Updated list: 1 2 4 3
Test case 2:
Input list: 1 2 3
Insert 4 after 5
Insertion successful: false
Updated list: 1 2 3
Test case 3:
Input list: []
Insert 2 after 1
Insertion successful: false
Updated list: []
Test case 4:
Input list: 5
Insert 6 after 5
Insertion successful: true
Updated list: 5 6
Test case 5:
Input list: 1 1 1
Insert 2 after 1
Insertion successful: true
Updated list: 1 2 1 1
Explanation:
- Test case 1: Inserts 4 after first 2, resulting in 1↔2↔4↔3.
- Test case 2: Target 5 not found, list unchanged, returns false.
- Test case 3: Empty list, no insertion, returns false.
- Test case 4: Inserts 6 after 5 in single-node list, resulting in 5↔6.
- Test case 5: Inserts 2 after first 1 in 1↔1↔1, resulting in 1↔2↔1↔1.
How It Works
- DoublyNode: Stores an integer value, a
nextpointer, and aprevpointer. - insertAfterValue:
- Returns false for empty lists.
- Traverses list to find first node with target value.
- If found, inserts new node, updating
nextandprevpointers, returns true. - If not found, returns false.
- printForward: Traverses list using
nextpointers, returns space-separated string or "[]". - Example Trace (Test case 1):
- Input: 1↔2↔3, target=2, newValue=4.
- current=1, no match, move to 2.
- current=2, match: create newNode(4), newNode.next=3, newNode.prev=2, 3.prev=newNode, 2.next=newNode.
- Result: 1↔2↔4↔3, return true.
- Main Method: Tests target exists, target does not exist, empty list, single node, and duplicates.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Insert After Value | O(n) | O(1) |
| Print Forward | O(n) | O(n) |
Note:
- n is the number of nodes in the list.
- Time complexity: O(n) for insertAfterValue (traverse to find target); O(n) for printForward (traverse list).
- Space complexity: O(1) for insertAfterValue (constant pointers); O(n) for printForward (StringBuilder).
- Worst case: O(n) time, O(n) space for output with large lists.
✅ Tip: Use the doubly linked list’s
prevpointers to easily link the new node to the next node’s previous pointer, ensuring bidirectional consistency. Test with duplicates to verify insertion after the first occurrence.
⚠ Warning: Update both
nextandprevpointers carefully to maintain the doubly linked list structure. Check for null pointers to handle edge cases like empty lists or inserting at the end.
Reverse a Doubly Linked List
Problem Statement
Write a Java program that reverses a doubly linked list. The doubly linked list consists of nodes, each containing an integer value, a reference to the next node, and a reference to the previous node. The program should reverse the order of the nodes (e.g., 1↔2↔3 becomes 3↔2↔1) by swapping the next and prev pointers of each node and updating the head. Test the implementation with doubly linked lists of varying sizes, including empty lists, single-node lists, and multi-node lists. You can visualize this as rearranging a chain of numbered cards, flipping their order so the last card becomes the first, with each card linked to both its predecessor and successor.
Input:
- A doubly linked list of integers (e.g., 1↔2↔3↔4). Output: The reversed doubly linked list as a string in forward order (e.g., "4 3 2 1"). Constraints:
- The list size is between 0 and 10^5.
- Node values are integers in the range [-10^9, 10^9].
- The list may be empty. Example:
- Input: 1↔2↔3↔4
- Output: "4 3 2 1"
- Explanation: The list 1↔2↔3↔4 is reversed to 4↔3↔2↔1.
- Input: []
- Output: "[]"
- Explanation: An empty list remains empty.
- Input: 5
- Output: "5"
- Explanation: A single-node list is unchanged.
Pseudocode
CLASS DoublyNode
SET value to integer
SET next to DoublyNode (null by default)
SET prev to DoublyNode (null by default)
ENDCLASS
FUNCTION reverseList(head)
IF head is null THEN
RETURN null
ENDIF
SET current to head
SET newHead to null
WHILE current is not null
SET temp to current.prev
SET current.prev to current.next
SET current.next to temp
SET newHead to current
SET current to current.prev
ENDWHILE
RETURN newHead
ENDFUNCTION
FUNCTION toString(head)
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
WHILE current is not null
APPEND current.value to result
IF current.next is not null THEN
APPEND " " to result
ENDIF
SET current to current.next
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of doubly linked lists
FOR each testCase in testCases
PRINT test case details
CALL reverseList(testCase.head)
PRINT reversed list using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
DoublyNodeclass with: a. An integervalue. b. Anextpointer to the next node. c. Aprevpointer to the previous node. - In
reverseList: a. If head is null, return null (empty list). b. Initializecurrentto head,newHeadto track the new head. c. Whilecurrentis not null:- Swap
current.prevandcurrent.next. - Update
newHeadtocurrent(last node processed becomes head). - Move
currentto the next node (now inprevdue to swap). d. ReturnnewHead.
- Swap
- In
toString: a. If head is null, return "[]". b. Traverse the list forward, append each value to a StringBuilder with spaces. c. Return the string representation. - In
main, test with empty, single-node, and multi-node doubly linked lists.
Java Implementation
public class ReverseDoublyLinkedList {
// Node class for the doubly linked list
static class DoublyNode {
int value;
DoublyNode next;
DoublyNode prev;
DoublyNode(int value) {
this.value = value;
this.next = null;
this.prev = null;
}
}
// Reverses the doubly linked list
public DoublyNode reverseList(DoublyNode head) {
if (head == null) {
return null;
}
DoublyNode current = head;
DoublyNode newHead = null;
while (current != null) {
// Swap prev and next pointers
DoublyNode temp = current.prev;
current.prev = current.next;
current.next = temp;
// Update newHead to current node (last node processed)
newHead = current;
// Move to next node (now in prev due to swap)
current = current.prev;
}
return newHead;
}
// Converts doubly linked list to string for output
public String toString(DoublyNode head) {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
DoublyNode current = head;
while (current != null) {
result.append(current.value);
if (current.next != null) {
result.append(" ");
}
current = current.next;
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
DoublyNode head;
TestCase(int[] values) {
if (values.length == 0) {
head = null;
return;
}
head = new DoublyNode(values[0]);
DoublyNode current = head;
for (int i = 1; i < values.length; i++) {
DoublyNode newNode = new DoublyNode(values[i]);
newNode.prev = current;
current.next = newNode;
current = newNode;
}
}
}
// Main method to test doubly linked list reversal
public static void main(String[] args) {
ReverseDoublyLinkedList reverser = new ReverseDoublyLinkedList();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{1, 2, 3, 4}), // Multi-node list
new TestCase(new int[]{}), // Empty list
new TestCase(new int[]{5}), // Single node
new TestCase(new int[]{1, 1, 1}), // List with duplicates
new TestCase(new int[]{10, 20}) // Two nodes
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
TestCase test = testCases[i];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input list: " + reverser.toString(test.head));
DoublyNode reversed = reverser.reverseList(test.head);
System.out.println("Reversed list: " + reverser.toString(reversed) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input list: 1 2 3 4
Reversed list: 4 3 2 1
Test case 2:
Input list: []
Reversed list: []
Test case 3:
Input list: 5
Reversed list: 5
Test case 4:
Input list: 1 1 1
Reversed list: 1 1 1
Test case 5:
Input list: 10 20
Reversed list: 20 10
Explanation:
- Test case 1: Reverses 1↔2↔3↔4 to 4↔3↔2↔1.
- Test case 2: Empty list remains empty.
- Test case 3: Single node 5 is unchanged.
- Test case 4: List with duplicates 1↔1↔1 reverses to 1↔1↔1.
- Test case 5: Two nodes 10↔20 reverse to 20↔10.
How It Works
- DoublyNode: Stores an integer value, a
nextpointer, and aprevpointer. - reverseList:
- Returns null for empty lists.
- Iteratively swaps
prevandnextpointers for each node. - Tracks the new head (last node processed).
- Moves to the next node using the swapped
prevpointer.
- toString: Converts the list to a space-separated string, traversing forward, returning "[]" for empty lists.
- Example Trace (Test case 1):
- Input: 1↔2↔3↔4.
- Initial: current=1, newHead=null.
- Step 1: 1(next=null,prev=2), newHead=1, current=2.
- Step 2: 2(next=1,prev=3), newHead=2, current=3.
- Step 3: 3(next=2,prev=4), newHead=3, current=4.
- Step 4: 4(next=3,prev=null), newHead=4, current=null.
- Return: 4↔3↔2↔1.
- Main Method: Tests multi-node, empty, single-node, duplicate, and two-node lists.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Reverse List | O(n) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of nodes in the list.
- Time complexity: O(n) for reverseList (single pass); O(n) for toString (traverse list).
- Space complexity: O(1) for reverseList (constant pointers); O(n) for toString (StringBuilder).
- Worst case: O(n) time, O(n) space for output with large lists.
✅ Tip: In a doubly linked list, reversing is simplified by swapping
nextandprevpointers. Always update the head to the last node processed to maintain the list structure.
⚠ Warning: Carefully swap
nextandprevpointers to avoid losing references. Ensure null checks for edge cases like empty or single-node lists.
Insert After Value in Circular Linked List
Problem Statement
Write a Java program that adds a method to insert a new node with a given value after the first occurrence of a specified target value in a circular linked list. A circular linked list is a singly linked list where the last node’s next pointer points to the head, forming a cycle. The method should insert the new node by updating the next pointers to maintain the circular structure and return true if the insertion is successful (target value found) or false if the target value is not found. Test the implementation with cases where the target value exists in the list and where it does not, including empty lists and single-node lists. You can visualize this as adding a new numbered card to a circular chain of cards, placing it right after the first card with a specific number.
Input:
- A circular linked list of integers (e.g., 1→2→3→1, where 3→1 forms the cycle), a target value (e.g., 2), and a new value to insert (e.g., 4). Output: A boolean indicating whether the insertion was successful, and the updated list printed as a string, listing nodes from the head until just before it cycles (e.g., "1 2 4 3"). Constraints:
- The list size is between 0 and 10^5.
- Node values and the new value are integers in the range [-10^9, 10^9].
- The list may be empty or not contain the target value. Example:
- Input: List = 1→2→3→1, target = 2, newValue = 4
- Output: true, "1 2 4 3"
- Explanation: Inserts 4 after the first 2, resulting in 1→2→4→3→1.
- Input: List = 1→2→3→1, target = 5, newValue = 4
- Output: false, "1 2 3"
- Explanation: Target 5 not found, list unchanged.
- Input: List = [], target = 1, newValue = 2
- Output: false, "[]"
- Explanation: Empty list, no insertion possible.
Pseudocode
CLASS Node
SET value to integer
SET next to Node (null by default)
ENDCLASS
FUNCTION insertAfterValue(head, target, newValue)
IF head is null THEN
RETURN false
ENDIF
SET current to head
SET found to false
REPEAT
IF current.value equals target THEN
SET found to true
BREAK
ENDIF
SET current to current.next
UNTIL current equals head
IF not found THEN
RETURN false
ENDIF
CREATE newNode as new Node(newValue)
SET newNode.next to current.next
SET current.next to newNode
RETURN true
ENDFUNCTION
FUNCTION toString(head)
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
SET visited as new Set
WHILE current is not null AND current not in visited
ADD current to visited
APPEND current.value to result
IF current.next is not head THEN
APPEND " " to result
ENDIF
SET current to current.next
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of test cases (list values, target, newValue)
FOR each testCase in testCases
PRINT test case details
CALL insertAfterValue(testCase.head, testCase.target, testCase.newValue)
PRINT insertion result and updated list using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Nodeclass with: a. An integervalue. b. Anextpointer to the next node. - In
insertAfterValue: a. If the list is empty, return false. b. Traverse the list starting from head until the target value is found or the traversal returns to head. c. If target is not found, return false. d. If target is found:- Create a new node with
newValue. - Set
newNode.nexttocurrent.next. - Set
current.nexttonewNode. - Return true.
- Create a new node with
- In
toString: a. If head is null, return "[]". b. Traverse from head, use a Set to avoid cycling, append values with spaces. c. Return the string representation. - In
main, test with cases where the target exists, does not exist, and edge cases like empty or single-node lists.
Java Implementation
import java.util.*;
public class InsertAfterValueCircular {
// Node class for the circular linked list
static class Node {
int value;
Node next;
Node(int value) {
this.value = value;
this.next = null;
}
}
// Inserts a new node after the first occurrence of target
public boolean insertAfterValue(Node head, int target, int newValue) {
if (head == null) {
return false;
}
Node current = head;
boolean found = false;
do {
if (current.value == target) {
found = true;
break;
}
current = current.next;
} while (current != head);
if (!found) {
return false;
}
Node newNode = new Node(newValue);
newNode.next = current.next;
current.next = newNode;
return true;
}
// Converts circular linked list to string
public String toString(Node head) {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
Node current = head;
Set<Node> visited = new HashSet<>();
while (current != null && !visited.contains(current)) {
visited.add(current);
result.append(current.value);
if (current.next != head) {
result.append(" ");
}
current = current.next;
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
Node head;
int target;
int newValue;
TestCase(int[] values, int target, int newValue) {
this.target = target;
this.newValue = newValue;
if (values.length == 0) {
head = null;
return;
}
head = new Node(values[0]);
Node current = head;
for (int i = 1; i < values.length; i++) {
current.next = new Node(values[i]);
current = current.next;
}
// Make circular
current.next = head;
}
}
// Main method to test insert after value
public static void main(String[] args) {
InsertAfterValueCircular inserter = new InsertAfterValueCircular();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{1, 2, 3}, 2, 4), // Target exists
new TestCase(new int[]{1, 2, 3}, 5, 4), // Target does not exist
new TestCase(new int[]{}, 1, 2), // Empty list
new TestCase(new int[]{5}, 5, 6), // Single node, target exists
new TestCase(new int[]{1, 1, 1}, 1, 2) // Duplicates, insert after first
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
TestCase test = testCases[i];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input list: " + inserter.toString(test.head));
System.out.println("Insert " + test.newValue + " after " + test.target);
boolean result = inserter.insertAfterValue(test.head, test.target, test.newValue);
System.out.println("Insertion successful: " + result);
System.out.println("Updated list: " + inserter.toString(test.head) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input list: 1 2 3
Insert 4 after 2
Insertion successful: true
Updated list: 1 2 4 3
Test case 2:
Input list: 1 2 3
Insert 4 after 5
Insertion successful: false
Updated list: 1 2 3
Test case 3:
Input list: []
Insert 2 after 1
Insertion successful: false
Updated list: []
Test case 4:
Input list: 5
Insert 6 after 5
Insertion successful: true
Updated list: 5 6
Test case 5:
Input list: 1 1 1
Insert 2 after 1
Insertion successful: true
Updated list: 1 2 1 1
Explanation:
- Test case 1: Inserts 4 after first 2, resulting in 1→2→4→3→1.
- Test case 2: Target 5 not found, list unchanged, returns false.
- Test case 3: Empty list, no insertion, returns false.
- Test case 4: Inserts 6 after 5 in single-node list, resulting in 5→6→5.
- Test case 5: Inserts 2 after first 1 in 1→1→1→1, resulting in 1→2→1→1→1.
How It Works
- Node: Stores an integer value and a
nextpointer. - insertAfterValue:
- Returns false for empty lists.
- Traverses the list in a loop, stopping at the first node with the target value or when returning to head.
- If target is not found, returns false.
- If found, inserts new node, updating
nextpointers, returns true.
- toString: Traverses from head, uses a Set to prevent cycling, returns space-separated string or "[]".
- Example Trace (Test case 1):
- Input: 1→2→3→1, target=2, newValue=4.
- current=1, no match, current=2, match.
- newNode(4).next=3, 2.next=newNode.
- Result: 1→2→4→3→1, return true.
- Main Method: Tests target exists, target does not exist, empty list, single node, and duplicates.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Insert After Value | O(n) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of nodes in the list.
- Time complexity: O(n) for insertAfterValue (traverse to find target); O(n) for toString (traverse list).
- Space complexity: O(1) for insertAfterValue (constant pointers); O(n) for toString (StringBuilder and Set).
- Worst case: O(n) time, O(n) space for output with large lists.
✅ Tip: Use a do-while loop to traverse the circular list and stop at the head to avoid infinite loops. Insert the new node by carefully updating
nextpointers to maintain the circular structure.
⚠ Warning: Check for the target value in one full cycle to handle cases where it doesn’t exist. Ensure proper handling of edge cases like empty or single-node lists to avoid null pointer issues.
Josephus Problem
Problem Statement
Write a Java program to solve the Josephus problem using a circular linked list. The Josephus problem involves n people standing in a circle, numbered from 1 to n, where every k-th person is eliminated until only one remains. The circular linked list is a singly linked list where the last node’s next pointer points to the head, forming a cycle. The program should return the value of the last person remaining and test the solution with different values of k and list sizes, including edge cases like single-person lists and k=1. You can visualize this as a circle of numbered cards where every k-th card is removed until only one card remains.
Input:
- A circular linked list of integers representing people (e.g., 1→2→3→4→1 for n=4) and an integer k (the step size for elimination). Output: The value of the last person remaining (e.g., 3 for n=4, k=2). Constraints:
- The list size n is between 1 and 10^5.
- Node values are integers from 1 to n.
- k is a positive integer (k ≥ 1). Example:
- Input: List = 1→2→3→4→1, k = 2
- Output: 3
- Explanation: Eliminate every 2nd person: 2, 4, 1, leaving 3.
- Input: List = 1→1, k = 1
- Output: 1
- Explanation: Single person remains.
- Input: List = 1→2→3→1, k = 3
- Output: 1
- Explanation: Eliminate every 3rd person: 3, 2, leaving 1.
Pseudocode
CLASS Node
SET value to integer
SET next to Node (null by default)
ENDCLASS
FUNCTION solveJosephus(head, k)
IF head is null THEN
RETURN -1
ENDIF
IF head.next is head THEN
RETURN head.value
ENDIF
SET current to head
WHILE current.next is not current
FOR i from 1 to k-1
SET current to current.next
ENDFOR
SET nextNode to current.next
SET current.next to nextNode.next
SET current to current.next
ENDWHILE
RETURN current.value
ENDFUNCTION
FUNCTION toString(head)
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
SET visited as new Set
WHILE current is not null AND current not in visited
ADD current to visited
APPEND current.value to result
IF current.next is not head THEN
APPEND " " to result
ENDIF
SET current to current.next
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of test cases (list values, k)
FOR each testCase in testCases
PRINT test case details
CALL solveJosephus(testCase.head, testCase.k)
PRINT last person remaining
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Nodeclass with: a. An integervalue(person number). b. Anextpointer to the next node. - In
solveJosephus: a. If the list is empty, return -1. b. If the list has one node, return its value. c. Setcurrentto head. d. While more than one node remains:- Traverse k-1 steps to find the node before the one to eliminate.
- Remove the k-th node by updating
current.nextto skip it. - Move
currentto the next node. e. Return the value of the last remaining node.
- In
toString: a. If head is null, return "[]". b. Traverse from head, use a Set to avoid cycling, append values with spaces. c. Return the string representation. - In
main, test with different list sizes (n=1, n>1) and k values (k=1, k>1), including edge cases.
Java Implementation
import java.util.*;
public class JosephusProblem {
// Node class for the circular linked list
static class Node {
int value;
Node next;
Node(int value) {
this.value = value;
this.next = null;
}
}
// Solves the Josephus problem
public int solveJosephus(Node head, int k) {
if (head == null) {
return -1;
}
if (head.next == head) {
return head.value;
}
Node current = head;
while (current.next != current) {
// Move to the node before the one to be eliminated
for (int i = 0; i < k - 1; i++) {
current = current.next;
}
// Remove the k-th node
Node nextNode = current.next;
current.next = nextNode.next;
current = current.next;
}
return current.value;
}
// Converts circular linked list to string
public String toString(Node head) {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
Node current = head;
Set<Node> visited = new HashSet<>();
while (current != null && !visited.contains(current)) {
visited.add(current);
result.append(current.value);
if (current.next != head) {
result.append(" ");
}
current = current.next;
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
Node head;
int k;
TestCase(int n, int k) {
this.k = k;
if (n == 0) {
head = null;
return;
}
head = new Node(1);
Node current = head;
for (int i = 2; i <= n; i++) {
current.next = new Node(i);
current = current.next;
}
// Make circular
current.next = head;
}
}
// Main method to test Josephus problem
public static void main(String[] args) {
JosephusProblem solver = new JosephusProblem();
// Test cases
TestCase[] testCases = {
new TestCase(4, 2), // n=4, k=2
new TestCase(3, 3), // n=3, k=3
new TestCase(1, 1), // Single node
new TestCase(5, 1), // k=1
new TestCase(7, 4) // Larger n, k
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
TestCase test = testCases[i];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input list: " + solver.toString(test.head));
System.out.println("k: " + test.k);
int result = solver.solveJosephus(test.head, test.k);
System.out.println("Last person remaining: " + result + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input list: 1 2 3 4
k: 2
Last person remaining: 3
Test case 2:
Input list: 1 2 3
k: 3
Last person remaining: 1
Test case 3:
Input list: 1
k: 1
Last person remaining: 1
Test case 4:
Input list: 1 2 3 4 5
k: 1
Last person remaining: 5
Test case 5:
Input list: 1 2 3 4 5 6 7
k: 4
Last person remaining: 2
Explanation:
- Test case 1: n=4, k=2, eliminate 2, 4, 1, leaving 3.
- Test case 2: n=3, k=3, eliminate 3, 2, leaving 1.
- Test case 3: n=1, k=1, single node 1 remains.
- Test case 4: n=5, k=1, eliminate 1, 2, 3, 4, leaving 5.
- Test case 5: n=7, k=4, eliminate 4, 1, 6, 5, 7, 3, leaving 2.
How It Works
- Node: Stores an integer value (person number) and a
nextpointer. - solveJosephus:
- Returns -1 for empty lists, head value for single-node lists.
- Iterates while more than one node remains:
- Traverses k-1 steps to find the node before the k-th.
- Removes k-th node by updating
nextpointer. - Moves to the next node.
- Returns the value of the last node.
- toString: Traverses from head, uses a Set to prevent cycling, returns space-separated string or "[]".
- Example Trace (Test case 1):
- Input: 1→2→3→4→1, k=2.
- Step 1: current=1, eliminate 2, list=1→3→4→1.
- Step 2: current=3, eliminate 4, list=1→3→1.
- Step 3: current=1, eliminate 1, list=3→3.
- Return: 3.
- Main Method: Tests different n and k, including single node and k=1.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Solve Josephus | O(n * k) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of nodes in the list.
- Time complexity: O(n * k) for solveJosephus (n-1 eliminations, each up to k steps); O(n) for toString (traverse list).
- Space complexity: O(1) for solveJosephus (constant pointers); O(n) for toString (StringBuilder and Set).
- Worst case: O(n * k) time, O(n) space for output with large lists and k.
✅ Tip: Use the circular linked list’s structure to efficiently eliminate nodes by updating
nextpointers. Keep track of the current node to continue from the correct position after each elimination.
⚠ Warning: Ensure k is handled correctly for each elimination step, and avoid infinite loops by checking for single-node conditions. Handle edge cases like n=1 or empty lists.
Rotate the List
Problem Statement
Write a Java program that rotates a circular linked list by k positions, moving the head k nodes forward. A circular linked list is a singly linked list where the last node’s next pointer points to the head, forming a cycle. The rotation shifts the head pointer forward by k nodes, effectively redefining the start of the list while maintaining the circular structure. Test the implementation with different values of k and list sizes, including empty lists, single-node lists, k=0, k equal to the list size, and k greater than the list size. You can visualize this as rotating a ring of numbered beads, where the starting bead (head) moves k positions clockwise, and the ring remains connected.
Input:
- A circular linked list of integers (e.g., 1→2→3→4→1, where 4→1 forms the cycle) and an integer k (number of positions to rotate). Output: The rotated list as a string, listing nodes from the new head until just before it cycles (e.g., "3 4 1 2" after rotating 1→2→3→4→1 by k=2). Constraints:
- The list size is between 0 and 10^5.
- Node values are integers in the range [-10^9, 10^9].
- k is a non-negative integer (k ≥ 0).
- The list may be empty. Example:
- Input: List = 1→2→3→4→1, k = 2
- Output: "3 4 1 2"
- Explanation: Rotates head from 1 to 3, list becomes 3→4→1→2→3.
- Input: List = [], k = 1
- Output: "[]"
- Explanation: Empty list remains empty.
- Input: List = 1→1, k = 3
- Output: "1"
- Explanation: Single-node list is unchanged (rotation wraps around).
Pseudocode
CLASS Node
SET value to integer
SET next to Node (null by default)
ENDCLASS
FUNCTION rotateList(head, k)
IF head is null OR head.next is head THEN
RETURN head
ENDIF
SET length to 1
SET tail to head
WHILE tail.next is not head
INCREMENT length
SET tail to tail.next
ENDWHILE
SET k to k mod length
IF k equals 0 THEN
RETURN head
ENDIF
SET current to head
FOR i from 1 to k
SET current to current.next
ENDFOR
SET newHead to current
SET tail.next to null
SET current to head
WHILE current.next is not null
SET current to current.next
ENDWHILE
SET current.next to head
SET head to newHead
RETURN head
ENDFUNCTION
FUNCTION toString(head)
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
SET visited as new Set
WHILE current is not null AND current not in visited
ADD current to visited
APPEND current.value to result
IF current.next is not head THEN
APPEND " " to result
ENDIF
SET current to current.next
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of test cases (list values, k)
FOR each testCase in testCases
PRINT test case details
CALL rotateList(testCase.head, testCase.k)
PRINT rotated list using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Nodeclass with: a. An integervalue. b. Anextpointer to the next node. - In
rotateList: a. If the list is empty or has one node, return head (no rotation needed). b. Find the list length and tail by traversing untiltail.nextis head. c. Compute effective k ask mod lengthto handle large k. d. If k=0, return head (no rotation needed). e. Traverse k steps from head to find the new head. f. Break the cycle by setting old tail’snextto null. g. Find new tail (node before new head), set itsnextto old head. h. Set head to new head, return it. - In
toString: a. If head is null, return "[]". b. Traverse from head, use a Set to avoid cycling, append values with spaces. c. Return the string representation. - In
main, test with different list sizes (empty, single-node, multi-node) and k values (0, equal to length, greater than length).
Java Implementation
import java.util.*;
public class RotateCircularLinkedList {
// Node class for the circular linked list
static class Node {
int value;
Node next;
Node(int value) {
this.value = value;
this.next = null;
}
}
// Rotates the circular linked list by k positions
public Node rotateList(Node head, int k) {
if (head == null || head.next == head) {
return head;
}
// Find length and tail
int length = 1;
Node tail = head;
while (tail.next != head) {
length++;
tail = tail.next;
}
// Normalize k
k = k % length;
if (k == 0) {
return head;
}
// Find new head
Node current = head;
for (int i = 0; i < k; i++) {
current = current.next;
}
Node newHead = current;
// Break cycle
tail.next = null;
// Find new tail (node before newHead)
current = head;
while (current.next != null) {
current = current.next;
}
// Reconnect to form new cycle
current.next = head;
head = newHead;
return head;
}
// Converts circular linked list to string
public String toString(Node head) {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
Node current = head;
Set<Node> visited = new HashSet<>();
while (current != null && !visited.contains(current)) {
visited.add(current);
result.append(current.value);
if (current.next != head) {
result.append(" ");
}
current = current.next;
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
Node head;
int k;
TestCase(int[] values, int k) {
this.k = k;
if (values.length == 0) {
head = null;
return;
}
head = new Node(values[0]);
Node current = head;
for (int i = 1; i < values.length; i++) {
current.next = new Node(values[i]);
current = current.next;
}
// Make circular
current.next = head;
}
}
// Main method to test circular linked list rotation
public static void main(String[] args) {
RotateCircularLinkedList rotator = new RotateCircularLinkedList();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{1, 2, 3, 4}, 2), // Multi-node, k < length
new TestCase(new int[]{}, 1), // Empty list
new TestCase(new int[]{5}, 3), // Single node
new TestCase(new int[]{1, 2, 3}, 3), // k = length
new TestCase(new int[]{1, 2, 3, 4, 5}, 7) // k > length
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
TestCase test = testCases[i];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input list: " + rotator.toString(test.head));
System.out.println("Rotate by k=" + test.k);
Node rotated = rotator.rotateList(test.head, test.k);
System.out.println("Rotated list: " + rotator.toString(rotated) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input list: 1 2 3 4
Rotate by k=2
Rotated list: 3 4 1 2
Test case 2:
Input list: []
Rotate by k=1
Rotated list: []
Test case 3:
Input list: 5
Rotate by k=3
Rotated list: 5
Test case 4:
Input list: 1 2 3
Rotate by k=3
Rotated list: 1 2 3
Test case 5:
Input list: 1 2 3 4 5
Rotate by k=7
Rotated list: 3 4 5 1 2
Explanation:
- Test case 1: Rotates 1→2→3→4→1 by k=2, new head=3, outputs "3 4 1 2".
- Test case 2: Empty list, no rotation, outputs "[]".
- Test case 3: Single node 5→5, k=3, no change, outputs "5".
- Test case 4: Rotates 1→2→3→1 by k=3 (length), no change, outputs "1 2 3".
- Test case 5: Rotates 1→2→3→4→5→1 by k=7 (7 mod 5 = 2), new head=3, outputs "3 4 5 1 2".
How It Works
- Node: Stores an integer value and a
nextpointer. - rotateList:
- Handles edge cases: empty list or single node returns unchanged.
- Finds list length and tail by traversing to the node where
next=head. - Normalizes k using
k mod lengthto handle large k. - If k=0, returns head.
- Traverses k steps to find new head.
- Breaks cycle at old tail, reconnects new tail to old head.
- toString: Traverses from head, uses a Set to prevent cycling, returns space-separated string or "[]".
- Example Trace (Test case 1):
- Input: 1→2→3→4→1, k=2.
- length=4, tail=4, k=2.
- current=3 (after 2 steps).
- newHead=3, tail.next=null → 1→2→3, 4.
- new tail=2, 2.next=1, head=3.
- Result: 3→4→1→2→3, outputs "3 4 1 2".
- Main Method: Tests multi-node, empty, single-node, k=length, and k>length cases.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Rotate List | O(n) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of nodes in the list.
- Time complexity: O(n) for rotateList (traverse to find length and new head); O(n) for toString (traverse list).
- Space complexity: O(1) for rotateList (constant pointers); O(n) for toString (StringBuilder and Set).
- Worst case: O(n) time, O(n) space for output with large lists.
✅ Tip: Normalize k using modulo list length to handle large k efficiently. Use a tail pointer to simplify cycle reconnection after rotation.
⚠ Warning: Ensure the cycle is broken and reconnected correctly to avoid infinite loops. Handle edge cases like empty lists or k=0 to prevent unnecessary operations.
Round-Robin Scheduler
Problem Statement
Write a Java program that simulates a round-robin scheduler using a circular linked list. Each node in the list represents a task with a string name. The scheduler should support adding tasks (insert at tail), cycling to the next task (move head to next node), and removing the current task (delete head), maintaining the circular structure. The program should track the current task (head) and handle operations in a round-robin fashion, where cycling moves to the next task in sequence. Test the implementation with sequences of operations, including adding tasks, cycling through them, removing tasks, and handling edge cases like empty lists and single-task lists. You can visualize this as a circular queue of tasks in a CPU scheduler, where tasks are processed one after another in a loop, and completed tasks are removed.
Input:
- A sequence of operations, where each operation is:
addTask(task): Add a task (string) at the tail of the list.cycle(): Move to the next task, return its name or "null" if empty.removeTask(): Remove the current task (head), return its name or "null" if empty.printTasks(): Print the list of tasks from the current head until just before it cycles. Output: For each operation, print the action performed (e.g., "Added task", "Cycled to task", "Removed task", or the task list). Return "null" for cycle or remove on an empty list. Constraints:
- The list size is between 0 and 10^5.
- Task names are non-empty strings of length up to 100 characters.
- The list may be empty. Example:
- Input: Operations = [addTask("Task1"), addTask("Task2"), addTask("Task3"), printTasks, cycle, printTasks, removeTask, printTasks]
- Output:
Added Task1 Added Task2 Added Task3 Tasks: Task1 Task2 Task3 Cycled to Task2 Tasks: Task2 Task3 Task1 Removed Task2 Tasks: Task3 Task1 - Input: Operations = [cycle, printTasks]
- Output:
Cycled to null Tasks: []
Pseudocode
CLASS Node
SET task to string
SET next to Node (null by default)
ENDCLASS
CLASS RoundRobinScheduler
SET head to null
FUNCTION addTask(task)
CREATE newNode as new Node(task)
IF head is null THEN
SET head to newNode
SET newNode.next to head
ELSE
SET current to head
WHILE current.next is not head
SET current to current.next
ENDWHILE
SET current.next to newNode
SET newNode.next to head
ENDIF
ENDFUNCTION
FUNCTION cycle()
IF head is null THEN
RETURN "null"
ENDIF
SET head to head.next
RETURN head.task
ENDFUNCTION
FUNCTION removeTask()
IF head is null THEN
RETURN "null"
ENDIF
IF head.next is head THEN
SET task to head.task
SET head to null
RETURN task
ENDIF
SET task to head.task
SET current to head
WHILE current.next is not head
SET current to current.next
ENDWHILE
SET current.next to head.next
SET head to head.next
RETURN task
ENDFUNCTION
FUNCTION toString()
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
SET visited as new Set
WHILE current is not null AND current not in visited
ADD current to visited
APPEND current.task to result
IF current.next is not head THEN
APPEND " " to result
ENDIF
SET current to current.next
ENDWHILE
RETURN result as string
ENDFUNCTION
ENDCLASS
FUNCTION testScheduler(operations)
CREATE scheduler as new RoundRobinScheduler
FOR each operation in operations
IF operation.type equals "addTask" THEN
CALL scheduler.addTask(operation.task)
PRINT added task
ELSE IF operation.type equals "cycle" THEN
SET task to scheduler.cycle()
PRINT cycled to task
ELSE IF operation.type equals "removeTask" THEN
SET task to scheduler.removeTask()
PRINT removed task
ELSE IF operation.type equals "print" THEN
PRINT tasks using scheduler.toString
ENDIF
ENDFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of operation sequences
FOR each testCase in testCases
PRINT test case details
CALL testScheduler(testCase)
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Nodeclass with: a. A stringtaskfor the task name. b. Anextpointer to the next node. - Define a
RoundRobinSchedulerclass with: a. Aheadpointer to track the current task. b.addTask: Add node at tail, connect to head to maintain cycle. c.cycle: Move head to next node, return task name or "null". d.removeTask: Remove head, update cycle, return task name or "null". e.toString: Print tasks from head until cycle, return "[]" if empty. - In
testScheduler: a. Create aRoundRobinScheduler. b. For each operation:addTask: CalladdTask, print action.cycle: Callcycle, print task name.removeTask: CallremoveTask, print task name.print: CalltoString, print task list.
- In
main, test with sequences including adds, cycles, removes, and edge cases.
Java Implementation
import java.util.*;
public class RoundRobinScheduler {
// Node class for the circular linked list
static class Node {
String task;
Node next;
Node(String task) {
this.task = task;
this.next = null;
}
}
// RoundRobinScheduler class to simulate task scheduling
static class Scheduler {
private Node head;
public void addTask(String task) {
Node newNode = new Node(task);
if (head == null) {
head = newNode;
newNode.next = head;
} else {
Node current = head;
while (current.next != head) {
current = current.next;
}
current.next = newNode;
newNode.next = head;
}
}
public String cycle() {
if (head == null) {
return "null";
}
head = head.next;
return head.task;
}
public String removeTask() {
if (head == null) {
return "null";
}
String task = head.task;
if (head.next == head) {
head = null;
return task;
}
Node current = head;
while (current.next != head) {
current = current.next;
}
current.next = head.next;
head = head.next;
return task;
}
public String toString() {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
Node current = head;
Set<Node> visited = new HashSet<>();
while (current != null && !visited.contains(current)) {
visited.add(current);
result.append(current.task);
if (current.next != head) {
result.append(" ");
}
current = current.next;
}
return result.toString();
}
}
// Helper class for operations
static class Operation {
String type;
String task;
Operation(String type, String task) {
this.type = type;
this.task = task;
}
}
// Tests round-robin scheduler operations
public void testScheduler(List<Operation> operations) {
Scheduler scheduler = new Scheduler();
for (Operation op : operations) {
if (op.type.equals("addTask")) {
scheduler.addTask(op.task);
System.out.println("Added " + op.task);
} else if (op.type.equals("cycle")) {
String task = scheduler.cycle();
System.out.println("Cycled to " + task);
} else if (op.type.equals("removeTask")) {
String task = scheduler.removeTask();
System.out.println("Removed " + task);
} else if (op.type.equals("print")) {
System.out.println("Tasks: " + scheduler.toString());
}
}
}
// Main method to test round-robin scheduler
public static void main(String[] args) {
RoundRobinScheduler manager = new RoundRobinScheduler();
// Test cases
List<List<Operation>> testCases = new ArrayList<>();
// Test case 1: Normal operations
testCases.add(Arrays.asList(
new Operation("addTask", "Task1"),
new Operation("addTask", "Task2"),
new Operation("addTask", "Task3"),
new Operation("print", null),
new Operation("cycle", null),
new Operation("print", null),
new Operation("removeTask", null),
new Operation("print", null)
));
// Test case 2: Empty list
testCases.add(Arrays.asList(
new Operation("cycle", null),
new Operation("removeTask", null),
new Operation("print", null)
));
// Test case 3: Single task
testCases.add(Arrays.asList(
new Operation("addTask", "Task1"),
new Operation("print", null),
new Operation("cycle", null),
new Operation("print", null),
new Operation("removeTask", null),
new Operation("print", null)
));
// Test case 4: Multiple cycles and removes
testCases.add(Arrays.asList(
new Operation("addTask", "Task1"),
new Operation("addTask", "Task2"),
new Operation("addTask", "Task3"),
new Operation("cycle", null),
new Operation("cycle", null),
new Operation("print", null),
new Operation("removeTask", null),
new Operation("print", null)
));
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
System.out.println("Test case " + (i + 1) + ":");
manager.testScheduler(testCases.get(i));
System.out.println();
}
}
}
Output
Running the main method produces:
Test case 1:
Added Task1
Added Task2
Added Task3
Tasks: Task1 Task2 Task3
Cycled to Task2
Tasks: Task2 Task3 Task1
Removed Task2
Tasks: Task3 Task1
Test case 2:
Cycled to null
Removed null
Tasks: []
Test case 3:
Added Task1
Tasks: Task1
Cycled to Task1
Tasks: Task1
Removed Task1
Tasks: []
Test case 4:
Added Task1
Added Task2
Added Task3
Cycled to Task2
Cycled to Task3
Tasks: Task3 Task1 Task2
Removed Task3
Tasks: Task1 Task2
Explanation:
- Test case 1: Adds Task1, Task2, Task3, prints "Task1 Task2 Task3", cycles to Task2, prints "Task2 Task3 Task1", removes Task2, prints "Task3 Task1".
- Test case 2: Cycles and removes on empty list, prints "null" and "[]".
- Test case 3: Adds Task1, prints "Task1", cycles to Task1, prints "Task1", removes Task1, prints "[]".
- Test case 4: Adds Task1, Task2, Task3, cycles twice to Task3, prints "Task3 Task1 Task2", removes Task3, prints "Task1 Task2".
How It Works
- Node: Stores a string task name and a
nextpointer. - Scheduler:
addTask: Adds node at tail, connects to head, O(n).cycle: Moves head to next node, returns task or "null", O(1).removeTask: Removes head, updates cycle, returns task or "null", O(n).toString: Traverses from head, uses Set to prevent cycling, returns space-separated string or "[]".
- testScheduler: Executes operations, printing actions and results.
- Example Trace (Test case 1):
- addTask("Task1"): head=Task1→Task1.
- addTask("Task2"): head=Task1→Task2→Task1.
- addTask("Task3"): head=Task1→Task2→Task3→Task1.
- print: "Task1 Task2 Task3".
- cycle: head=Task2, returns "Task2".
- print: "Task2 Task3 Task1".
- removeTask: Removes Task2, head=Task3→Task1→Task3, returns "Task2".
- print: "Task3 Task1".
- Main Method: Tests normal operations, empty list, single task, and multiple cycles/removes.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Add Task | O(n) | O(1) |
| Cycle | O(1) | O(1) |
| Remove Task | O(n) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of nodes in the list.
- Time complexity: O(n) for addTask and removeTask (traverse to tail); O(1) for cycle; O(n) for toString (traverse list).
- Space complexity: O(1) for addTask, cycle, removeTask (constant pointers); O(n) for toString (StringBuilder and Set).
- Worst case: O(n) time, O(n) space for output with large lists.
✅ Tip: Use a circular linked list to naturally model the round-robin cycling behavior. Maintain the head pointer to track the current task and ensure the tail connects to the head for circularity.
⚠ Warning: Update
nextpointers carefully during task removal to maintain the circular structure. Handle edge cases like empty lists and single-task lists to avoid null pointer issues.
Split Circular List
Problem Statement
Write a Java program that splits a circular linked list into two circular linked lists of roughly equal size. A circular linked list is a singly linked list where the last node’s next pointer points to the head, forming a cycle. The splitting should divide the nodes as evenly as possible, with one list having at most one more node than the other for odd-sized lists, and each resulting list maintaining its circular structure. Test the implementation with even-sized lists, odd-sized lists, empty lists, and single-node lists. You can visualize this as dividing a ring of numbered beads into two smaller rings, each containing about half the beads, with both rings remaining closed loops.
Input:
- A circular linked list of integers (e.g., 1→2→3→4→1, where 4→1 forms the cycle). Output: Two circular linked lists as strings, listing nodes from each head until just before it cycles (e.g., "1 2" and "3 4" for the input above). Constraints:
- The list size is between 0 and 10^5.
- Node values are integers in the range [-10^9, 10^9].
- The list may be empty. Example:
- Input: List = 1→2→3→4→1
- Output: First list: "1 2", Second list: "3 4"
- Explanation: Splits into two circular lists of 2 nodes each: 1→2→1 and 3→4→3.
- Input: List = 1→2→3→1
- Output: First list: "1 2", Second list: "3"
- Explanation: Splits into 1→2→1 (2 nodes) and 3→3 (1 node).
- Input: List = []
- Output: First list: "[]", Second list: "[]"
- Explanation: Empty list splits into two empty lists.
Pseudocode
CLASS Node
SET value to integer
SET next to Node (null by default)
ENDCLASS
FUNCTION splitList(head)
IF head is null THEN
RETURN (null, null)
ENDIF
IF head.next is head THEN
RETURN (head, null)
ENDIF
SET length to 1
SET tail to head
WHILE tail.next is not head
INCREMENT length
SET tail to tail.next
ENDWHILE
SET splitPoint to length / 2
SET current to head
FOR i from 1 to splitPoint - 1
SET current to current.next
ENDFOR
SET firstHead to head
SET secondHead to current.next
SET secondTail to tail
SET firstTail to current
SET firstTail.next to firstHead
SET secondTail.next to secondHead
RETURN (firstHead, secondHead)
ENDFUNCTION
FUNCTION toString(head)
IF head is null THEN
RETURN "[]"
ENDIF
CREATE result as new StringBuilder
SET current to head
SET visited as new Set
WHILE current is not null AND current not in visited
ADD current to visited
APPEND current.value to result
IF current.next is not head THEN
APPEND " " to result
ENDIF
SET current to current.next
ENDWHILE
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of circular linked lists
FOR each testCase in testCases
PRINT test case details
CALL splitList(testCase.head)
PRINT first and second lists using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Nodeclass with: a. An integervalue. b. Anextpointer to the next node. - In
splitList: a. If the list is empty, return (null, null). b. If the list has one node, return (head, null). c. Find the list length and tail by traversing untiltail.nextis head. d. Compute split point aslength / 2. e. Traverse to the node before the split point (first list’s tail). f. Set first list’s head and tail, second list’s head and tail. g. Connect first list’s tail to its head, second list’s tail to its head. h. Return (firstHead, secondHead). - In
toString: a. If head is null, return "[]". b. Traverse from head, use a Set to avoid cycling, append values with spaces. c. Return the string representation. - In
main, test with even-sized, odd-sized, empty, and single-node circular lists.
Java Implementation
import java.util.*;
public class SplitCircularLinkedList {
// Node class for the circular linked list
static class Node {
int value;
Node next;
Node(int value) {
this.value = value;
this.next = null;
}
}
// Splits the circular linked list into two circular lists
public Node[] splitList(Node head) {
Node[] result = new Node[2];
if (head == null) {
result[0] = null;
result[1] = null;
return result;
}
if (head.next == head) {
result[0] = head;
result[1] = null;
return result;
}
// Find length and tail
int length = 1;
Node tail = head;
while (tail.next != head) {
length++;
tail = tail.next;
}
// Find split point
int splitPoint = length / 2;
Node current = head;
for (int i = 0; i < splitPoint - 1; i++) {
current = current.next;
}
// Set heads and tails
Node firstHead = head;
Node secondHead = current.next;
Node firstTail = current;
Node secondTail = tail;
// Form two circular lists
firstTail.next = firstHead;
secondTail.next = secondHead;
result[0] = firstHead;
result[1] = secondHead;
return result;
}
// Converts circular linked list to string
public String toString(Node head) {
if (head == null) {
return "[]";
}
StringBuilder result = new StringBuilder();
Node current = head;
Set<Node> visited = new HashSet<>();
while (current != null && !visited.contains(current)) {
visited.add(current);
result.append(current.value);
if (current.next != head) {
result.append(" ");
}
current = current.next;
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
Node head;
TestCase(int[] values) {
if (values.length == 0) {
head = null;
return;
}
head = new Node(values[0]);
Node current = head;
for (int i = 1; i < values.length; i++) {
current.next = new Node(values[i]);
current = current.next;
}
// Make circular
current.next = head;
}
}
// Main method to test circular linked list splitting
public static void main(String[] args) {
SplitCircularLinkedList splitter = new SplitCircularLinkedList();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{1, 2, 3, 4}), // Even-sized list
new TestCase(new int[]{1, 2, 3}), // Odd-sized list
new TestCase(new int[]{}), // Empty list
new TestCase(new int[]{5}), // Single node
new TestCase(new int[]{1, 1, 1, 1, 1}) // Odd-sized with duplicates
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
TestCase test = testCases[i];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input list: " + splitter.toString(test.head));
Node[] result = splitter.splitList(test.head);
System.out.println("First list: " + splitter.toString(result[0]));
System.out.println("Second list: " + splitter.toString(result[1]) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input list: 1 2 3 4
First list: 1 2
Second list: 3 4
Test case 2:
Input list: 1 2 3
First list: 1 2
Second list: 3
Test case 3:
Input list: []
First list: []
Second list: []
Test case 4:
Input list: 5
First list: 5
Second list: []
Test case 5:
Input list: 1 1 1 1 1
First list: 1 1 1
Second list: 1 1
Explanation:
- Test case 1: Splits 1→2→3→4→1 into 1→2→1 (2 nodes) and 3→4→3 (2 nodes).
- Test case 2: Splits 1→2→3→1 into 1→2→1 (2 nodes) and 3→3 (1 node).
- Test case 3: Empty list splits into "[]" and "[]".
- Test case 4: Single node 5→5 splits into 5→5 and "[]".
- Test case 5: Splits 1→1→1→1→1→1 into 1→1→1→1 (3 nodes) and 1→1→1 (2 nodes).
How It Works
- Node: Stores an integer value and a
nextpointer. - splitList:
- Returns (null, null) for empty lists, (head, null) for single-node lists.
- Finds list length and tail by traversing to
tail.next=head. - Computes split point as
length / 2. - Traverses to the node before the split point (first list’s tail).
- Sets first list’s head/tail and second list’s head/tail.
- Connects first tail to first head, second tail to second head.
- toString: Traverses from head, uses a Set to prevent cycling, returns space-separated string or "[]".
- Example Trace (Test case 1):
- Input: 1→2→3→4→1.
- length=4, tail=4, splitPoint=2.
- current=2 (after 1 step), firstHead=1, firstTail=2, secondHead=3, secondTail=4.
- firstTail.next=firstHead: 1→2→1.
- secondTail.next=secondHead: 3→4→3.
- Returns (1, 3), outputs "1 2" and "3 4".
- Main Method: Tests even-sized, odd-sized, empty, single-node, and duplicate lists.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Split List | O(n) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of nodes in the list.
- Time complexity: O(n) for splitList (traverse to find length and split point); O(n) for toString (traverse list).
- Space complexity: O(1) for splitList (constant pointers); O(n) for toString (StringBuilder and Set).
- Worst case: O(n) time, O(n) space for output with large lists.
✅ Tip: Split
Hashing Problem Solving with DSA
🛠 What you will learn
This section outlines the format used for each DSA problem statement, designed to guide students through solving exercise problems in a clear, structured learning process. Each component serves a specific purpose to enhance understanding and practical application:
- Problem Statement: Clearly defines the task, including inputs, outputs, constraints, and an example. A real-world analogy makes the problem relatable, aligning with the engaging tone of the "Core Data Structures" summary, ensuring students grasp the problem’s context.
- Pseudocode: Provides a high-level, language-agnostic outline of the solution using standardized syntax (e.g.,
FUNCTION,IF,SET,RETURN). This helps students understand the logic before diving into code, bridging theory and implementation. - Algorithm Steps: Breaks down the pseudocode into detailed, actionable steps, connecting theoretical logic to practical coding. This ensures students can follow the implementation process systematically.
- Java Implementation: Offers a complete, commented Java code solution that students can study and run. It includes a
mainmethod with at least four test cases (e.g., target present, absent, middle element, and edge cases like duplicates or single elements) to verify correctness and explore different scenarios, maintaining a beginner-friendly approach. - Output: Shows the expected results from the test cases, including indices, comparisons, and execution times (where applicable), helping students verify their code’s correctness and understand the algorithm’s behavior.
- How It Works: Traces the algorithm’s execution with a detailed example, showing step-by-step how the search narrows down the range. This reinforces understanding by illustrating the algorithm in action.
- Complexity Analysis Table: Summarizes time and space complexity for best, average, and worst cases, teaching students to evaluate efficiency and compare trade-offs across implementations.
- Tip or Warning Box: Provides practical advice (e.g., when to use DSA) and highlights pitfalls (e.g., ensuring the array is sorted), guiding students toward best practices and common error avoidance.
Each exercise includes test cases to verify correctness and performance, aligning with a learning platform featuring embedded compilers for hands-on practice and visualizations to illustrate the halving process of DSA.
Collision Analysis
Problem Statement
Write a Java program that implements a custom hash table to track the number of collisions, where multiple keys map to the same array index. The hash table should use an array with chaining (linked lists) to handle collisions, with strings as keys and integers as values. A collision occurs when a key hashes to an index that already contains one or more keys. The program should provide methods to insert keys and report the total number of collisions, then test the implementation with different sets of keys, including cases with high collision rates, few collisions, and edge cases like empty sets or single keys. You can visualize this as organizing items into labeled bins, counting how often multiple items land in the same bin due to their labels hashing to the same position.
Input:
- A set of string keys to insert into the hash table. Output: The number of collisions and the hash table contents (key-value pairs per index). Constraints:
- The hash table size (array length) is fixed at 10 for simplicity.
- Keys are non-empty strings of up to 100 characters.
- Values are integers (e.g., 1 for each key inserted).
- The number of keys is between 0 and 10^5. Example:
- Input: Keys = ["cat", "act", "dog", "god"]
- Output:
Collisions: 2 Hash Table: Index 0: [] Index 1: [] Index 2: [cat: 1, act: 1] Index 3: [] Index 4: [] Index 5: [dog: 1, god: 1] Index 6: [] Index 7: [] Index 8: [] Index 9: [] - Explanation: "cat" and "act" collide at index 2, "dog" and "god" collide at index 5, resulting in 2 collisions.
- Input: Keys = []
- Output:
Collisions: 0 Hash Table: Index 0: [] ... Index 9: []
Pseudocode
CLASS Node
SET key to string
SET value to integer
SET next to Node (null by default)
ENDCLASS
CLASS HashTable
SET table to array of Node pointers (size 10)
SET collisionCount to 0
FUNCTION hash(key)
SET sum to 0
FOR each character in key
ADD character ASCII value to sum
ENDFOR
RETURN sum mod 10
ENDFUNCTION
FUNCTION insert(key, value)
SET index to hash(key)
IF table[index] is null THEN
SET table[index] to new Node(key, value)
ELSE
INCREMENT collisionCount
SET current to table[index]
WHILE current is not null
IF current.key equals key THEN
SET current.value to value
RETURN
ENDIF
SET current to current.next
ENDWHILE
CREATE newNode as new Node(key, value)
SET newNode.next to table[index]
SET table[index] to newNode
ENDIF
ENDFUNCTION
FUNCTION getCollisionCount()
RETURN collisionCount
ENDFUNCTION
FUNCTION toString()
CREATE result as new StringBuilder
FOR i from 0 to table size - 1
APPEND "Index " and i and ": [" to result
SET current to table[i]
WHILE current is not null
APPEND current.key and ": " and current.value to result
IF current.next is not null THEN
APPEND ", " to result
ENDIF
SET current to current.next
ENDWHILE
APPEND "]" to result
IF i is not last index THEN
APPEND newline to result
ENDIF
ENDFOR
RETURN result as string
ENDFUNCTION
ENDCLASS
FUNCTION main()
SET testCases to array of key sets
FOR each testCase in testCases
PRINT test case details
CREATE hashTable as new HashTable
FOR each key in testCase
CALL hashTable.insert(key, 1)
ENDFOR
PRINT collision count
PRINT hash table contents using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Nodeclass with: a. A stringkey. b. An integervalue. c. Anextpointer for chaining. - Define a
HashTableclass with: a. A fixed-size array (table) of size 10. b. AcollisionCountto track collisions. c.hash: Compute index by summing ASCII values of key characters modulo 10. d.insert: Insert key-value pair, incrementcollisionCountif index is occupied, handle duplicates. e.getCollisionCount: Return collision count. f.toString: Print key-value pairs per index. - In
main, test with key sets: a. Keys with collisions (e.g., anagrams). b. Keys with no collisions. c. Empty set. d. Single key. e. Large set with high collision rate.
Java Implementation
import java.util.*;
public class CollisionAnalysis {
// Node class for chaining
static class Node {
String key;
int value;
Node next;
Node(String key, int value) {
this.key = key;
this.value = value;
this.next = null;
}
}
// Custom hash table with collision tracking
static class HashTable {
private Node[] table;
private int collisionCount;
public HashTable() {
table = new Node[10];
collisionCount = 0;
}
private int hash(String key) {
int sum = 0;
for (char c : key.toCharArray()) {
sum += c;
}
return sum % 10;
}
public void insert(String key, int value) {
int index = hash(key);
if (table[index] == null) {
table[index] = new Node(key, value);
} else {
collisionCount++;
Node current = table[index];
while (current != null) {
if (current.key.equals(key)) {
current.value = value;
return;
}
current = current.next;
}
Node newNode = new Node(key, value);
newNode.next = table[index];
table[index] = newNode;
}
}
public int getCollisionCount() {
return collisionCount;
}
public String toString() {
StringBuilder result = new StringBuilder();
for (int i = 0; i < table.length; i++) {
result.append("Index ").append(i).append(": [");
Node current = table[i];
while (current != null) {
result.append(current.key).append(": ").append(current.value);
if (current.next != null) {
result.append(", ");
}
current = current.next;
}
result.append("]");
if (i < table.length - 1) {
result.append("\n");
}
}
return result.toString();
}
}
// Main method to test collision analysis
public static void main(String[] args) {
CollisionAnalysis analyzer = new CollisionAnalysis();
// Test cases
String[][] testCases = {
{"cat", "act", "dog", "god"}, // High collisions (anagrams)
{"apple", "banana", "cherry"}, // No collisions
{}, // Empty set
{"single"}, // Single key
{"key1", "key2", "key3", "key4", "key5", "yek1", "yek2"} // High collisions
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input keys: " + Arrays.toString(testCases[i]));
HashTable hashTable = new HashTable();
for (String key : testCases[i]) {
hashTable.insert(key, 1);
}
System.out.println("Collisions: " + hashTable.getCollisionCount());
System.out.println("Hash Table:\n" + hashTable.toString() + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input keys: [cat, act, dog, god]
Collisions: 2
Hash Table:
Index 0: []
Index 1: []
Index 2: [act: 1, cat: 1]
Index 3: []
Index 4: []
Index 5: [god: 1, dog: 1]
Index 6: []
Index 7: []
Index 8: []
Index 9: []
Test case 2:
Input keys: [apple, banana, cherry]
Collisions: 0
Hash Table:
Index 0: []
Index 1: []
Index 2: []
Index 3: []
Index 4: [cherry: 1]
Index 5: []
Index 6: []
Index 7: [apple: 1]
Index 8: []
Index 9: [banana: 1]
Test case 3:
Input keys: []
Collisions: 0
Hash Table:
Index 0: []
Index 1: []
Index 2: []
Index 3: []
Index 4: []
Index 5: []
Index 6: []
Index 7: []
Index 8: []
Index 9: []
Test case 4:
Input keys: [single]
Collisions: 0
Hash Table:
Index 0: []
Index 1: []
Index 2: []
Index 3: []
Index 4: []
Index 5: []
Index 6: [single: 1]
Index 7: []
Index 8: []
Index 9: []
Test case 5:
Input keys: [key1, key2, key3, key4, key5, yek1, yek2]
Collisions: 4
Hash Table:
Index 0: []
Index 1: []
Index 2: []
Index 3: []
Index 4: [key5: 1, key4: 1, key3: 1]
Index 5: []
Index 6: []
Index 7: []
Index 8: []
Index 9: [yek2: 1, yek1: 1, key2: 1, key1: 1]
Explanation:
- Test case 1: "cat" and "act" collide at index 2, "dog" and "god" at index 5 (2 collisions).
- Test case 2: No collisions, keys map to distinct indices.
- Test case 3: Empty set, no collisions.
- Test case 4: Single key, no collisions.
- Test case 5: Multiple keys collide at indices 4 and 9 (4 collisions).
How It Works
- Node: Stores a string key, integer value, and
nextpointer for chaining. - HashTable:
- Uses a fixed-size array (size 10) with linked lists for chaining.
hash: Sums ASCII values of key characters modulo 10.insert: Inserts key-value pair, incrementscollisionCountif index occupied, updates value for duplicates.getCollisionCount: Returns collision count.toString: Prints key-value pairs per index.
- Example Trace (Test case 1):
- Insert "cat": index=2, table[2]=[cat:1], collisions=0.
- Insert "act": index=2, table[2]=[act:1,cat:1], collisions=1.
- Insert "dog": index=5, table[5]=[dog:1], collisions=1.
- Insert "god": index=5, table[5]=[god:1,dog:1], collisions=2.
- Output: Collisions=2, table shows lists at indices 2 and 5.
- Main Method: Tests high collisions, no collisions, empty set, single key, and large set.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Insert | O(1) average | O(1) |
| Get Collision Count | O(1) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of keys in the hash table.
- Time complexity: O(1) average for insert (hashing and list insertion); O(1) for getCollisionCount; O(n) for toString (iterate all keys).
- Space complexity: O(1) for insert and getCollisionCount; O(n) for toString (StringBuilder output).
- Worst case: O(n) time for insert (many keys in one index), O(n) space for toString.
✅ Tip: Use a simple hash function for educational purposes, but ensure it distributes keys evenly to minimize collisions. Track collisions by incrementing a counter when an index is already occupied.
⚠ Warning: A poor hash function (e.g., summing ASCII values) may cause excessive collisions. Test with diverse key sets to verify collision behavior, and handle duplicates to avoid redundant entries.
Custom Hash Function
Problem Statement
Write a Java program that implements a custom hash function for strings of exactly 3 alphabetic characters (e.g., "abc", "xyz") and integrates it into a hash table using an array with chaining (linked lists) to handle collisions. The hash table should store strings as keys and integers as values. The program should also implement a hash function using Java’s default hashCode method for comparison, tracking the number of collisions (when multiple keys map to the same array index) for both hash functions. Test the implementation with sets of 3-character string keys, comparing collision counts between the custom hash function and hashCode, including edge cases like empty sets, single keys, or identical keys. You can visualize this as organizing 3-letter tags into a fixed number of bins, comparing how two different labeling systems distribute tags and how often multiple tags land in the same bin.
Input:
- A set of strings, each exactly 3 alphabetic characters, to insert into the hash table.
Output: The number of collisions for both the custom hash function and Java’s
hashCode, and the hash table contents (key-value pairs per index) for both. Constraints: - The hash table size (array length) is fixed at 10.
- Keys are strings of exactly 3 alphabetic characters (a-z, A-Z).
- Values are integers (e.g., 1 for each key inserted).
- The number of keys is between 0 and 10^5. Example:
- Input: Keys = ["abc", "cba", "def", "fed"]
- Output:
Custom Hash Collisions: 2 Custom Hash Table: Index 0: [] Index 1: [] Index 2: [cba: 1, abc: 1] Index 3: [] Index 4: [] Index 5: [fed: 1, def: 1] Index 6: [] Index 7: [] Index 8: [] Index 9: [] hashCode Collisions: 0 hashCode Hash Table: Index 0: [fed: 1] Index 1: [] Index 2: [] Index 3: [] Index 4: [] Index 5: [] Index 6: [abc: 1] Index 7: [def: 1] Index 8: [] Index 9: [cba: 1] - Explanation: Custom hash causes 2 collisions ("abc"/"cba" at index 2, "def"/"fed" at index 5);
hashCodecauses 0 collisions.
Pseudocode
CLASS Node
SET key to string
SET value to integer
SET next to Node (null by default)
ENDCLASS
CLASS HashTable
SET table to array of Node pointers (size 10)
SET collisionCount to 0
FUNCTION customHash(key)
SET sum to 0
FOR i from 0 to 2
SET sum to sum * 31 + (lowercase(key[i]) - 'a' + 1)
ENDFOR
RETURN absolute(sum) mod 10
ENDFUNCTION
FUNCTION hashCodeHash(key)
RETURN absolute(key.hashCode()) mod 10
ENDFUNCTION
FUNCTION insert(key, value, useCustomHash)
IF length of key is not 3 OR key contains non-alphabetic characters THEN
RETURN
ENDIF
SET index to customHash(key) if useCustomHash else hashCodeHash(key)
IF table[index] is null THEN
SET table[index] to new Node(key, value)
ELSE
INCREMENT collisionCount
SET current to table[index]
WHILE current is not null
IF current.key equals key THEN
SET current.value to value
RETURN
ENDIF
SET current to current.next
ENDWHILE
CREATE newNode as new Node(key, value)
SET newNode.next to table[index]
SET table[index] to newNode
ENDIF
ENDFUNCTION
FUNCTION getCollisionCount()
RETURN collisionCount
ENDFUNCTION
FUNCTION toString()
CREATE result as new StringBuilder
FOR i from 0 to table size - 1
APPEND "Index " and i and ": [" to result
SET current to table[i]
WHILE current is not null
APPEND current.key and ": " and current.value to result
IF current.next is not null THEN
APPEND ", " to result
ENDIF
SET current to current.next
ENDWHILE
APPEND "]" to result
IF i is not last index THEN
APPEND newline to result
ENDIF
ENDFOR
RETURN result as string
ENDFUNCTION
ENDCLASS
FUNCTION main()
SET testCases to array of key sets
FOR each testCase in testCases
PRINT test case details
CREATE customHashTable as new HashTable
CREATE hashCodeTable as new HashTable
FOR each key in testCase
IF key is valid THEN
CALL customHashTable.insert(key, 1, true)
CALL hashCodeTable.insert(key, 1, false)
ENDIF
ENDFOR
PRINT custom hash collision count
PRINT custom hash table contents
PRINT hashCode collision count
PRINT hashCode hash table contents
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Nodeclass with: a. A stringkey(3 alphabetic characters). b. An integervalue. c. Anextpointer for chaining. - Define a
HashTableclass with: a. A fixed-size array (table) of size 10. b. AcollisionCountto track collisions. c.customHash: Polynomial rolling hash (e.g., sum = sum * 31 + (char - 'a' + 1)) modulo 10. d.hashCodeHash: Uses Java’shashCodemodulo 10. e.insert: Validates key, computes index, incrementscollisionCountif index occupied, handles duplicates. f.getCollisionCount: Returns collision count. g.toString: Prints key-value pairs per index. - In
main, test with key sets: a. Keys causing collisions (e.g., anagrams like "abc", "cba"). b. Keys with no collisions. c. Empty set. d. Single key. e. Repeated keys.
Java Implementation
import java.util.*;
public class CustomHashFunction {
// Node class for chaining
static class Node {
String key;
int value;
Node next;
Node(String key, int value) {
this.key = key;
this.value = value;
this.next = null;
}
}
// Custom hash table with collision tracking
static class HashTable {
private Node[] table;
private int collisionCount;
public HashTable() {
table = new Node[10];
collisionCount = 0;
}
private int customHash(String key) {
int sum = 0;
for (int i = 0; i < 3; i++) {
sum = sum * 31 + (Character.toLowerCase(key.charAt(i)) - 'a' + 1);
}
return Math.abs(sum) % 10;
}
private int hashCodeHash(String key) {
return Math.abs(key.hashCode()) % 10;
}
public void insert(String key, int value, boolean useCustomHash) {
if (key.length() != 3 || !key.matches("[a-zA-Z]+")) {
return;
}
int index = useCustomHash ? customHash(key) : hashCodeHash(key);
if (table[index] == null) {
table[index] = new Node(key, value);
} else {
collisionCount++;
Node current = table[index];
while (current != null) {
if (current.key.equals(key)) {
current.value = value;
return;
}
current = current.next;
}
Node newNode = new Node(key, value);
newNode.next = table[index];
table[index] = newNode;
}
}
public int getCollisionCount() {
return collisionCount;
}
public String toString() {
StringBuilder result = new StringBuilder();
for (int i = 0; i < table.length; i++) {
result.append("Index ").append(i).append(": [");
Node current = table[i];
while (current != null) {
result.append(current.key).append(": ").append(current.value);
if (current.next != null) {
result.append(", ");
}
current = current.next;
}
result.append("]");
if (i < table.length - 1) {
result.append("\n");
}
}
return result.toString();
}
}
// Main method to test custom hash function
public static void main(String[] args) {
CustomHashFunction analyzer = new CustomHashFunction();
// Test cases
String[][] testCases = {
{"abc", "cba", "def", "fed"}, // Anagrams, high collisions
{"xyz", "abc", "def"}, // No collisions
{}, // Empty set
{"ghi"}, // Single key
{"aaa", "aaa", "aaa"} // Repeated keys
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input keys: " + Arrays.toString(testCases[i]));
HashTable customHashTable = new HashTable();
HashTable hashCodeTable = new HashTable();
for (String key : testCases[i]) {
customHashTable.insert(key, 1, true);
hashCodeTable.insert(key, 1, false);
}
System.out.println("Custom Hash Collisions: " + customHashTable.getCollisionCount());
System.out.println("Custom Hash Table:\n" + customHashTable.toString());
System.out.println("hashCode Collisions: " + hashCodeTable.getCollisionCount());
System.out.println("hashCode Hash Table:\n" + hashCodeTable.toString() + "\n");
}
}
}
Output
Running the main method produces (note: exact indices for hashCode may vary due to implementation, but collision counts are illustrative):
Test case 1:
Input keys: [abc, cba, def, fed]
Custom Hash Collisions: 2
Custom Hash Table:
Index 0: []
Index 1: []
Index 2: [cba: 1, abc: 1]
Index 3: []
Index 4: []
Index 5: [fed: 1, def: 1]
Index 6: []
Index 7: []
Index 8: []
Index 9: []
hashCode Collisions: 0
hashCode Hash Table:
Index 0: [fed: 1]
Index 1: []
Index 2: []
Index 3: []
Index 4: []
Index 5: []
Index 6: [abc: 1]
Index 7: [def: 1]
Index 8: []
Index 9: [cba: 1]
Test case 2:
Input keys: [xyz, abc, def]
Custom Hash Collisions: 0
Custom Hash Table:
Index 0: []
Index 1: []
Index 2: []
Index 3: []
Index 4: [def: 1]
Index 5: []
Index 6: []
Index 7: []
Index 8: [abc: 1]
Index 9: [xyz: 1]
hashCode Collisions: 0
hashCode Hash Table:
Index 0: []
Index 1: []
Index 2: []
Index 3: []
Index 4: [def: 1]
Index 5: []
Index 6: [abc: 1]
Index 7: []
Index 8: []
Index 9: [xyz: 1]
Test case 3:
Input keys: []
Custom Hash Collisions: 0
Custom Hash Table:
Index 0: []
Index 1: []
Index 2: []
Index 3: []
Index 4: []
Index 5: []
Index 6: []
Index 7: []
Index 8: []
Index 9: []
hashCode Collisions: 0
hashCode Hash Table:
Index 0: []
Index 1: []
Index 2: []
Index 3: []
Index 4: []
Index 5: []
Index 6: []
Index 7: []
Index 8: []
Index 9: []
Test case 4:
Input keys: [ghi]
Custom Hash Collisions: 0
Custom Hash Table:
Index 0: []
Index 1: []
Index 2: []
Index 3: []
Index 4: []
Index 5: []
Index 6: []
Index 7: [ghi: 1]
Index 8: []
Index 9: []
hashCode Collisions: 0
hashCode Hash Table:
Index 0: []
Index 1: []
Index 2: []
Index 3: []
Index 4: []
Index 5: []
Index 6: []
Index 7: [ghi: 1]
Index 8: []
Index 9: []
Test case 5:
Input keys: [aaa, aaa, aaa]
Custom Hash Collisions: 2
Custom Hash Table:
Index 0: []
Index 1: []
Index 2: []
Index 3: []
Index 4: []
Index 5: []
Index 6: []
Index 7: []
Index 8: []
Index 9: [aaa: 1]
hashCode Collisions: 2
hashCode Hash Table:
Index 0: []
Index 1: []
Index 2: []
Index 3: []
Index 4: []
Index 5: []
Index 6: []
Index 7: []
Index 8: []
Index 9: [aaa: 1]
Explanation:
- Test case 1: Custom hash causes 2 collisions ("abc"/"cba", "def"/"fed" at indices 2, 5);
hashCodespreads keys evenly, 0 collisions. - Test case 2: Both hash functions distribute keys without collisions.
- Test case 3: Empty set, no collisions for either.
- Test case 4: Single key, no collisions for either.
- Test case 5: Repeated keys cause 2 collisions for both (only one key stored due to duplicates).
How It Works
- Node: Stores a 3-character string key, integer value, and
nextpointer. - HashTable:
- Uses a fixed-size array (size 10) with linked lists for chaining.
customHash: Polynomial rolling hash (sum = sum * 31 + (char - 'a' + 1)), case-insensitive, modulo 10.hashCodeHash: Uses Java’shashCodemodulo 10.insert: Validates 3-character alphabetic key, computes index, incrementscollisionCountif index occupied, updates value for duplicates.getCollisionCount: Returns collision count.toString: Prints key-value pairs per index.
- Example Trace (Test case 1, custom hash):
- Insert "abc": index=2, table[2]=[abc:1], collisions=0.
- Insert "cba": index=2, table[2]=[cba:1,abc:1], collisions=1.
- Insert "def": index=5, table[5]=[def:1], collisions=1.
- Insert "fed": index=5, table[5]=[fed:1,def:1], collisions=2.
- Main Method: Tests anagrams, no collisions, empty set, single key, and duplicates, comparing collisions.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Insert | O(1) average | O(1) |
| Get Collision Count | O(1) | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of keys in the hash table.
- Time complexity: O(1) average for insert (hashing and list insertion); O(1) for getCollisionCount; O(n) for toString (iterate all keys).
- Space complexity: O(1) for insert and getCollisionCount; O(n) for toString (StringBuilder output).
- Worst case: O(n) time for insert (many keys in one index), O(n) space for toString.
✅ Tip: Design a custom hash function that leverages the key’s structure (e.g., fixed 3-character strings) to distribute keys evenly. A polynomial rolling hash works well for strings by combining character positions.
⚠ Warning: Validate input keys to ensure they match the expected format (3 alphabetic characters). Poor hash functions may lead to excessive collisions, degrading performance to O(n) in worst cases.
Phone Book Application
Problem Statement
Write a Java program that implements a phone book using a hash table, with contact names as keys and phone numbers as values. The program should support operations to insert a contact (name and phone number), look up a phone number by name, and delete a contact by name. The hash table should handle case-sensitive names and return appropriate messages for operations on non-existent contacts or duplicate insertions. Test the implementation with sequences of operations, including cases with duplicate names, non-existent contacts, and empty phone books. You can visualize this as a digital phone book where you can add, find, or remove a person’s contact details quickly using their name.
Input:
- A sequence of operations, where each operation is:
insert(name, phoneNumber): Add a contact, return "Added" or "Name already exists".lookup(name): Return the phone number or "Not found" if the name doesn’t exist.delete(name): Remove the contact, return "Deleted" or "Not found".printPhoneBook(): Print all contacts or "Empty" if none exist. Output: For each operation, print the action performed and its result (e.g., "Added John", "Found: 123-456-7890", "Deleted John", or "Empty"). Constraints:
- The phone book size is between 0 and 10^5 contacts.
- Names and phone numbers are non-empty strings of up to 100 characters.
- Names are case-sensitive (e.g., "John" and "john" are distinct).
- Phone numbers are strings (e.g., "123-456-7890", no specific format required). Example:
- Input: Operations = [insert("John", "123-456-7890"), insert("Jane", "987-654-3210"), lookup("John"), delete("Jane"), printPhoneBook]
- Output:
Added John Added Jane Found: 123-456-7890 Deleted Jane Phone Book: John: 123-456-7890 - Input: Operations = [lookup("John"), printPhoneBook]
- Output:
Not found Empty
Pseudocode
CLASS PhoneBook
SET hashTable to new HashMap
FUNCTION insert(name, phoneNumber)
IF hashTable contains name THEN
RETURN "Name already exists"
ENDIF
SET hashTable[name] to phoneNumber
RETURN "Added " + name
ENDFUNCTION
FUNCTION lookup(name)
IF hashTable contains name THEN
RETURN "Found: " + hashTable[name]
ENDIF
RETURN "Not found"
ENDFUNCTION
FUNCTION delete(name)
IF hashTable contains name THEN
REMOVE hashTable[name]
RETURN "Deleted " + name
ENDIF
RETURN "Not found"
ENDFUNCTION
FUNCTION toString()
IF hashTable is empty THEN
RETURN "Empty"
ENDIF
CREATE result as new StringBuilder
FOR each entry in hashTable
APPEND entry.key and ": " and entry.value to result
IF not last entry THEN
APPEND ", " to result
ENDIF
ENDFOR
RETURN result as string
ENDFUNCTION
ENDCLASS
FUNCTION testPhoneBook(operations)
CREATE phoneBook as new PhoneBook
FOR each operation in operations
IF operation.type equals "insert" THEN
CALL phoneBook.insert(operation.name, operation.phoneNumber)
PRINT result
ELSE IF operation.type equals "lookup" THEN
CALL phoneBook.lookup(operation.name)
PRINT result
ELSE IF operation.type equals "delete" THEN
CALL phoneBook.delete(operation.name)
PRINT result
ELSE IF operation.type equals "print" THEN
PRINT "Phone Book: " + phoneBook.toString
ENDIF
ENFOR
ENDFUNCTION
FUNCTION main()
SET testCases to array of operation sequences
FOR each testCase in testCases
PRINT test case details
CALL testPhoneBook(testCase)
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
PhoneBookclass with: a. AHashMap<String, String>to store name-phone number pairs. b.insert: Add contact if name doesn’t exist, return status. c.lookup: Return phone number or "Not found". d.delete: Remove contact by name, return status. e.toString: Return comma-separated string of contacts or "Empty". - In
testPhoneBook: a. Create aPhoneBookinstance. b. For each operation:insert: Callinsert, print result.lookup: Calllookup, print result.delete: Calldelete, print result.print: CalltoString, print phone book.
- In
main, test with sequences including: a. Normal operations (insert, lookup, delete). b. Empty phone book operations. c. Duplicate name insertion. d. Lookup and delete non-existent contacts. e. Single contact operations.
Java Implementation
import java.util.*;
public class PhoneBookApplication {
// PhoneBook class to manage contacts
static class PhoneBook {
private HashMap<String, String> hashTable;
public PhoneBook() {
hashTable = new HashMap<>();
}
public String insert(String name, String phoneNumber) {
if (hashTable.containsKey(name)) {
return "Name already exists";
}
hashTable.put(name, phoneNumber);
return "Added " + name;
}
public String lookup(String name) {
if (hashTable.containsKey(name)) {
return "Found: " + hashTable.get(name);
}
return "Not found";
}
public String delete(String name) {
if (hashTable.containsKey(name)) {
hashTable.remove(name);
return "Deleted " + name;
}
return "Not found";
}
public String toString() {
if (hashTable.isEmpty()) {
return "Empty";
}
StringBuilder result = new StringBuilder();
int index = 0;
for (Map.Entry<String, String> entry : hashTable.entrySet()) {
result.append(entry.getKey()).append(": ").append(entry.getValue());
if (index < hashTable.size() - 1) {
result.append(", ");
}
index++;
}
return result.toString();
}
}
// Helper class for operations
static class Operation {
String type;
String name;
String phoneNumber;
Operation(String type, String name, String phoneNumber) {
this.type = type;
this.name = name;
this.phoneNumber = phoneNumber;
}
}
// Tests phone book operations
public void testPhoneBook(List<Operation> operations) {
PhoneBook phoneBook = new PhoneBook();
for (Operation op : operations) {
if (op.type.equals("insert")) {
System.out.println(phoneBook.insert(op.name, op.phoneNumber));
} else if (op.type.equals("lookup")) {
System.out.println(phoneBook.lookup(op.name));
} else if (op.type.equals("delete")) {
System.out.println(phoneBook.delete(op.name));
} else if (op.type.equals("print")) {
System.out.println("Phone Book: " + phoneBook.toString());
}
}
}
// Main method to test phone book
public static void main(String[] args) {
PhoneBookApplication manager = new PhoneBookApplication();
// Test cases
List<List<Operation>> testCases = new ArrayList<>();
// Test case 1: Normal operations
testCases.add(Arrays.asList(
new Operation("insert", "John", "123-456-7890"),
new Operation("insert", "Jane", "987-654-3210"),
new Operation("lookup", "John", null),
new Operation("delete", "Jane", null),
new Operation("print", null, null)
));
// Test case 2: Empty phone book
testCases.add(Arrays.asList(
new Operation("lookup", "John", null),
new Operation("delete", "John", null),
new Operation("print", null, null)
));
// Test case 3: Duplicate name
testCases.add(Arrays.asList(
new Operation("insert", "Alice", "111-222-3333"),
new Operation("insert", "Alice", "444-555-6666"),
new Operation("print", null, null)
));
// Test case 4: Non-existent contact
testCases.add(Arrays.asList(
new Operation("insert", "Bob", "222-333-4444"),
new Operation("lookup", "Charlie", null),
new Operation("delete", "Charlie", null),
new Operation("print", null, null)
));
// Test case 5: Single contact
testCases.add(Arrays.asList(
new Operation("insert", "Eve", "555-666-7777"),
new Operation("lookup", "Eve", null),
new Operation("delete", "Eve", null),
new Operation("print", null, null)
));
// Run test cases
for (int i = 0; i < testCases.size(); i++) {
System.out.println("Test case " + (i + 1) + ":");
manager.testPhoneBook(testCases.get(i));
System.out.println();
}
}
}
Output
Running the main method produces:
Test case 1:
Added John
Added Jane
Found: 123-456-7890
Deleted Jane
Phone Book: John: 123-456-7890
Test case 2:
Not found
Not found
Phone Book: Empty
Test case 3:
Added Alice
Name already exists
Phone Book: Alice: 111-222-3333
Test case 4:
Added Bob
Not found
Not found
Phone Book: Bob: 222-333-4444
Test case 5:
Added Eve
Found: 555-666-7777
Deleted Eve
Phone Book: Empty
Explanation:
- Test case 1: Adds John and Jane, looks up John, deletes Jane, prints remaining contact.
- Test case 2: Attempts lookup and delete on empty phone book, prints "Empty".
- Test case 3: Adds Alice, tries to add Alice again (fails), prints single contact.
- Test case 4: Adds Bob, tries lookup and delete for non-existent Charlie, prints Bob’s contact.
- Test case 5: Adds Eve, looks up Eve, deletes Eve, prints empty phone book.
How It Works
- PhoneBook:
- Uses
HashMap<String, String>to store name-phone number pairs. insert: Adds contact if name doesn’t exist, returns status.lookup: Returns phone number or "Not found".delete: Removes contact if name exists, returns status.toString: Returns comma-separated string of contacts or "Empty".
- Uses
- testPhoneBook: Executes operations, printing results.
- Example Trace (Test case 1):
- insert("John", "123-456-7890"): hashTable={John:123-456-7890}, returns "Added John".
- insert("Jane", "987-654-3210"): hashTable={John:123-456-7890, Jane:987-654-3210}, returns "Added Jane".
- lookup("John"): Returns "Found: 123-456-7890".
- delete("Jane"): Removes Jane, hashTable={John:123-456-7890}, returns "Deleted Jane".
- print: Returns "John: 123-456-7890".
- Main Method: Tests normal operations, empty phone book, duplicate names, non-existent contacts, and single contact.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Insert | O(1) average | O(1) |
| Lookup | O(1) average | O(1) |
| Delete | O(1) average | O(1) |
| To String | O(n) | O(n) |
Note:
- n is the number of contacts in the phone book.
- Time complexity: O(1) average for insert, lookup, delete (HashMap operations); O(n) for toString (iterate all entries).
- Space complexity: O(1) for insert, lookup, delete (constant space); O(n) for toString (StringBuilder output).
- Worst case: O(n) time and space for toString with large phone books.
✅ Tip: Use a
HashMapfor fast O(1) average-case operations. Ensure names are treated case-sensitively to avoid confusion between similar names (e.g., "John" vs. "john").
⚠ Warning: Check for duplicate names before insertion to prevent overwriting existing contacts. Handle empty phone book cases to avoid null pointer issues during lookup or deletion.
Two Sum Problem
Problem Statement
Write a Java program that uses a hash table to solve the Two Sum problem: given an array of integers and a target sum, find two numbers in the array that add up to the target sum and return their indices. The hash table should store numbers as keys and their indices as values. The solution should assume each input has at most one valid pair of indices and that no number can be used twice. Test the implementation with different arrays and target sums, including cases with no solution, valid solutions, and edge cases like empty arrays or arrays with fewer than two elements. You can visualize this as searching a list of numbers to find a pair of puzzle pieces that fit together to match a specific total.
Input:
- An array of integers and a target sum (integer). Output: An array of two integers representing the indices of the two numbers that sum to the target, or an empty array if no solution exists. Constraints:
- The array length is between 0 and 10^5.
- Array elements and the target sum are integers in the range [-10^9, 10^9].
- Each input has at most one valid solution.
- The same element cannot be used twice. Example:
- Input: Array = [2, 7, 11, 15], Target = 9
- Output: [0, 1]
- Explanation: 2 + 7 = 9, indices 0 and 1.
- Input: Array = [3, 2, 4], Target = 8
- Output: []
- Explanation: No pair sums to 8.
- Input: Array = [], Target = 5
- Output: []
- Explanation: Empty array, no solution.
Pseudocode
FUNCTION twoSum(numbers, target)
CREATE hashTable as new HashMap
FOR i from 0 to numbers length - 1
SET complement to target - numbers[i]
IF hashTable contains complement THEN
RETURN array of [hashTable[complement], i]
ENDIF
SET hashTable[numbers[i]] to i
ENDFOR
RETURN empty array
ENDFUNCTION
FUNCTION main()
SET testCases to array of (numbers, target) pairs
FOR each testCase in testCases
PRINT test case details
CALL twoSum(testCase.numbers, testCase.target)
PRINT result
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
twoSum: a. Create aHashMap<Integer, Integer>to store numbers and their indices. b. For each number at index i:- Compute complement = target - number.
- If complement exists in hash table, return [hashTable[complement], i].
- Otherwise, add number and its index to hash table. c. If no solution is found, return empty array.
- In
main, test with: a. Arrays with a valid solution. b. Arrays with no solution. c. Empty array. d. Array with one element. e. Array with duplicate numbers.
Java Implementation
import java.util.*;
public class TwoSumProblem {
// Solves the Two Sum problem using a hash table
public int[] twoSum(int[] numbers, int target) {
HashMap<Integer, Integer> hashTable = new HashMap<>();
for (int i = 0; i < numbers.length; i++) {
int complement = target - numbers[i];
if (hashTable.containsKey(complement)) {
return new int[] {hashTable.get(complement), i};
}
hashTable.put(numbers[i], i);
}
return new int[] {};
}
// Main method to test Two Sum
public static void main(String[] args) {
TwoSumProblem solver = new TwoSumProblem();
// Test cases
Object[] testCases = {
new Object[] {new int[] {2, 7, 11, 15}, 9}, // Valid solution
new Object[] {new int[] {3, 2, 4}, 8}, // No solution
new Object[] {new int[] {}, 5}, // Empty array
new Object[] {new int[] {1}, 2}, // Single element
new Object[] {new int[] {3, 3}, 6} // Duplicate numbers
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
int[] numbers = (int[]) ((Object[]) testCases[i])[0];
int target = (int) ((Object[]) testCases[i])[1];
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input array: " + Arrays.toString(numbers));
System.out.println("Target sum: " + target);
int[] result = solver.twoSum(numbers, target);
System.out.println("Result: " + Arrays.toString(result) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input array: [2, 7, 11, 15]
Target sum: 9
Result: [0, 1]
Test case 2:
Input array: [3, 2, 4]
Target sum: 8
Result: []
Test case 3:
Input array: []
Target sum: 5
Result: []
Test case 4:
Input array: [1]
Target sum: 2
Result: []
Test case 5:
Input array: [3, 3]
Target sum: 6
Result: [0, 1]
Explanation:
- Test case 1: 2 + 7 = 9, returns indices [0, 1].
- Test case 2: No pair sums to 8, returns [].
- Test case 3: Empty array, returns [].
- Test case 4: Single element, no pair possible, returns [].
- Test case 5: 3 + 3 = 6, returns indices [0, 1].
How It Works
- twoSum:
- Uses a
HashMap<Integer, Integer>to store numbers and their indices. - For each number, checks if complement (target - number) exists in hash table.
- If found, returns indices of complement and current number.
- Otherwise, adds current number and index to hash table.
- Returns empty array if no solution is found.
- Uses a
- Example Trace (Test case 1):
- Input: [2, 7, 11, 15], target=9.
- i=0: complement=9-2=7, hashTable={}, add {2:0}.
- i=1: complement=9-7=2, hashTable={2:0}, return [0, 1].
- Main Method: Tests valid solution, no solution, empty array, single element, and duplicates.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Two Sum | O(n) | O(n) |
Note:
- n is the length of the input array.
- Time complexity: O(n) for single pass through array; HashMap operations (put, get) are O(1) average case.
- Space complexity: O(n) for storing up to n numbers in HashMap.
- Worst case: O(n) time and space when all numbers are processed.
✅ Tip: Use a single-pass approach with a hash table to achieve O(n) time complexity, storing numbers and indices to quickly find the complement.
⚠ Warning: Ensure the solution handles edge cases like empty arrays or arrays with fewer than two elements. Avoid using the same element twice by checking the complement before adding the current number.
Word Frequency Counter
Problem Statement
Write a Java program that uses a hash table to count the frequency of words in a given text. The hash table should use words (case-insensitive) as keys and their frequency counts as values. The program should process the input text by splitting it into words, ignoring punctuation, and print the word-frequency pairs in a readable format. Test the implementation with different text inputs, including empty text, single-word text, and text with repeated words. You can visualize this as analyzing a book page to count how often each word appears, storing the results in a dictionary-like structure for quick lookup.
Input:
- A string of text containing words separated by spaces, punctuation, or other delimiters. Output: A string representation of the hash table’s word-frequency pairs, or "Empty" if no words are present. Constraints:
- The text length is between 0 and 10^5 characters.
- Words are sequences of alphabetic characters (a-z, A-Z).
- Punctuation and non-alphabetic characters are ignored.
- Words are case-insensitive (e.g., "The" and "the" are the same). Example:
- Input: Text = "The quick brown fox, the quick!"
- Output: "the: 2, quick: 2, brown: 1, fox: 1"
- Explanation: Words are counted case-insensitively, ignoring punctuation.
- Input: Text = ""
- Output: "Empty"
- Explanation: No words in empty text.
- Input: Text = "hello Hello HELLO"
- Output: "hello: 3"
- Explanation: Case-insensitive, all "hello" variants count as one word.
Pseudocode
FUNCTION countWordFrequencies(text)
CREATE hashTable as new HashMap
IF text is empty THEN
RETURN "Empty"
ENDIF
SET words to split text by non-alphabetic characters
FOR each word in words
IF word is not empty THEN
SET word to lowercase(word)
IF hashTable contains word THEN
INCREMENT hashTable[word] by 1
ELSE
SET hashTable[word] to 1
ENDIF
ENDIF
ENDFOR
IF hashTable is empty THEN
RETURN "Empty"
ENDIF
CREATE result as new StringBuilder
FOR each entry in hashTable
APPEND entry.key and ":" and entry.value to result
IF not last entry THEN
APPEND ", " to result
ENDIF
ENDFOR
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of text inputs
FOR each testCase in testCases
PRINT test case details
CALL countWordFrequencies(testCase.text)
PRINT word frequencies
ENFOR
ENDFUNCTION
Algorithm Steps
- Define
countWordFrequencies: a. Create aHashMap<String, Integer>to store word frequencies. b. If text is empty, return "Empty". c. Split text into words using non-alphabetic characters as delimiters. d. For each non-empty word:- Convert to lowercase for case-insensitive counting.
- If word exists in hash table, increment its count.
- Otherwise, add word with count 1. e. If hash table is empty, return "Empty". f. Build a string of word-frequency pairs, separated by commas.
- In
main, test with different texts: a. Normal text with repeated words. b. Empty text. c. Single-word text. d. Text with punctuation and mixed case. e. Text with only non-alphabetic characters.
Java Implementation
import java.util.*;
public class WordFrequencyCounter {
// Counts word frequencies in the given text
public String countWordFrequencies(String text) {
HashMap<String, Integer> hashTable = new HashMap<>();
if (text == null || text.trim().isEmpty()) {
return "Empty";
}
// Split text into words, ignoring non-alphabetic characters
String[] words = text.split("[^a-zA-Z]+");
for (String word : words) {
if (!word.isEmpty()) {
word = word.toLowerCase();
hashTable.put(word, hashTable.getOrDefault(word, 0) + 1);
}
}
if (hashTable.isEmpty()) {
return "Empty";
}
// Build result string
StringBuilder result = new StringBuilder();
int index = 0;
for (Map.Entry<String, Integer> entry : hashTable.entrySet()) {
result.append(entry.getKey()).append(": ").append(entry.getValue());
if (index < hashTable.size() - 1) {
result.append(", ");
}
index++;
}
return result.toString();
}
// Main method to test word frequency counter
public static void main(String[] args) {
WordFrequencyCounter counter = new WordFrequencyCounter();
// Test cases
String[] testCases = {
"The quick brown fox, the quick!", // Normal text with repeats
"", // Empty text
"hello Hello HELLO", // Case-insensitive repeats
"hello", // Single word
"!!! --- 123", // No words
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Input text: \"" + testCases[i] + "\"");
String result = counter.countWordFrequencies(testCases[i]);
System.out.println("Word frequencies: " + result + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input text: "The quick brown fox, the quick!"
Word frequencies: the: 2, quick: 2, brown: 1, fox: 1
Test case 2:
Input text: ""
Word frequencies: Empty
Test case 3:
Input text: "hello Hello HELLO"
Word frequencies: hello: 3
Test case 4:
Input text: "hello"
Word frequencies: hello: 1
Test case 5:
Input text: "!!! --- 123"
Word frequencies: Empty
Explanation:
- Test case 1: Counts "the" (2), "quick" (2), "brown" (1), "fox" (1), ignoring punctuation.
- Test case 2: Empty text returns "Empty".
- Test case 3: Counts "hello" (3) case-insensitively.
- Test case 4: Single word "hello" has frequency 1.
- Test case 5: No alphabetic words, returns "Empty".
How It Works
- countWordFrequencies:
- Creates a
HashMap<String, Integer>for word frequencies. - Handles null or empty text by returning "Empty".
- Splits text using regex
[^a-zA-Z]+to ignore non-alphabetic characters. - Processes each non-empty word:
- Converts to lowercase.
- Updates count in hash table using
getOrDefault.
- Builds a comma-separated string of word-frequency pairs or returns "Empty" if none.
- Creates a
- Example Trace (Test case 1):
- Input: "The quick brown fox, the quick!".
- Split: ["The", "quick", "brown", "fox", "the", "quick"].
- Process: hashTable={the:2, quick:2, brown:1, fox:1}.
- Output: "the: 2, quick: 2, brown: 1, fox: 1".
- Main Method: Tests normal text, empty text, repeated words, single word, and non-alphabetic text.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Count Frequencies | O(n) | O(m) |
Note:
- n is the length of the input text (characters).
- m is the number of unique words.
- Time complexity: O(n) for splitting and processing text; HashMap operations (put, get) are O(1) average case.
- Space complexity: O(m) for HashMap storing unique words; O(n) for output string in worst case.
- Worst case: O(n) time, O(n) space for many unique words or long output.
✅ Tip: Use a regular expression like
[^a-zA-Z]+to split text and handle punctuation effectively. Convert words to lowercase to ensure case-insensitive counting.
⚠ Warning: Handle edge cases like empty text or non-alphabetic input to avoid empty hash tables. Ensure regex splitting correctly separates words to prevent invalid tokens.
Trees Problem Solving with DSA
🛠 What you will learn
This section outlines the format used for each DSA problem statement, designed to guide students through solving exercise problems in a clear, structured learning process. Each component serves a specific purpose to enhance understanding and practical application:
- Problem Statement: Clearly defines the task, including inputs, outputs, constraints, and an example. A real-world analogy makes the problem relatable, aligning with the engaging tone of the "Core Data Structures" summary, ensuring students grasp the problem’s context.
- Pseudocode: Provides a high-level, language-agnostic outline of the solution using standardized syntax (e.g.,
FUNCTION,IF,SET,RETURN). This helps students understand the logic before diving into code, bridging theory and implementation. - Algorithm Steps: Breaks down the pseudocode into detailed, actionable steps, connecting theoretical logic to practical coding. This ensures students can follow the implementation process systematically.
- Java Implementation: Offers a complete, commented Java code solution that students can study and run. It includes a
mainmethod with at least four test cases (e.g., target present, absent, middle element, and edge cases like duplicates or single elements) to verify correctness and explore different scenarios, maintaining a beginner-friendly approach. - Output: Shows the expected results from the test cases, including indices, comparisons, and execution times (where applicable), helping students verify their code’s correctness and understand the algorithm’s behavior.
- How It Works: Traces the algorithm’s execution with a detailed example, showing step-by-step how the search narrows down the range. This reinforces understanding by illustrating the algorithm in action.
- Complexity Analysis Table: Summarizes time and space complexity for best, average, and worst cases, teaching students to evaluate efficiency and compare trade-offs across implementations.
- Tip or Warning Box: Provides practical advice (e.g., when to use DSA) and highlights pitfalls (e.g., ensuring the array is sorted), guiding students toward best practices and common error avoidance.
Each exercise includes test cases to verify correctness and performance, aligning with a learning platform featuring embedded compilers for hands-on practice and visualizations to illustrate the halving process of DSA.
BST Validator
Problem Statement
Write a Java program that checks if a given tree is a valid Binary Search Tree (BST). A BST is valid if, for each node, all values in its left subtree are less than the node’s value, and all values in its right subtree are greater than the node’s value. The program should implement a method to validate the tree and test it with at least three different trees, including valid and invalid BSTs, and edge cases like a single node or empty tree. You can visualize this as inspecting a family tree where each parent’s value must be greater than all their left descendants and less than all their right descendants, ensuring the hierarchy follows strict rules.
Input:
- A binary tree represented by nodes with integer values. Output: A boolean indicating whether the tree is a valid BST, and a string representation of the tree for clarity. Constraints:
- The tree has between 0 and 10^5 nodes.
- Node values are integers in the range [-10^9, 10^9].
- Duplicate values are not allowed in the BST. Example:
- Input: Tree = [5, left: [3, left: [1], right: [4]], right: [7]]
- Output: true, "Preorder: 5 3 1 4 7"
- Explanation: All nodes satisfy the BST property.
- Input: Tree = [5, left: [3, left: [1], right: [6]], right: [7]]
- Output: false, "Preorder: 5 3 1 6 7"
- Explanation: Node 6 in the left subtree is greater than 5, violating the BST property.
- Input: Tree = []
- Output: true, "Empty"
- Explanation: An empty tree is a valid BST.
Pseudocode
CLASS Node
SET value to integer
SET left to Node (null by default)
SET right to Node (null by default)
ENDCLASS
FUNCTION isValidBST(root)
FUNCTION validate(node, min, max)
IF node is null THEN
RETURN true
ENDIF
IF node.value <= min OR node.value >= max THEN
RETURN false
ENDIF
RETURN validate(node.left, min, node.value) AND validate(node.right, node.value, max)
ENDFUNCTION
RETURN validate(root, negative infinity, positive infinity)
ENDFUNCTION
FUNCTION toString(root)
IF root is null THEN
RETURN "Empty"
ENDIF
CREATE result as new StringBuilder
FUNCTION preorder(node)
IF node is null THEN
RETURN
ENDIF
APPEND node.value and " " to result
CALL preorder(node.left)
CALL preorder(node.right)
ENDFUNCTION
CALL preorder(root)
RETURN "Preorder: " + result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of trees
FOR each testCase in testCases
PRINT test case details
CALL isValidBST(testCase.root)
PRINT result and tree using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define a
Nodeclass with: a. An integervalue. b.leftandrightpointers to child nodes. - Define
isValidBST: a. Use a helper functionvalidate(node, min, max)to check if each node’s value is within [min, max]. b. For each node:- If null, return true.
- If value is not in (min, max), return false.
- Recursively validate left subtree with updated max = node.value.
- Recursively validate right subtree with updated min = node.value. c. Call validate with initial min = negative infinity, max = positive infinity.
- Define
toString: a. If root is null, return "Empty". b. Perform preorder traversal, appending values to a string. - In
main, test with at least three trees: a. A valid BST. b. An invalid BST. c. An empty tree. d. A single-node tree.
Java Implementation
public class BSTValidator {
// Node class for the binary tree
static class Node {
int value;
Node left, right;
Node(int value) {
this.value = value;
this.left = null;
this.right = null;
}
}
// Validates if the tree is a BST
public boolean isValidBST(Node root) {
return validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean validate(Node node, long min, long max) {
if (node == null) {
return true;
}
if (node.value <= min || node.value >= max) {
return false;
}
return validate(node.left, min, node.value) && validate(node.right, node.value, max);
}
// Converts tree to string (preorder traversal)
public String toString(Node root) {
if (root == null) {
return "Empty";
}
StringBuilder result = new StringBuilder();
preorder(root, result);
return "Preorder: " + result.toString().trim();
}
private void preorder(Node node, StringBuilder result) {
if (node == null) {
return;
}
result.append(node.value).append(" ");
preorder(node.left, result);
preorder(node.right, result);
}
// Helper class for test cases
static class TestCase {
Node root;
TestCase(int[] values, int[][] edges) {
if (values.length == 0) {
root = null;
return;
}
Node[] nodes = new Node[values.length];
for (int i = 0; i < values.length; i++) {
nodes[i] = new Node(values[i]);
}
for (int[] edge : edges) {
int parent = edge[0], child = edge[1];
if (edge[2] == 0) {
nodes[parent].left = nodes[child];
} else {
nodes[parent].right = nodes[child];
}
}
root = nodes[0];
}
}
// Main method to test BST validator
public static void main(String[] args) {
BSTValidator validator = new BSTValidator();
// Test cases
TestCase[] testCases = {
// Valid BST: [5, left: [3, left: 1, right: 4], right: 7]
new TestCase(
new int[]{5, 3, 1, 4, 7},
new int[][]{{0, 1, 0}, {0, 4, 1}, {1, 2, 0}, {1, 3, 1}}
),
// Invalid BST: [5, left: [3, left: 1, right: 6], right: 7]
new TestCase(
new int[]{5, 3, 1, 6, 7},
new int[][]{{0, 1, 0}, {0, 4, 1}, {1, 2, 0}, {1, 3, 1}}
),
// Empty tree
new TestCase(new int[]{}, new int[][]{}),
// Single node
new TestCase(new int[]{10}, new int[][]{})
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Tree: " + validator.toString(testCases[i].root));
boolean isValid = validator.isValidBST(testCases[i].root);
System.out.println("Is Valid BST: " + isValid + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Tree: Preorder: 5 3 1 4 7
Is Valid BST: true
Test case 2:
Tree: Preorder: 5 3 1 6 7
Is Valid BST: false
Test case 3:
Tree: Empty
Is Valid BST: true
Test case 4:
Tree: Preorder: 10
Is Valid BST: true
Explanation:
- Test case 1: Tree [5, left: [3, left: 1, right: 4], right: 7] is valid (all left subtree values < node, all right > node).
- Test case 2: Tree [5, left: [3, left: 1, right: 6], right: 7] is invalid (6 > 5 in left subtree).
- Test case 3: Empty tree is valid by definition.
- Test case 4: Single node (10) is valid (no subtrees to violate).
How It Works
- Node: Stores an integer value and pointers to left and right children.
- isValidBST:
- Uses recursive helper
validatewith min and max bounds. - Checks if node value is within (min, max).
- Updates bounds for left (max = node.value) and right (min = node.value) subtrees.
- Returns true for null nodes, false if bounds violated.
- Uses recursive helper
- toString: Performs preorder traversal, returns space-separated values or "Empty".
- Example Trace (Test case 1):
- Root=5: validate(5, -∞, +∞) → true.
- Left=3: validate(3, -∞, 5) → true.
- Left=1: validate(1, -∞, 3) → true.
- Right=4: validate(4, 3, 5) → true.
- Right=7: validate(7, 5, +∞) → true.
- Result: true.
- Main Method: Tests valid BST, invalid BST, empty tree, and single node.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| isValidBST | O(n) | O(h) |
| toString | O(n) | O(h) |
Note:
- n is the number of nodes in the tree.
- h is the height of the tree (O(n) for skewed, O(log n) for balanced).
- Time complexity: O(n) for isValidBST and toString (visit each node once).
- Space complexity: O(h) for recursion stack in isValidBST and toString.
- Worst case: O(n) time and O(n) space for skewed trees.
✅ Tip: Use a range-based validation (min, max) to enforce the BST property recursively. Use
longfor bounds to handle edge cases with integer values nearInteger.MIN_VALUEorMAX_VALUE.
⚠ Warning: Ensure no duplicate values in the BST, as they violate the strict inequality required. Handle empty trees and single nodes explicitly to avoid incorrect results.
Height of BST
Problem Statement
Write a Java program that extends a Binary Search Tree (BST) implementation to include a method that computes the height of the tree. The height of a tree is the number of edges on the longest path from the root to a leaf, with an empty tree having a height of -1. The program should reuse the BST node structure and test the height computation with balanced and unbalanced trees, including edge cases like an empty tree and a single-node tree. You can visualize this as measuring the tallest branch of a family tree, counting the number of parent-child connections from the top to the deepest descendant.
Input:
- A binary search tree represented by nodes with integer values. Output: An integer representing the height of the tree, and a string representation of the tree for clarity. Constraints:
- The tree has between 0 and 10^5 nodes.
- Node values are integers in the range [-10^9, 10^9].
- Duplicate values are not allowed in the BST. Example:
- Input: Tree = [5, left: [3, left: [1], right: [4]], right: [7]] (balanced)
- Output: Height = 2, "Preorder: 5 3 1 4 7"
- Explanation: Longest path has 2 edges (e.g., 5→3→1).
- Input: Tree = [5, right: [7, right: [9, right: [10]]] (unbalanced, skewed)
- Output: Height = 3, "Preorder: 5 7 9 10"
- Explanation: Longest path has 3 edges (5→7→9→10).
- Input: Tree = []
- Output: Height = -1, "Empty"
- Explanation: Empty tree has height -1.
Pseudocode
CLASS Node
SET value to integer
SET left to Node (null by default)
SET right to Node (null by default)
ENDCLASS
FUNCTION getHeight(root)
IF root is null THEN
RETURN -1
ENDIF
SET leftHeight to getHeight(root.left)
SET rightHeight to getHeight(root.right)
RETURN maximum of leftHeight, rightHeight plus 1
ENDFUNCTION
FUNCTION toString(root)
IF root is null THEN
RETURN "Empty"
ENDIF
CREATE result as new StringBuilder
FUNCTION preorder(node)
IF node is null THEN
RETURN
ENDIF
APPEND node.value and " " to result
CALL preorder(node.left)
CALL preorder(node.right)
ENDFUNCTION
CALL preorder(root)
RETURN "Preorder: " + result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of trees
FOR each testCase in testCases
PRINT test case details
CALL getHeight(testCase.root)
PRINT height and tree using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Reuse the
Nodeclass with: a. An integervalue. b.leftandrightpointers to child nodes. - Define
getHeight: a. If root is null, return -1. b. Recursively compute the height of the left subtree. c. Recursively compute the height of the right subtree. d. Return the maximum of left and right heights plus 1. - Define
toString: a. If root is null, return "Empty". b. Perform preorder traversal, appending values to a string. - In
main, test with: a. A balanced BST (e.g., complete binary tree). b. An unbalanced BST (e.g., skewed tree). c. An empty tree. d. A single-node tree.
Java Implementation
public class HeightOfBST {
// Node class for the binary search tree
static class Node {
int value;
Node left, right;
Node(int value) {
this.value = value;
this.left = null;
this.right = null;
}
}
// Computes the height of the BST
public int getHeight(Node root) {
if (root == null) {
return -1;
}
int leftHeight = getHeight(root.left);
int rightHeight = getHeight(root.right);
return Math.max(leftHeight, rightHeight) + 1;
}
// Converts tree to string (preorder traversal)
public String toString(Node root) {
if (root == null) {
return "Empty";
}
StringBuilder result = new StringBuilder();
preorder(root, result);
return "Preorder: " + result.toString().trim();
}
private void preorder(Node node, StringBuilder result) {
if (node == null) {
return;
}
result.append(node.value).append(" ");
preorder(node.left, result);
preorder(node.right, result);
}
// Helper class for test cases
static class TestCase {
Node root;
TestCase(int[] values, int[][] edges) {
if (values.length == 0) {
root = null;
return;
}
Node[] nodes = new Node[values.length];
for (int i = 0; i < values.length; i++) {
nodes[i] = new Node(values[i]);
}
for (int[] edge : edges) {
int parent = edge[0], child = edge[1];
if (edge[2] == 0) {
nodes[parent].left = nodes[child];
} else {
nodes[parent].right = nodes[child];
}
}
root = nodes[0];
}
}
// Main method to test BST height
public static void main(String[] args) {
HeightOfBST heightCalculator = new HeightOfBST();
// Test cases
TestCase[] testCases = {
// Balanced BST: [5, left: [3, left: 1, right: 4], right: 7]
new TestCase(
new int[]{5, 3, 1, 4, 7},
new int[][]{{0, 1, 0}, {0, 4, 1}, {1, 2, 0}, {1, 3, 1}}
),
// Unbalanced BST: [5, right: [7, right: [9, right: 10]]]
new TestCase(
new int[]{5, 7, 9, 10},
new int[][]{{0, 1, 1}, {1, 2, 1}, {2, 3, 1}}
),
// Empty tree
new TestCase(new int[]{}, new int[][]{}),
// Single node
new TestCase(new int[]{10}, new int[][]{})
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Tree: " + heightCalculator.toString(testCases[i].root));
int height = heightCalculator.getHeight(testCases[i].root);
System.out.println("Height: " + height + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Tree: Preorder: 5 3 1 4 7
Height: 2
Test case 2:
Tree: Preorder: 5 7 9 10
Height: 3
Test case 3:
Tree: Empty
Height: -1
Test case 4:
Tree: Preorder: 10
Height: 0
Explanation:
- Test case 1: Balanced BST with root 5, height 2 (path 5→3→1 or 5→3→4).
- Test case 2: Unbalanced (skewed) BST, height 3 (path 5→7→9→10).
- Test case 3: Empty tree, height -1.
- Test case 4: Single node, height 0 (no edges).
How It Works
- Node: Stores an integer value and pointers to left and right children.
- getHeight:
- Returns -1 for null nodes (empty tree).
- Recursively computes heights of left and right subtrees.
- Returns maximum of left and right heights plus 1.
- toString: Performs preorder traversal, returns space-separated values or "Empty".
- Example Trace (Test case 1):
- Root=5: leftHeight = getHeight(3), rightHeight = getHeight(7).
- Node 3: leftHeight = getHeight(1) = 0, rightHeight = getHeight(4) = 0, return max(0, 0) + 1 = 1.
- Node 7: leftHeight = -1, rightHeight = -1, return max(-1, -1) + 1 = 0.
- Root: max(1, 0) + 1 = 2.
- Main Method: Tests balanced BST, unbalanced BST, empty tree, and single node.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| getHeight | O(n) | O(h) |
| toString | O(n) | O(h) |
Note:
- n is the number of nodes in the tree.
- h is the height of the tree (O(n) for skewed, O(log n) for balanced).
- Time complexity: O(n) for getHeight and toString (visit each node once).
- Space complexity: O(h) for recursion stack in getHeight and toString.
- Worst case: O(n) time and O(n) space for skewed trees.
✅ Tip: Use recursion to compute the height by taking the maximum of left and right subtree heights. Define empty tree height as -1 to align with edge-counting conventions.
⚠ Warning: Handle null nodes correctly to avoid null pointer exceptions. Be aware that unbalanced trees (e.g., skewed) have higher recursion stack space complexity.
Lowest Common Ancestor
Problem Statement
Write a Java program that extends a Binary Search Tree (BST) implementation to include a method that finds the lowest common ancestor (LCA) of two nodes given their values. The LCA is the deepest node that is an ancestor of both nodes. The program should reuse the BST node structure and test the LCA method with different pairs of values, including cases where both nodes exist, one or both don’t exist, and edge cases like empty trees or single-node trees. The BST property (left subtree values < node value < right subtree values) should be leveraged for efficiency. You can visualize this as finding the closest common ancestor in a family tree for two individuals, using the ordered structure to navigate efficiently.
Input:
- A BST represented by nodes with integer values.
- Two integer values,
value1andvalue2, representing the nodes to find the LCA for. Output: The value of the LCA node, or -1 if the LCA cannot be determined (e.g., one or both values don’t exist), and a string representation of the tree for clarity. Constraints: - The tree has between 0 and 10^5 nodes.
- Node values,
value1, andvalue2are integers in the range [-10^9, 10^9]. - Duplicate values are not allowed in the BST. Example:
- Input: Tree = [5, left: [3, left: [1], right: [4]], right: [7]], Values = [1, 4]
- Output: LCA = 3, "Preorder: 5 3 1 4 7"
- Explanation: Node 3 is the LCA of nodes 1 and 4 (deepest common ancestor).
- Input: Tree = [5, left: [3, left: [1], right: [4]], right: [7]], Values = [1, 6]
- Output: LCA = -1, "Preorder: 5 3 1 4 7"
- Explanation: Node 6 doesn’t exist, so no LCA.
- Input: Tree = [], Values = [1, 2]
- Output: LCA = -1, "Empty"
- Explanation: Empty tree, no LCA.
Pseudocode
CLASS Node
SET value to integer
SET left to Node (null by default)
SET right to Node (null by default)
ENDCLASS
FUNCTION findLCA(root, value1, value2)
FUNCTION exists(node, value)
IF node is null THEN
RETURN false
ENDIF
IF node.value equals value THEN
RETURN true
ENDIF
IF value < node.value THEN
RETURN exists(node.left, value)
ELSE
RETURN exists(node.right, value)
ENDIF
ENDFUNCTION
IF root is null OR NOT exists(root, value1) OR NOT exists(root, value2) THEN
RETURN -1
ENDIF
SET current to root
WHILE current is not null
IF value1 < current.value AND value2 < current.value THEN
SET current to current.left
ELSE IF value1 > current.value AND value2 > current.value THEN
SET current to current.right
ELSE
RETURN current.value
ENDIF
ENDWHILE
RETURN -1
ENDFUNCTION
FUNCTION toString(root)
IF root is null THEN
RETURN "Empty"
ENDIF
CREATE result as new StringBuilder
FUNCTION preorder(node)
IF node is null THEN
RETURN
ENDIF
APPEND node.value and " " to result
CALL preorder(node.left)
CALL preorder(node.right)
ENDFUNCTION
CALL preorder(root)
RETURN "Preorder: " + result as string trimmed
ENDFUNCTION
FUNCTION main()
SET testCases to array of (tree, value1, value2) pairs
FOR each testCase in testCases
PRINT test case details
CALL findLCA(testCase.root, testCase.value1, testCase.value2)
PRINT LCA and tree using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Reuse the
Nodeclass with: a. An integervalue. b.leftandrightpointers to child nodes. - Define
findLCA: a. Check if bothvalue1andvalue2exist in the tree using a helper functionexists. b. If the tree is empty or either value doesn’t exist, return -1. c. Iteratively traverse from the root:- If both values are less than the current node’s value, move to the left child.
- If both values are greater than the current node’s value, move to the right child.
- Otherwise, the current node is the LCA (values split or one matches the node). d. Return the LCA’s value, or -1 if not found.
- Define
toString: a. If root is null, return "Empty". b. Perform preorder traversal, appending values to a string. - In
main, test with: a. A pair where both nodes exist (LCA in subtree). b. A pair where one node doesn’t exist. c. An empty tree. d. A single-node tree with matching and non-matching pairs.
Java Implementation
public class LowestCommonAncestor {
// Node class for the binary search tree
static class Node {
int value;
Node left, right;
Node(int value) {
this.value = value;
this.left = null;
this.right = null;
}
}
// Finds the lowest common ancestor of two values
public int findLCA(Node root, int value1, int value2) {
// Helper function to check if a value exists in the BST
boolean exists(Node node, int value) {
if (node == null) {
return false;
}
if (node.value == value) {
return true;
}
if (value < node.value) {
return exists(node.left, value);
} else {
return exists(node.right, value);
}
}
// Check if both values exist
if (root == null || !exists(root, value1) || !exists(root, value2)) {
return -1;
}
// Iterative LCA search
Node current = root;
while (current != null) {
if (value1 < current.value && value2 < current.value) {
current = current.left;
} else if (value1 > current.value && value2 > current.value) {
current = current.right;
} else {
return current.value;
}
}
return -1;
}
// Converts tree to string (preorder traversal)
public String toString(Node root) {
if (root == null) {
return "Empty";
}
StringBuilder result = new StringBuilder();
preorder(root, result);
return "Preorder: " + result.toString().trim();
}
private void preorder(Node node, StringBuilder result) {
if (node == null) {
return;
}
result.append(node.value).append(" ");
preorder(node.left, result);
preorder(node.right, result);
}
// Helper class for test cases
static class TestCase {
Node root;
int value1, value2;
TestCase(int[] values, int[][] edges, int value1, int value2) {
this.value1 = value1;
this.value2 = value2;
if (values.length == 0) {
root = null;
return;
}
Node[] nodes = new Node[values.length];
for (int i = 0; i < values.length; i++) {
nodes[i] = new Node(values[i]);
}
for (int[] edge : edges) {
int parent = edge[0], child = edge[1];
if (edge[2] == 0) {
nodes[parent].left = nodes[child];
} else {
nodes[parent].right = nodes[child];
}
}
root = nodes[0];
}
}
// Main method to test LCA
public static void main(String[] args) {
LowestCommonAncestor lcaFinder = new LowestCommonAncestor();
// Test cases
TestCase[] testCases = {
// Both nodes exist: [5, left: [3, left: 1, right: 4], right: 7], values [1, 4]
new TestCase(
new int[]{5, 3, 1, 4, 7},
new int[][]{{0, 1, 0}, {0, 4, 1}, {1, 2, 0}, {1, 3, 1}},
1, 4
),
// One node doesn’t exist: [5, left: [3, left: 1, right: 4], right: 7], values [1, 6]
new TestCase(
new int[]{5, 3, 1, 4, 7},
new int[][]{{0, 1, 0}, {0, 4, 1}, {1, 2, 0}, {1, 3, 1}},
1, 6
),
// Empty tree: [], values [1, 2]
new TestCase(new int[]{}, new int[][]{}, 1, 2),
// Single node, matching pair: [10], values [10, 10]
new TestCase(new int[]{10}, new int[][]{}, 10, 10),
// Single node, non-matching pair: [10], values [10, 20]
new TestCase(new int[]{10}, new int[][]{}, 10, 20)
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Tree: " + lcaFinder.toString(testCases[i].root));
System.out.println("Values: [" + testCases[i].value1 + ", " + testCases[i].value2 + "]");
int lca = lcaFinder.findLCA(testCases[i].root, testCases[i].value1, testCases[i].value2);
System.out.println("LCA: " + lca + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Tree: Preorder: 5 3 1 4 7
Values: [1, 4]
LCA: 3
Test case 2:
Tree: Preorder: 5 3 1 4 7
Values: [1, 6]
LCA: -1
Test case 3:
Tree: Empty
Values: [1, 2]
LCA: -1
Test case 4:
Tree: Preorder: 10
Values: [10, 10]
LCA: 10
Test case 5:
Tree: Preorder: 10
Values: [10, 20]
LCA: -1
Explanation:
- Test case 1: LCA of 1 and 4 is 3 (deepest common ancestor).
- Test case 2: Node 6 doesn’t exist, so LCA is -1.
- Test case 3: Empty tree, LCA is -1.
- Test case 4: Both values are 10, LCA is 10 (same node).
- Test case 5: Node 20 doesn’t exist, LCA is -1.
How It Works
- Node: Stores an integer value and pointers to left and right children.
- findLCA:
- Uses
existshelper to verify bothvalue1andvalue2are in the tree. - If tree is empty or either value is missing, returns -1.
- Iteratively traverses: if both values are less than current node, go left; if both greater, go right; otherwise, current node is LCA.
- Uses
- toString: Performs preorder traversal, returns space-separated values or "Empty".
- Example Trace (Test case 1):
- Verify 1 and 4 exist: true.
- Root=5: 1 < 5, 4 < 5, go left.
- Node=3: 1 ≤ 3, 4 > 3, return 3 (split point).
- Main Method: Tests existing nodes, non-existing node, empty tree, single node with matching and non-matching pairs.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| findLCA | O(h) | O(h) |
| toString | O(n) | O(h) |
Note:
- n is the number of nodes in the tree.
- h is the height of the tree (O(n) for skewed, O(log n) for balanced).
- Time complexity: O(h) for findLCA (traverse to LCA and check existence); O(n) for toString (visit all nodes).
- Space complexity: O(h) for recursion stack in exists and toString.
- Worst case: O(n) time and O(n) space for skewed trees.
✅ Tip: Leverage the BST property to find the LCA efficiently by moving left or right based on both values’ relation to the current node. Verify node existence to handle invalid inputs.
⚠ Warning: Ensure both values exist in the tree before computing the LCA to avoid incorrect results. Handle edge cases like empty trees or identical values correctly.
Preorder and Postorder Traversals
Problem Statement
Write a Java program that extends a Binary Search Tree (BST) implementation to include methods for preorder (root, left, right) and postorder (left, right, root) traversals. The program should reuse the BST node structure and print the results of both traversals for sample trees, including balanced, unbalanced, empty, and single-node trees. The traversals should output node values in the respective orders as space-separated strings. You can visualize this as exploring a family tree in two different ways: preorder visits the parent before children, while postorder visits children before the parent, listing their names in the order visited.
Input:
- A BST represented by nodes with integer values. Output: Two strings representing the preorder and postorder traversals of the tree, each containing space-separated node values, or "Empty" for an empty tree. Constraints:
- The tree has between 0 and 10^5 nodes.
- Node values are integers in the range [-10^9, 10^9].
- Duplicate values are not allowed in the BST. Example:
- Input: Tree = [5, left: [3, left: [1], right: [4]], right: [7]]
- Output:
- Preorder: "5 3 1 4 7"
- Postorder: "1 4 3 7 5"
- Explanation: Preorder visits root first (5), then left (3, 1, 4), then right (7); postorder visits left (1, 4), then right (7), then root (5).
- Input: Tree = []
- Output:
- Preorder: "Empty"
- Postorder: "Empty"
- Explanation: Empty tree has no nodes to traverse.
Pseudocode
CLASS Node
SET value to integer
SET left to Node (null by default)
SET right to Node (null by default)
ENDCLASS
FUNCTION preorderTraversal(root)
IF root is null THEN
RETURN "Empty"
ENDIF
CREATE result as new StringBuilder
FUNCTION preorder(node)
IF node is null THEN
RETURN
ENDIF
APPEND node.value and " " to result
CALL preorder(node.left)
CALL preorder(node.right)
ENDFUNCTION
CALL preorder(root)
RETURN result as string trimmed
ENDFUNCTION
FUNCTION postorderTraversal(root)
IF root is null THEN
RETURN "Empty"
ENDIF
CREATE result as new StringBuilder
FUNCTION postorder(node)
IF node is null THEN
RETURN
ENDIF
CALL postorder(node.left)
CALL postorder(node.right)
APPEND node.value and " " to result
ENDFUNCTION
CALL postorder(root)
RETURN result as string trimmed
ENDFUNCTION
FUNCTION main()
SET testCases to array of trees
FOR each testCase in testCases
PRINT test case details
CALL preorderTraversal(testCase.root)
CALL postorderTraversal(testCase.root)
PRINT preorder and postorder results
ENDFOR
ENDFUNCTION
Algorithm Steps
- Reuse the
Nodeclass with: a. An integervalue. b.leftandrightpointers to child nodes. - Define
preorderTraversal: a. If root is null, return "Empty". b. Use a helper function to:- Append node value.
- Recurse on left subtree.
- Recurse on right subtree. c. Return trimmed result string.
- Define
postorderTraversal: a. If root is null, return "Empty". b. Use a helper function to:- Recurse on left subtree.
- Recurse on right subtree.
- Append node value. c. Return trimmed result string.
- In
main, test with: a. A balanced BST. b. An unbalanced (skewed) BST. c. An empty tree. d. A single-node tree.
Java Implementation
public class PreorderPostorderTraversals {
// Node class for the binary search tree
static class Node {
int value;
Node left, right;
Node(int value) {
this.value = value;
this.left = null;
this.right = null;
}
}
// Performs preorder traversal (root, left, right)
public String preorderTraversal(Node root) {
if (root == null) {
return "Empty";
}
StringBuilder result = new StringBuilder();
preorder(root, result);
return result.toString().trim();
}
private void preorder(Node node, StringBuilder result) {
if (node == null) {
return;
}
result.append(node.value).append(" ");
preorder(node.left, result);
preorder(node.right, result);
}
// Performs postorder traversal (left, right, root)
public String postorderTraversal(Node root) {
if (root == null) {
return "Empty";
}
StringBuilder result = new StringBuilder();
postorder(root, result);
return result.toString().trim();
}
private void postorder(Node node, StringBuilder result) {
if (node == null) {
return;
}
postorder(node.left, result);
postorder(node.right, result);
result.append(node.value).append(" ");
}
// Helper class for test cases
static class TestCase {
Node root;
TestCase(int[] values, int[][] edges) {
if (values.length == 0) {
root = null;
return;
}
Node[] nodes = new Node[values.length];
for (int i = 0; i < values.length; i++) {
nodes[i] = new Node(values[i]);
}
for (int[] edge : edges) {
int parent = edge[0], child = edge[1];
if (edge[2] == 0) {
nodes[parent].left = nodes[child];
} else {
nodes[parent].right = nodes[child];
}
}
root = nodes[0];
}
}
// Main method to test traversals
public static void main(String[] args) {
PreorderPostorderTraversals traverser = new PreorderPostorderTraversals();
// Test cases
TestCase[] testCases = {
// Balanced BST: [5, left: [3, left: 1, right: 4], right: 7]
new TestCase(
new int[]{5, 3, 1, 4, 7},
new int[][]{{0, 1, 0}, {0, 4, 1}, {1, 2, 0}, {1, 3, 1}}
),
// Unbalanced BST: [5, right: [7, right: [9, right: 10]]]
new TestCase(
new int[]{5, 7, 9, 10},
new int[][]{{0, 1, 1}, {1, 2, 1}, {2, 3, 1}}
),
// Empty tree
new TestCase(new int[]{}, new int[][]{}),
// Single node
new TestCase(new int[]{10}, new int[][]{})
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Preorder: " + traverser.preorderTraversal(testCases[i].root));
System.out.println("Postorder: " + traverser.postorderTraversal(testCases[i].root) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Preorder: 5 3 1 4 7
Postorder: 1 4 3 7 5
Test case 2:
Preorder: 5 7 9 10
Postorder: 10 9 7 5
Test case 3:
Preorder: Empty
Postorder: Empty
Test case 4:
Preorder: 10
Postorder: 10
Explanation:
- Test case 1: Balanced BST, preorder visits root (5), left (3, 1, 4), right (7); postorder visits left (1, 4), right (7), root (5).
- Test case 2: Unbalanced (skewed) BST, preorder visits 5, 7, 9, 10; postorder visits 10, 9, 7, 5.
- Test case 3: Empty tree, both traversals return "Empty".
- Test case 4: Single node, both traversals return "10".
How It Works
- Node: Stores an integer value and pointers to left and right children.
- preorderTraversal:
- Returns "Empty" for null root.
- Appends root value, recurses on left, then right.
- postorderTraversal:
- Returns "Empty" for null root.
- Recurses on left, then right, then appends root value.
- Example Trace (Test case 1):
- Preorder: Visit 5, left (3, left (1), right (4)), right (7) → "5 3 1 4 7".
- Postorder: Left (1, 4), right (7), root (5) → "1 4 3 7 5".
- Main Method: Tests balanced BST, unbalanced BST, empty tree, and single node.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| preorderTraversal | O(n) | O(h) |
| postorderTraversal | O(n) | O(h) |
Note:
- n is the number of nodes in the tree.
- h is the height of the tree (O(n) for skewed, O(log n) for balanced).
- Time complexity: O(n) for both traversals (visit each node once).
- Space complexity: O(h) for recursion stack.
- Worst case: O(n) time and O(n) space for skewed trees.
✅ Tip: Use recursive traversal methods for simplicity, as they naturally follow the tree’s structure. Preorder is useful for copying a tree, while postorder is useful for deleting a tree.
⚠ Warning: Handle null nodes to avoid null pointer exceptions. Ensure the output string is trimmed to remove trailing spaces for clean presentation.
Range Sum Query
Problem Statement
Write a Java program that uses a Binary Search Tree (BST) to find the sum of all node values within a given range (e.g., between 20 and 60, inclusive). The program should use inorder traversal to collect values, leveraging the BST property to optimize by pruning branches outside the range. The implementation should reuse the BST node structure and test the range sum query with various trees and ranges, including edge cases like empty trees, ranges with no values in the tree, and ranges covering all or none of the tree’s values. You can visualize this as summing the ages of family members in a family tree whose ages fall within a specific range, using the tree’s ordered structure to efficiently skip irrelevant branches.
Input:
- A BST represented by nodes with integer values.
- Two integers,
lowandhigh, defining the inclusive range [low, high]. Output: An integer representing the sum of all node values within [low, high], and a string representation of the tree for clarity. Constraints: - The tree has between 0 and 10^5 nodes.
- Node values,
low, andhighare integers in the range [-10^9, 10^9]. - Duplicate values are not allowed in the BST.
low≤high. Example:- Input: Tree = [5, left: [3, left: [1], right: [4]], right: [7]], Range = [2, 6]
- Output: Sum = 12, "Preorder: 5 3 1 4 7"
- Explanation: Values 3, 4, 5 are in [2, 6], sum = 3 + 4 + 5 = 12.
- Input: Tree = [5, left: [3, left: [1], right: [4]], right: [7]], Range = [8, 10]
- Output: Sum = 0, "Preorder: 5 3 1 4 7"
- Explanation: No values in [8, 10], sum = 0.
- Input: Tree = [], Range = [1, 5]
- Output: Sum = 0, "Empty"
- Explanation: Empty tree, sum = 0.
Pseudocode
CLASS Node
SET value to integer
SET left to Node (null by default)
SET right to Node (null by default)
ENDCLASS
FUNCTION rangeSum(root, low, high)
IF root is null THEN
RETURN 0
ENDIF
IF root.value < low THEN
RETURN rangeSum(root.right, low, high)
ENDIF
IF root.value > high THEN
RETURN rangeSum(root.left, low, high)
ENDIF
RETURN root.value + rangeSum(root.left, low, high) + rangeSum(root.right, low, high)
ENDFUNCTION
FUNCTION toString(root)
IF root is null THEN
RETURN "Empty"
ENDIF
CREATE result as new StringBuilder
FUNCTION preorder(node)
IF node is null THEN
RETURN
ENDIF
APPEND node.value and " " to result
CALL preorder(node.left)
CALL preorder(node.right)
ENDFUNCTION
CALL preorder(root)
RETURN "Preorder: " + result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of (tree, low, high) pairs
FOR each testCase in testCases
PRINT test case details
CALL rangeSum(testCase.root, testCase.low, testCase.high)
PRINT sum and tree using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Reuse the
Nodeclass with: a. An integervalue. b.leftandrightpointers to child nodes. - Define
rangeSum: a. If root is null, return 0. b. If root.value < low, skip left subtree, recurse on right. c. If root.value > high, skip right subtree, recurse on left. d. If root.value is in [low, high], include value and recurse on both subtrees. - Define
toString: a. If root is null, return "Empty". b. Perform preorder traversal, appending values to a string. - In
main, test with: a. A tree with values in the range. b. A tree with no values in the range. c. An empty tree. d. A range covering all values. e. A single-node tree.
Java Implementation
public class RangeSumQuery {
// Node class for the binary search tree
static class Node {
int value;
Node left, right;
Node(int value) {
this.value = value;
this.left = null;
this.right = null;
}
}
// Computes the sum of values in the range [low, high]
public int rangeSum(Node root, int low, int high) {
if (root == null) {
return 0;
}
if (root.value < low) {
return rangeSum(root.right, low, high);
}
if (root.value > high) {
return rangeSum(root.left, low, high);
}
return root.value + rangeSum(root.left, low, high) + rangeSum(root.right, low, high);
}
// Converts tree to string (preorder traversal)
public String toString(Node root) {
if (root == null) {
return "Empty";
}
StringBuilder result = new StringBuilder();
preorder(root, result);
return "Preorder: " + result.toString().trim();
}
private void preorder(Node node, StringBuilder result) {
if (node == null) {
return;
}
result.append(node.value).append(" ");
preorder(node.left, result);
preorder(node.right, result);
}
// Helper class for test cases
static class TestCase {
Node root;
int low, high;
TestCase(int[] values, int[][] edges, int low, int high) {
this.low = low;
this.high = high;
if (values.length == 0) {
root = null;
return;
}
Node[] nodes = new Node[values.length];
for (int i = 0; i < values.length; i++) {
nodes[i] = new Node(values[i]);
}
for (int[] edge : edges) {
int parent = edge[0], child = edge[1];
if (edge[2] == 0) {
nodes[parent].left = nodes[child];
} else {
nodes[parent].right = nodes[child];
}
}
root = nodes[0];
}
}
// Main method to test range sum query
public static void main(String[] args) {
RangeSumQuery query = new RangeSumQuery();
// Test cases
TestCase[] testCases = {
// Values in range: [5, left: [3, left: 1, right: 4], right: 7], range [2, 6]
new TestCase(
new int[]{5, 3, 1, 4, 7},
new int[][]{{0, 1, 0}, {0, 4, 1}, {1, 2, 0}, {1, 3, 1}},
2, 6
),
// No values in range: [5, left: [3, left: 1, right: 4], right: 7], range [8, 10]
new TestCase(
new int[]{5, 3, 1, 4, 7},
new int[][]{{0, 1, 0}, {0, 4, 1}, {1, 2, 0}, {1, 3, 1}},
8, 10
),
// Empty tree: [], range [1, 5]
new TestCase(new int[]{}, new int[][]{}, 1, 5),
// Range covers all: [5, left: [3, left: 1, right: 4], right: 7], range [0, 10]
new TestCase(
new int[]{5, 3, 1, 4, 7},
new int[][]{{0, 1, 0}, {0, 4, 1}, {1, 2, 0}, {1, 3, 1}},
0, 10
),
// Single node: [10], range [5, 15]
new TestCase(new int[]{10}, new int[][]{}, 5, 15)
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Tree: " + query.toString(testCases[i].root));
System.out.println("Range: [" + testCases[i].low + ", " + testCases[i].high + "]");
int sum = query.rangeSum(testCases[i].root, testCases[i].low, testCases[i].high);
System.out.println("Sum: " + sum + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Tree: Preorder: 5 3 1 4 7
Range: [2, 6]
Sum: 12
Test case 2:
Tree: Preorder: 5 3 1 4 7
Range: [8, 10]
Sum: 0
Test case 3:
Tree: Empty
Range: [1, 5]
Sum: 0
Test case 4:
Tree: Preorder: 5 3 1 4 7
Range: [0, 10]
Sum: 20
Test case 5:
Tree: Preorder: 10
Range: [5, 15]
Sum: 10
Explanation:
- Test case 1: Values 3, 4, 5 are in [2, 6], sum = 3 + 4 + 5 = 12.
- Test case 2: No values in [8, 10], sum = 0.
- Test case 3: Empty tree, sum = 0.
- Test case 4: All values (1, 3, 4, 5, 7) in [0, 10], sum = 1 + 3 + 4 + 5 + 7 = 20.
- Test case 5: Single node 10 in [5, 15], sum = 10.
How It Works
- Node: Stores an integer value and pointers to left and right children.
- rangeSum:
- Returns 0 for null nodes.
- If root.value < low, skips left subtree, recurses on right.
- If root.value > high, skips right subtree, recurses on left.
- If root.value in [low, high], includes value and recurses on both subtrees.
- toString: Performs preorder traversal, returns space-separated values or "Empty".
- Example Trace (Test case 1):
- Root=5: In [2, 6], sum = 5 + rangeSum(left, 2, 6) + rangeSum(right, 2, 6).
- Left=3: In [2, 6], sum = 3 + rangeSum(left=1, 2, 6) + rangeSum(right=4, 2, 6).
- Left=1: Not in [2, 6], skip left, sum = rangeSum(right=null, 2, 6) = 0.
- Right=4: In [2, 6], sum = 4 + rangeSum(null, 2, 6) + rangeSum(null, 2, 6) = 4.
- Right=7: > 6, skip right, sum = rangeSum(left=null, 2, 6) = 0.
- Total: 5 + (3 + 0 + 4) + 0 = 12.
- Main Method: Tests range with values, no values, empty tree, all values, and single node.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| rangeSum | O(n) worst case | O(h) |
| toString | O(n) | O(h) |
Note:
- n is the number of nodes in the tree.
- h is the height of the tree (O(n) for skewed, O(log n) for balanced).
- Time complexity: O(n) for rangeSum in worst case (all nodes in range); O(h) in best case (pruning skips subtrees).
- Space complexity: O(h) for recursion stack in rangeSum and toString.
- Worst case: O(n) time and O(n) space for skewed trees with all nodes in range.
✅ Tip: Leverage the BST property to prune branches outside the range, reducing unnecessary traversals. Use inorder traversal logic to ensure values are processed in sorted order.
⚠ Warning: Ensure the range is inclusive ([low, high]) and handle edge cases like empty trees or ranges outside the tree’s values to avoid incorrect sums.
Graphs Problem Solving with DSA
🛠 What you will learn
This section outlines the format used for each DSA problem statement, designed to guide students through solving exercise problems in a clear, structured learning process. Each component serves a specific purpose to enhance understanding and practical application:
- Problem Statement: Clearly defines the task, including inputs, outputs, constraints, and an example. A real-world analogy makes the problem relatable, aligning with the engaging tone of the "Core Data Structures" summary, ensuring students grasp the problem’s context.
- Pseudocode: Provides a high-level, language-agnostic outline of the solution using standardized syntax (e.g.,
FUNCTION,IF,SET,RETURN). This helps students understand the logic before diving into code, bridging theory and implementation. - Algorithm Steps: Breaks down the pseudocode into detailed, actionable steps, connecting theoretical logic to practical coding. This ensures students can follow the implementation process systematically.
- Java Implementation: Offers a complete, commented Java code solution that students can study and run. It includes a
mainmethod with at least four test cases (e.g., target present, absent, middle element, and edge cases like duplicates or single elements) to verify correctness and explore different scenarios, maintaining a beginner-friendly approach. - Output: Shows the expected results from the test cases, including indices, comparisons, and execution times (where applicable), helping students verify their code’s correctness and understand the algorithm’s behavior.
- How It Works: Traces the algorithm’s execution with a detailed example, showing step-by-step how the search narrows down the range. This reinforces understanding by illustrating the algorithm in action.
- Complexity Analysis Table: Summarizes time and space complexity for best, average, and worst cases, teaching students to evaluate efficiency and compare trade-offs across implementations.
- Tip or Warning Box: Provides practical advice (e.g., when to use DSA) and highlights pitfalls (e.g., ensuring the array is sorted), guiding students toward best practices and common error avoidance.
Each exercise includes test cases to verify correctness and performance, aligning with a learning platform featuring embedded compilers for hands-on practice and visualizations to illustrate the halving process of DSA.
Cycle Detection
Problem Statement
Write a Java program that extends an undirected graph implementation to detect if the graph contains a cycle using Depth-First Search (DFS). A cycle exists if there is a path that starts and ends at the same vertex, passing through at least one other vertex. The program should reuse the adjacency list representation and test cycle detection with cyclic and acyclic graphs, including edge cases like empty graphs and single-vertex graphs. You can visualize this as exploring a network of roads to check if you can loop back to a starting city without retracing steps, ensuring no dead ends create a false cycle.
Input:
- An undirected graph represented by an adjacency list, with vertices as integers (0 to n-1).
- The number of vertices
nand a list of edges (pairs of vertices). Output: A boolean indicating whether the graph contains a cycle, and a string representation of the graph’s adjacency list for clarity. Constraints: - The number of vertices
nis between 0 and 10^5. - Edges are pairs of integers [u, v] where 0 ≤ u, v < n.
- The graph is undirected (edge [u, v] implies [v, u]). Example:
- Input: n = 4, edges = [[0, 1], [1, 2], [2, 3], [3, 0]]
- Output: Has Cycle = true, "Adjacency List: {0=[1, 3], 1=[0, 2], 2=[1, 3], 3=[2, 0]}"
- Explanation: The graph has a cycle (0→1→2→3→0).
- Input: n = 4, edges = [[0, 1], [1, 2], [2, 3]]
- Output: Has Cycle = false, "Adjacency List: {0=[1], 1=[0, 2], 2=[1, 3], 3=[2]}"
- Explanation: The graph is a tree (acyclic).
- Input: n = 0, edges = []
- Output: Has Cycle = false, "Adjacency List: {}"
- Explanation: An empty graph has no cycles.
Pseudocode
FUNCTION createGraph(n, edges)
CREATE adjList as new HashMap
FOR i from 0 to n-1
SET adjList[i] to empty list
ENDFOR
FOR each edge [u, v] in edges
ADD v to adjList[u]
ADD u to adjList[v]
ENDFOR
RETURN adjList
ENDFUNCTION
FUNCTION hasCycle(adjList, n)
IF n is 0 THEN
RETURN false
ENDIF
CREATE visited as boolean array of size n, initialized to false
FUNCTION dfs(vertex, parent)
SET visited[vertex] to true
FOR each neighbor in adjList[vertex]
IF NOT visited[neighbor] THEN
IF dfs(neighbor, vertex) THEN
RETURN true
ENDIF
ELSE IF neighbor is not parent THEN
RETURN true
ENDIF
ENDFOR
RETURN false
ENDFUNCTION
FOR i from 0 to n-1
IF NOT visited[i] THEN
IF dfs(i, -1) THEN
RETURN true
ENDIF
ENDIF
ENDFOR
RETURN false
ENDFUNCTION
FUNCTION toString(adjList)
CREATE result as new StringBuilder
APPEND "Adjacency List: {" to result
FOR each vertex in adjList
APPEND vertex and "=" and adjList[vertex] to result
IF vertex is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "}" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of (n, edges) pairs
FOR each testCase in testCases
PRINT test case details
SET adjList to createGraph(testCase.n, testCase.edges)
CALL hasCycle(adjList, testCase.n)
PRINT cycle result and graph using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Reuse
createGraph: a. Create a HashMapadjListmapping vertices to lists of neighbors. b. Initialize empty lists for vertices 0 to n-1. c. Add edges bidirectionally (u→v, v→u). - Define
hasCycle: a. If n = 0, return false (empty graph has no cycles). b. Use DFS with a parent parameter to avoid false cycles:- Mark vertex as visited.
- For each neighbor, if unvisited, recurse; if visited and not parent, cycle found. c. Run DFS from each unvisited vertex to handle disconnected components. d. Return true if a cycle is found, false otherwise.
- Define
toString: a. Convert adjacency list to a string, e.g., "{0=[1, 3], 1=[0, 2], ...}". - In
main, test with: a. A cyclic graph. b. An acyclic graph (tree). c. An empty graph (n = 0). d. A single-vertex graph.
Java Implementation
import java.util.*;
public class CycleDetection {
// Creates adjacency list representation of the graph
private Map<Integer, List<Integer>> createGraph(int n, int[][] edges) {
Map<Integer, List<Integer>> adjList = new HashMap<>();
for (int i = 0; i < n; i++) {
adjList.put(i, new ArrayList<>());
}
for (int[] edge : edges) {
int u = edge[0], v = edge[1];
adjList.get(u).add(v);
adjList.get(v).add(u); // Undirected graph
}
return adjList;
}
// Checks if the graph contains a cycle using DFS
public boolean hasCycle(Map<Integer, List<Integer>> adjList, int n) {
if (n == 0) {
return false;
}
boolean[] visited = new boolean[n];
for (int i = 0; i < n; i++) {
if (!visited[i]) {
if (dfs(i, -1, adjList, visited)) {
return true;
}
}
}
return false;
}
private boolean dfs(int vertex, int parent, Map<Integer, List<Integer>> adjList, boolean[] visited) {
visited[vertex] = true;
for (int neighbor : adjList.get(vertex)) {
if (!visited[neighbor]) {
if (dfs(neighbor, vertex, adjList, visited)) {
return true;
}
} else if (neighbor != parent) {
return true; // Cycle found
}
}
return false;
}
// Converts graph to string (adjacency list)
public String toString(Map<Integer, List<Integer>> adjList) {
StringBuilder result = new StringBuilder("Adjacency List: {");
List<Integer> vertices = new ArrayList<>(adjList.keySet());
Collections.sort(vertices); // For consistent output
for (int i = 0; i < vertices.size(); i++) {
int vertex = vertices.get(i);
result.append(vertex).append("=").append(adjList.get(vertex));
if (i < vertices.size() - 1) {
result.append(", ");
}
}
result.append("}");
return result.toString();
}
// Helper class for test cases
static class TestCase {
int n;
int[][] edges;
TestCase(int n, int[][] edges) {
this.n = n;
this.edges = edges;
}
}
// Main method to test cycle detection
public static void main(String[] args) {
CycleDetection cycleDetector = new CycleDetection();
// Test cases
TestCase[] testCases = {
// Cyclic graph: 0-1-2-3-0
new TestCase(4, new int[][]{{0, 1}, {1, 2}, {2, 3}, {3, 0}}),
// Acyclic graph (tree): 0-1-2-3
new TestCase(4, new int[][]{{0, 1}, {1, 2}, {2, 3}}),
// Empty graph
new TestCase(0, new int[][]{}),
// Single vertex
new TestCase(1, new int[][]{})
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Vertices: " + testCases[i].n);
System.out.println("Edges: " + Arrays.deepToString(testCases[i].edges));
Map<Integer, List<Integer>> adjList = cycleDetector.createGraph(testCases[i].n, testCases[i].edges);
boolean hasCycle = cycleDetector.hasCycle(adjList, testCases[i].n);
System.out.println("Has Cycle: " + hasCycle);
System.out.println(cycleDetector.toString(adjList) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Vertices: 4
Edges: [[0, 1], [1, 2], [2, 3], [3, 0]]
Has Cycle: true
Adjacency List: {0=[1, 3], 1=[0, 2], 2=[1, 3], 3=[2, 0]}
Test case 2:
Vertices: 4
Edges: [[0, 1], [1, 2], [2, 3]]
Has Cycle: false
Adjacency List: {0=[1], 1=[0, 2], 2=[1, 3], 3=[2]}
Test case 3:
Vertices: 0
Edges: []
Has Cycle: false
Adjacency List: {}
Test case 4:
Vertices: 1
Edges: []
Has Cycle: false
Adjacency List: {0=[]}
Explanation:
- Test case 1: Graph has a cycle (0→1→2→3→0), so returns true.
- Test case 2: Graph is a tree (acyclic), so returns false.
- Test case 3: Empty graph has no cycles, returns false.
- Test case 4: Single vertex has no cycles, returns false.
How It Works
- createGraph: Builds an adjacency list using a HashMap, adding undirected edges (u→v, v→u).
- hasCycle:
- Returns false for n = 0 (empty graph).
- Runs DFS from each unvisited vertex to handle disconnected components.
- In DFS, marks vertex as visited, checks neighbors:
- If neighbor is unvisited, recurse.
- If neighbor is visited and not the parent, a cycle is found.
- Returns true if a cycle is detected, false otherwise.
- dfs: Tracks parent to avoid false cycles in undirected graphs.
- toString: Formats adjacency list as a string, sorting vertices for consistency.
- Example Trace (Test case 1):
- DFS from 0: Visit 0, explore 1 (parent=0), explore 2 (parent=1), explore 3 (parent=2).
- At 3, neighbor 0 is visited and not parent (2), cycle found.
- Main Method: Tests cyclic graph, acyclic graph, empty graph, and single vertex.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| createGraph | O(n + e) | O(n + e) |
| hasCycle | O(n + e) | O(n) |
| toString | O(n log n + e) | O(n + e) |
Note:
- n is the number of vertices, e is the number of edges.
- Time complexity: O(n + e) for createGraph (initialize lists and add edges); O(n + e) for hasCycle (DFS visits each vertex and edge once); O(n log n + e) for toString (sorting vertices).
- Space complexity: O(n + e) for createGraph and toString (adjacency list); O(n) for hasCycle (visited array and recursion stack).
- Worst case: O(n + e) time and space for dense graphs.
✅ Tip: Use DFS with a parent parameter to detect cycles in undirected graphs, ensuring visited neighbors that aren’t parents indicate a cycle. Start DFS from each unvisited vertex to handle disconnected components.
⚠ Warning: In undirected graphs, avoid false cycle detection by checking the parent vertex. Handle edge cases like empty graphs or single vertices to ensure correct results.
Graph Connectivity
Problem Statement
Write a Java program that implements a graph using an adjacency list and checks if the graph is connected, meaning all vertices are reachable from a starting vertex, using Depth-First Search (DFS). The graph is undirected, and connectivity is determined by checking if a DFS from any vertex visits all vertices. The program should test the connectivity check with various graphs, including connected, disconnected, empty, and single-vertex graphs. You can visualize this as exploring a network of cities connected by roads, ensuring you can travel from one city to all others.
Input:
- An undirected graph represented by an adjacency list, with vertices as integers (0 to n-1).
- The number of vertices
nand a list of edges (pairs of vertices). Output: A boolean indicating whether the graph is connected, and a string representation of the graph’s adjacency list for clarity. Constraints: - The number of vertices
nis between 0 and 10^5. - Edges are pairs of integers [u, v] where 0 ≤ u, v < n.
- The graph is undirected (edge [u, v] implies [v, u]). Example:
- Input: n = 5, edges = [[0, 1], [1, 2], [2, 3], [3, 4]]
- Output: Connected = true, "Adjacency List: {0=[1], 1=[0, 2], 2=[1, 3], 3=[2, 4], 4=[3]}"
- Explanation: All vertices are reachable from vertex 0.
- Input: n = 5, edges = [[0, 1], [2, 3]]
- Output: Connected = false, "Adjacency List: {0=[1], 1=[0], 2=[3], 3=[2], 4=[]}"
- Explanation: Vertex 4 is unreachable, so the graph is disconnected.
- Input: n = 0, edges = []
- Output: Connected = true, "Adjacency List: {}"
- Explanation: An empty graph is considered connected.
Pseudocode
FUNCTION createGraph(n, edges)
CREATE adjList as new HashMap
FOR i from 0 to n-1
SET adjList[i] to empty list
ENDFOR
FOR each edge [u, v] in edges
ADD v to adjList[u]
ADD u to adjList[v]
ENDFOR
RETURN adjList
ENDFUNCTION
FUNCTION isConnected(adjList, n)
IF n is 0 THEN
RETURN true
ENDIF
CREATE visited as boolean array of size n, initialized to false
FUNCTION dfs(vertex)
SET visited[vertex] to true
FOR each neighbor in adjList[vertex]
IF NOT visited[neighbor] THEN
CALL dfs(neighbor)
ENDIF
ENDFOR
ENDFUNCTION
CALL dfs(0)
FOR i from 0 to n-1
IF NOT visited[i] THEN
RETURN false
ENDIF
ENDFOR
RETURN true
ENDFUNCTION
FUNCTION toString(adjList)
CREATE result as new StringBuilder
APPEND "Adjacency List: {" to result
FOR each vertex in adjList
APPEND vertex and "=" and adjList[vertex] to result
IF vertex is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "}" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of (n, edges) pairs
FOR each testCase in testCases
PRINT test case details
SET adjList to createGraph(testCase.n, testCase.edges)
CALL isConnected(adjList, testCase.n)
PRINT connected result and graph using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
createGraph: a. Create a HashMapadjListmapping vertices to lists of neighbors. b. Initialize empty lists for vertices 0 to n-1. c. Add edges bidirectionally (u→v, v→u). - Define
isConnected: a. If n = 0, return true (empty graph is connected). b. Use DFS starting from vertex 0:- Mark vertex as visited.
- Recursively visit unvisited neighbors. c. Check if all vertices are visited; return false if any are not.
- Define
toString: a. Convert adjacency list to a string, e.g., "{0=[1], 1=[0, 2], ...}". - In
main, test with: a. A connected graph. b. A disconnected graph. c. An empty graph (n = 0). d. A single-vertex graph.
Java Implementation
import java.util.*;
public class GraphConnectivity {
// Creates adjacency list representation of the graph
private Map<Integer, List<Integer>> createGraph(int n, int[][] edges) {
Map<Integer, List<Integer>> adjList = new HashMap<>();
for (int i = 0; i < n; i++) {
adjList.put(i, new ArrayList<>());
}
for (int[] edge : edges) {
int u = edge[0], v = edge[1];
adjList.get(u).add(v);
adjList.get(v).add(u); // Undirected graph
}
return adjList;
}
// Checks if the graph is connected using DFS
public boolean isConnected(Map<Integer, List<Integer>> adjList, int n) {
if (n == 0) {
return true;
}
boolean[] visited = new boolean[n];
dfs(0, adjList, visited);
for (int i = 0; i < n; i++) {
if (!visited[i]) {
return false;
}
}
return true;
}
private void dfs(int vertex, Map<Integer, List<Integer>> adjList, boolean[] visited) {
visited[vertex] = true;
for (int neighbor : adjList.get(vertex)) {
if (!visited[neighbor]) {
dfs(neighbor, adjList, visited);
}
}
}
// Converts graph to string (adjacency list)
public String toString(Map<Integer, List<Integer>> adjList) {
StringBuilder result = new StringBuilder("Adjacency List: {");
List<Integer> vertices = new ArrayList<>(adjList.keySet());
Collections.sort(vertices); // For consistent output
for (int i = 0; i < vertices.size(); i++) {
int vertex = vertices.get(i);
result.append(vertex).append("=").append(adjList.get(vertex));
if (i < vertices.size() - 1) {
result.append(", ");
}
}
result.append("}");
return result.toString();
}
// Helper class for test cases
static class TestCase {
int n;
int[][] edges;
TestCase(int n, int[][] edges) {
this.n = n;
this.edges = edges;
}
}
// Main method to test graph connectivity
public static void main(String[] args) {
GraphConnectivity connectivityChecker = new GraphConnectivity();
// Test cases
TestCase[] testCases = {
// Connected graph: 0-1-2-3-4
new TestCase(5, new int[][]{{0, 1}, {1, 2}, {2, 3}, {3, 4}}),
// Disconnected graph: 0-1, 2-3, 4 isolated
new TestCase(5, new int[][]{{0, 1}, {2, 3}}),
// Empty graph
new TestCase(0, new int[][]{}),
// Single vertex
new TestCase(1, new int[][]{})
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Vertices: " + testCases[i].n);
System.out.println("Edges: " + Arrays.deepToString(testCases[i].edges));
Map<Integer, List<Integer>> adjList = connectivityChecker.createGraph(testCases[i].n, testCases[i].edges);
boolean isConnected = connectivityChecker.isConnected(adjList, testCases[i].n);
System.out.println("Connected: " + isConnected);
System.out.println(connectivityChecker.toString(adjList) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Vertices: 5
Edges: [[0, 1], [1, 2], [2, 3], [3, 4]]
Connected: true
Adjacency List: {0=[1], 1=[0, 2], 2=[1, 3], 3=[2, 4], 4=[3]}
Test case 2:
Vertices: 5
Edges: [[0, 1], [2, 3]]
Connected: false
Adjacency List: {0=[1], 1=[0], 2=[3], 3=[2], 4=[]}
Test case 3:
Vertices: 0
Edges: []
Connected: true
Adjacency List: {}
Test case 4:
Vertices: 1
Edges: []
Connected: true
Adjacency List: {0=[]}
Explanation:
- Test case 1: All vertices are reachable from 0 (0→1→2→3→4), so connected.
- Test case 2: Vertex 4 is isolated, so disconnected.
- Test case 3: Empty graph is connected by definition.
- Test case 4: Single vertex is connected (no edges needed).
How It Works
- createGraph: Builds an adjacency list using a HashMap, adding undirected edges (u→v, v→u).
- isConnected:
- Returns true for n = 0 (empty graph).
- Runs DFS from vertex 0, marking visited vertices.
- Checks if all vertices are visited; returns false if any are not.
- dfs: Marks current vertex, recursively visits unvisited neighbors.
- toString: Formats adjacency list as a string, sorting vertices for consistency.
- Example Trace (Test case 1):
- DFS from 0: Visit 0, 1, 2, 3, 4 (via 0→1→2→3→4).
- All vertices visited, return true.
- Main Method: Tests connected graph, disconnected graph, empty graph, and single vertex.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| createGraph | O(n + e) | O(n + e) |
| isConnected | O(n + e) | O(n) |
| toString | O(n log n + e) | O(n + e) |
Note:
- n is the number of vertices, e is the number of edges.
- Time complexity: O(n + e) for createGraph (initialize lists and add edges); O(n + e) for isConnected (DFS visits each vertex and edge once); O(n log n + e) for toString (sorting vertices).
- Space complexity: O(n + e) for createGraph and toString (adjacency list); O(n) for isConnected (visited array and recursion stack).
- Worst case: O(n + e) time and space for dense graphs.
✅ Tip: Use DFS to efficiently explore all reachable vertices from a starting point. For undirected graphs, a single DFS from any vertex is sufficient to check connectivity.
⚠ Warning: Ensure the graph is undirected by adding edges both ways. Handle edge cases like empty graphs or isolated vertices to avoid incorrect connectivity results.
Shortest Path (Unweighted)
Problem Statement
Write a Java program that extends an unweighted undirected graph implementation to find the shortest path between two vertices using Breadth-First Search (BFS). The shortest path is the sequence of vertices with the minimum number of edges from the source to the target vertex. The program should use an adjacency list representation, return the path as a list of vertices, and test with various graphs and vertex pairs, including cases where a path exists, no path exists, and edge cases like empty or single-vertex graphs. You can visualize this as finding the fewest roads to travel between two cities in a network, where each road has the same length.
Input:
- An undirected unweighted graph represented by an adjacency list, with vertices as integers (0 to n-1).
- The number of vertices
n, a list of edges (pairs of vertices), and two verticessourceandtarget. Output: A list of integers representing the shortest path from source to target, or an empty list if no path exists, and a string representation of the graph’s adjacency list for clarity. Constraints: - The number of vertices
nis between 0 and 10^5. - Edges are pairs of integers [u, v] where 0 ≤ u, v < n.
- Source and target are integers in [0, n-1].
- The graph is undirected (edge [u, v] implies [v, u]). Example:
- Input: n = 5, edges = [[0, 1], [1, 2], [2, 3], [3, 4], [1, 4]], source = 0, target = 4
- Output: Path = [0, 1, 4], "Adjacency List: {0=[1], 1=[0, 2, 4], 2=[1, 3], 3=[2, 4], 4=[3, 1]}"
- Explanation: Shortest path from 0 to 4 is 0→1→4 (2 edges).
- Input: n = 5, edges = [[0, 1], [2, 3]], source = 0, target = 3
- Output: Path = [], "Adjacency List: {0=[1], 1=[0], 2=[3], 3=[2], 4=[]}"
- Explanation: No path exists from 0 to 3.
- Input: n = 0, edges = [], source = 0, target = 0
- Output: Path = [], "Adjacency List: {}"
- Explanation: Empty graph, no path exists.
Pseudocode
FUNCTION createGraph(n, edges)
CREATE adjList as new HashMap
FOR i from 0 to n-1
SET adjList[i] to empty list
ENDFOR
FOR each edge [u, v] in edges
ADD v to adjList[u]
ADD u to adjList[v]
ENDFOR
RETURN adjList
ENDFUNCTION
FUNCTION shortestPath(adjList, n, source, target)
IF n is 0 OR source >= n OR target >= n THEN
RETURN empty list
ENDIF
CREATE visited as boolean array of size n, initialized to false
CREATE parent as integer array of size n, initialized to -1
CREATE queue as new Queue
ADD source to queue
SET visited[source] to true
WHILE queue is not empty
SET current to queue.dequeue
IF current equals target THEN
CREATE path as new list
SET node to target
WHILE node is not -1
PREPEND node to path
SET node to parent[node]
ENDWHILE
RETURN path
ENDIF
FOR each neighbor in adjList[current]
IF NOT visited[neighbor] THEN
SET visited[neighbor] to true
SET parent[neighbor] to current
ADD neighbor to queue
ENDIF
ENDFOR
ENDWHILE
RETURN empty list
ENDFUNCTION
FUNCTION toString(adjList)
CREATE result as new StringBuilder
APPEND "Adjacency List: {" to result
FOR each vertex in adjList
APPEND vertex and "=" and adjList[vertex] to result
IF vertex is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "}" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of (n, edges, source, target) pairs
FOR each testCase in testCases
PRINT test case details
SET adjList to createGraph(testCase.n, testCase.edges)
CALL shortestPath(adjList, testCase.n, testCase.source, testCase.target)
PRINT path and graph using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Reuse
createGraph: a. Create a HashMapadjListmapping vertices to lists of neighbors. b. Initialize empty lists for vertices 0 to n-1. c. Add edges bidirectionally (u→v, v→u). - Define
shortestPath: a. If n = 0 or source/target ≥ n, return empty list. b. Use BFS:- Initialize a queue with source, mark source as visited, set parent array.
- Dequeue vertex, if it’s target, reconstruct path using parent array.
- For each unvisited neighbor, mark visited, set parent, enqueue. c. If target not reached, return empty list.
- Define
toString: a. Convert adjacency list to a string, e.g., "{0=[1], 1=[0, 2], ...}". - In
main, test with: a. A graph with a valid path. b. A graph with no path between vertices. c. An empty graph. d. A single-vertex graph.
Java Implementation
import java.util.*;
public class ShortestPathUnweighted {
// Creates adjacency list representation of the graph
private Map<Integer, List<Integer>> createGraph(int n, int[][] edges) {
Map<Integer, List<Integer>> adjList = new HashMap<>();
for (int i = 0; i < n; i++) {
adjList.put(i, new ArrayList<>());
}
for (int[] edge : edges) {
int u = edge[0], v = edge[1];
adjList.get(u).add(v);
adjList.get(v).add(u); // Undirected graph
}
return adjList;
}
// Finds shortest path from source to target using BFS
public List<Integer> shortestPath(Map<Integer, List<Integer>> adjList, int n, int source, int target) {
List<Integer> path = new ArrayList<>();
if (n == 0 || source >= n || target >= n) {
return path;
}
boolean[] visited = new boolean[n];
int[] parent = new int[n];
Arrays.fill(parent, -1);
Queue<Integer> queue = new LinkedList<>();
queue.offer(source);
visited[source] = true;
while (!queue.isEmpty()) {
int current = queue.poll();
if (current == target) {
// Reconstruct path
int node = target;
while (node != -1) {
path.add(0, node);
node = parent[node];
}
return path;
}
for (int neighbor : adjList.get(current)) {
if (!visited[neighbor]) {
visited[neighbor] = true;
parent[neighbor] = current;
queue.offer(neighbor);
}
}
}
return path; // Empty if no path exists
}
// Converts graph to string (adjacency list)
public String toString(Map<Integer, List<Integer>> adjList) {
StringBuilder result = new StringBuilder("Adjacency List: {");
List<Integer> vertices = new ArrayList<>(adjList.keySet());
Collections.sort(vertices); // For consistent output
for (int i = 0; i < vertices.size(); i++) {
int vertex = vertices.get(i);
result.append(vertex).append("=").append(adjList.get(vertex));
if (i < vertices.size() - 1) {
result.append(", ");
}
}
result.append("}");
return result.toString();
}
// Helper class for test cases
static class TestCase {
int n;
int[][] edges;
int source, target;
TestCase(int n, int[][] edges, int source, int target) {
this.n = n;
this.edges = edges;
this.source = source;
this.target = target;
}
}
// Main method to test shortest path
public static void main(String[] args) {
ShortestPathUnweighted pathFinder = new ShortestPathUnweighted();
// Test cases
TestCase[] testCases = {
// Valid path: 0-1-4
new TestCase(5, new int[][]{{0, 1}, {1, 2}, {2, 3}, {3, 4}, {1, 4}}, 0, 4),
// No path: 0 and 3 disconnected
new TestCase(5, new int[][]{{0, 1}, {2, 3}}, 0, 3),
// Empty graph
new TestCase(0, new int[][]{}, 0, 0),
// Single vertex
new TestCase(1, new int[][]{}, 0, 0)
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Vertices: " + testCases[i].n);
System.out.println("Edges: " + Arrays.deepToString(testCases[i].edges));
System.out.println("Source: " + testCases[i].source + ", Target: " + testCases[i].target);
Map<Integer, List<Integer>> adjList = pathFinder.createGraph(testCases[i].n, testCases[i].edges);
List<Integer> path = pathFinder.shortestPath(adjList, testCases[i].n, testCases[i].source, testCases[i].target);
System.out.println("Path: " + path);
System.out.println(pathFinder.toString(adjList) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Vertices: 5
Edges: [[0, 1], [1, 2], [2, 3], [3, 4], [1, 4]]
Source: 0, Target: 4
Path: [0, 1, 4]
Adjacency List: {0=[1], 1=[0, 2, 4], 2=[1, 3], 3=[2, 4], 4=[3, 1]}
Test case 2:
Vertices: 5
Edges: [[0, 1], [2, 3]]
Source: 0, Target: 3
Path: []
Adjacency List: {0=[1], 1=[0], 2=[3], 3=[2], 4=[]}
Test case 3:
Vertices: 0
Edges: []
Source: 0, Target: 0
Path: []
Adjacency List: {}
Test case 4:
Vertices: 1
Edges: []
Source: 0, Target: 0
Path: [0]
Adjacency List: {0=[]}
Explanation:
- Test case 1: Shortest path from 0 to 4 is 0→1→4 (2 edges).
- Test case 2: No path exists from 0 to 3 (disconnected components).
- Test case 3: Empty graph, no path exists.
- Test case 4: Source and target are same (0), path is [0].
How It Works
- createGraph: Builds an adjacency list using a HashMap, adding undirected edges (u→v, v→u).
- shortestPath:
- Returns empty list for invalid inputs (n = 0 or source/target ≥ n).
- Uses BFS: enqueues source, marks visited, tracks parents.
- If target reached, reconstructs path by backtracking through parent array.
- Returns empty list if no path found.
- toString: Formats adjacency list as a string, sorting vertices for consistency.
- Example Trace (Test case 1):
- BFS from 0: Enqueue 0, visit 1, enqueue 1.
- Dequeue 1, visit 2, 4, enqueue 2, 4 (parent[2]=1, parent[4]=1).
- Dequeue 2, visit 3, enqueue 3 (parent[3]=2).
- Dequeue 4, target reached, reconstruct: 4→1→0, reverse to [0, 1, 4].
- Main Method: Tests valid path, no path, empty graph, and single vertex.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| createGraph | O(n + e) | O(n + e) |
| shortestPath | O(n + e) | O(n) |
| toString | O(n log n + e) | O(n + e) |
Note:
- n is the number of vertices, e is the number of edges.
- Time complexity: O(n + e) for createGraph (initialize lists and add edges); O(n + e) for shortestPath (BFS visits each vertex and edge once); O(n log n + e) for toString (sorting vertices).
- Space complexity: O(n + e) for createGraph and toString (adjacency list); O(n) for shortestPath (visited array, parent array, queue).
- Worst case: O(n + e) time and space for dense graphs.
✅ Tip: Use BFS for finding the shortest path in unweighted graphs, as it explores vertices level by level. Store parent information to reconstruct the path efficiently.
⚠ Warning: Validate source and target vertices to ensure they are within the graph’s bounds. Handle cases where no path exists by returning an empty list.
Social Network Simulation
Problem Statement
Write a Java program that simulates a social network using an undirected weighted graph. The program should allow adding users (vertices), forming friendships (weighted edges, where weights represent interaction strength), and finding mutual friends between two users using Depth-First Search (DFS). Mutual friends are users directly connected to both input users. The program should reuse the weighted graph implementation with an adjacency list and test with various scenarios, including users with mutual friends, no mutual friends, empty graphs, and isolated users. You can visualize this as a social media platform where users connect with friends, and you query who is friends with both of two selected users, like finding common contacts in a network.
Input:
- An undirected weighted graph with vertices as integers (0 to n-1, representing users).
- Operations to add users (vertices), add friendships (edges with weights), and query mutual friends (pairs of users [u, v]). Output: Confirmation of user and friendship additions, a list of mutual friends’ IDs for queried pairs, or an empty list if none, and a string representation of the graph’s adjacency list for clarity. Constraints:
- The number of vertices
nis between 0 and 10^5. - Edges are triplets [u, v, weight] where 0 ≤ u, v < n, and weight is a positive integer in [1, 10^9].
- The graph is undirected (friendship [u, v, weight] implies [v, u, weight]). Example:
- Input: Add users 0, 1, 2, 3; Add friendships [0, 1, 5], [1, 2, 3], [2, 3, 7], [0, 2, 4]; Query mutual friends [0, 2]
- Output:
- Added users and friendships successfully
- Mutual friends of 0 and 2: [1]
- "Adjacency List: {0=[(1, 5), (2, 4)], 1=[(0, 5), (2, 3)], 2=[(1, 3), (3, 7), (0, 4)], 3=[(2, 7)]}"
- Explanation: User 1 is a mutual friend of users 0 and 2 (directly connected to both).
- Input: Add users 0, 1, 2; Add friendships [0, 1, 5]; Query mutual friends [0, 2]
- Output:
- Added users and friendships successfully
- Mutual friends of 0 and 2: []
- "Adjacency List: {0=[(1, 5)], 1=[(0, 5)], 2=[]}"
- Explanation: No mutual friends between 0 and 2.
Pseudocode
CLASS Edge
SET destination to integer
SET weight to integer
ENDCLASS
FUNCTION createGraph(n)
CREATE adjList as new HashMap
FOR i from 0 to n-1
SET adjList[i] to empty list
ENDFOR
RETURN adjList
ENDFUNCTION
FUNCTION addFriendship(adjList, u, v, weight)
IF u not in adjList OR v not in adjList THEN
RETURN false
ENDIF
CREATE edge1 as new Edge(v, weight)
CREATE edge2 as new Edge(u, weight)
ADD edge1 to adjList[u]
ADD edge2 to adjList[v]
RETURN true
ENDFUNCTION
FUNCTION getFriends(vertex, adjList, visited)
CREATE friends as new list
FUNCTION dfs(node)
SET visited[node] to true
FOR each edge in adjList[node]
IF NOT visited[edge.destination] THEN
ADD edge.destination to friends
CALL dfs(edge.destination)
ENDIF
ENDFOR
ENDFUNCTION
SET visited[vertex] to true
FOR each edge in adjList[vertex]
ADD edge.destination to friends
ENDFOR
RETURN friends
ENDFUNCTION
FUNCTION findMutualFriends(adjList, u, v)
IF u not in adjList OR v not in adjList THEN
RETURN empty list
ENDIF
CREATE visited1 as boolean array, initialized to false
CREATE visited2 as boolean array, initialized to false
SET friends1 to getFriends(u, adjList, visited1)
SET friends2 to getFriends(v, adjList, visited2)
CREATE mutual as new list
FOR each friend in friends1
IF friend in friends2 THEN
ADD friend to mutual
ENDIF
ENDFOR
RETURN mutual
ENDFUNCTION
FUNCTION toString(adjList)
CREATE result as new StringBuilder
APPEND "Adjacency List: {" to result
FOR each vertex in adjList
APPEND vertex and "=" to result
APPEND "[" to result
FOR each edge in adjList[vertex]
APPEND "(" and edge.destination and ", " and edge.weight and ")" to result
IF edge is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
IF vertex is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "}" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of (n, edges, queries) triples
FOR each testCase in testCases
PRINT test case details
SET adjList to createGraph(testCase.n)
FOR each edge [u, v, weight] in testCase.edges
CALL addFriendship(adjList, u, v, weight)
ENDFOR
FOR each query [u, v] in testCase.queries
CALL findMutualFriends(adjList, u, v)
PRINT mutual friends
ENDFOR
PRINT graph using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Reuse the
Edgeclass with: a.destination(integer for target vertex). b.weight(integer for friendship strength). - Define
createGraph: a. Create a HashMapadjListmapping vertices to lists ofEdgeobjects. b. Initialize empty lists for vertices 0 to n-1. - Define
addFriendship: a. Validate that users u and v exist. b. Add bidirectional edgesEdge(v, weight)to u’s list andEdge(u, weight)to v’s list. - Define
getFriends: a. Use DFS to collect direct neighbors of a vertex (friends). b. Mark visited vertices to avoid cycles. - Define
findMutualFriends: a. Validate that users u and v exist. b. Get friends of u and v using DFS. c. Return the intersection of their friend lists. - Define
toString: a. Convert adjacency list to a string, e.g., "{0=[(1, 5), (2, 4)], ...}". - In
main, test with: a. A graph with mutual friends. b. A graph with no mutual friends. c. An empty graph. d. A single user.
Java Implementation
import java.util.*;
public class SocialNetworkSimulation {
// Edge class to store destination and weight
static class Edge {
int destination;
int weight;
Edge(int destination, int weight) {
this.destination = destination;
this.weight = weight;
}
@Override
public String toString() {
return "(" + destination + ", " + weight + ")";
}
}
// Creates adjacency list for n users
private Map<Integer, List<Edge>> createGraph(int n) {
Map<Integer, List<Edge>> adjList = new HashMap<>();
for (int i = 0; i < n; i++) {
adjList.put(i, new ArrayList<>());
}
return adjList;
}
// Adds a friendship (weighted edge) between users u and v
public boolean addFriendship(Map<Integer, List<Edge>> adjList, int u, int v, int weight) {
if (!adjList.containsKey(u) || !adjList.containsKey(v)) {
return false;
}
adjList.get(u).add(new Edge(v, weight));
adjList.get(v).add(new Edge(u, weight)); // Undirected graph
return true;
}
// Gets friends of a vertex using DFS (direct neighbors only for mutual friends)
private List<Integer> getFriends(int vertex, Map<Integer, List<Edge>> adjList, boolean[] visited) {
List<Integer> friends = new ArrayList<>();
visited[vertex] = true;
for (Edge edge : adjList.get(vertex)) {
friends.add(edge.destination);
}
return friends;
}
// Finds mutual friends between users u and v
public List<Integer> findMutualFriends(Map<Integer, List<Edge>> adjList, int u, int v) {
List<Integer> mutual = new ArrayList<>();
if (!adjList.containsKey(u) || !adjList.containsKey(v)) {
return mutual;
}
boolean[] visited1 = new boolean[adjList.size()];
boolean[] visited2 = new boolean[adjList.size()];
List<Integer> friends1 = getFriends(u, adjList, visited1);
List<Integer> friends2 = getFriends(v, adjList, visited2);
for (int friend : friends1) {
if (friends2.contains(friend)) {
mutual.add(friend);
}
}
Collections.sort(mutual); // For consistent output
return mutual;
}
// Converts graph to string (adjacency list)
public String toString(Map<Integer, List<Edge>> adjList) {
StringBuilder result = new StringBuilder("Adjacency List: {");
List<Integer> vertices = new ArrayList<>(adjList.keySet());
Collections.sort(vertices); // For consistent output
for (int i = 0; i < vertices.size(); i++) {
int vertex = vertices.get(i);
result.append(vertex).append("=[");
List<Edge> edges = adjList.get(vertex);
for (int j = 0; j < edges.size(); j++) {
result.append(edges.get(j));
if (j < edges.size() - 1) {
result.append(", ");
}
}
result.append("]");
if (i < vertices.size() - 1) {
result.append(", ");
}
}
result.append("}");
return result.toString();
}
// Helper class for test cases
static class TestCase {
int n;
int[][] edges;
int[][] queries;
TestCase(int n, int[][] edges, int[][] queries) {
this.n = n;
this.edges = edges;
this.queries = queries;
}
}
// Main method to test social network simulation
public static void main(String[] args) {
SocialNetworkSimulation network = new SocialNetworkSimulation();
// Test cases
TestCase[] testCases = {
// Graph with mutual friends
new TestCase(
4,
new int[][]{{0, 1, 5}, {1, 2, 3}, {2, 3, 7}, {0, 2, 4}},
new int[][]{{0, 2}}
),
// Graph with no mutual friends
new TestCase(
3,
new int[][]{{0, 1, 5}},
new int[][]{{0, 2}}
),
// Empty graph
new TestCase(
0,
new int[][]{},
new int[][]{{0, 1}}
),
// Single user
new TestCase(
1,
new int[][]{},
new int[][]{{0, 0}}
)
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Vertices (Users): " + testCases[i].n);
System.out.println("Friendships: " + Arrays.deepToString(testCases[i].edges));
Map<Integer, List<Edge>> adjList = network.createGraph(testCases[i].n);
System.out.print("Added users");
if (testCases[i].edges.length > 0) {
System.out.print(" and friendships");
for (int[] edge : testCases[i].edges) {
network.addFriendship(adjList, edge[0], edge[1], edge[2]);
}
}
System.out.println(" successfully");
System.out.println("Queries:");
for (int[] query : testCases[i].queries) {
int u = query[0], v = query[1];
List<Integer> mutual = network.findMutualFriends(adjList, u, v);
System.out.println("Mutual friends of " + u + " and " + v + ": " + mutual);
}
System.out.println(network.toString(adjList) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Vertices (Users): 4
Friendships: [[0, 1, 5], [1, 2, 3], [2, 3, 7], [0, 2, 4]]
Added users and friendships successfully
Queries:
Mutual friends of 0 and 2: [1]
Adjacency List: {0=[(1, 5), (2, 4)], 1=[(0, 5), (2, 3)], 2=[(1, 3), (3, 7), (0, 4)], 3=[(2, 7)]}
Test case 2:
Vertices (Users): 3
Friendships: [[0, 1, 5]]
Added users and friendships successfully
Queries:
Mutual friends of 0 and 2: []
Adjacency List: {0=[(1, 5)], 1=[(0, 5)], 2=[]}
Test case 3:
Vertices (Users): 0
Friendships: []
Added users successfully
Queries:
Mutual friends of 0 and 1: []
Adjacency List: {}
Test case 4:
Vertices (Users): 1
Friendships: []
Added users successfully
Queries:
Mutual friends of 0 and 0: []
Adjacency List: {0=[]}
Explanation:
- Test case 1: Users 0 and 2 have mutual friend 1 (connected to both).
- Test case 2: Users 0 and 2 have no mutual friends (2 is isolated).
- Test case 3: Empty graph, no mutual friends.
- Test case 4: Single user, no mutual friends with self.
How It Works
- Edge: Stores destination vertex and weight (friendship strength).
- createGraph: Initializes adjacency list for n users.
- addFriendship: Adds bidirectional weighted edges if users exist.
- getFriends: Uses DFS to collect direct neighbors (friends), marking visited to avoid cycles.
- findMutualFriends: Finds intersection of friends’ lists for two users.
- toString: Formats adjacency list with edge weights, sorting vertices.
- Example Trace (Test case 1):
- Add users 0–3, friendships (0, 1, 5), (1, 2, 3), (2, 3, 7), (0, 2, 4).
- Query (0, 2): Friends of 0 = [1, 2], friends of 2 = [1, 3, 0], intersection = [1].
- Main Method: Tests mutual friends, no mutual friends, empty graph, and single user.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| createGraph | O(n) | O(n) |
| addFriendship | O(1) | O(1) |
| findMutualFriends | O(d_u + d_v) average | O(n) |
| toString | O(n log n + e) | O(n + e) |
Note:
- n is the number of vertices, e is the number of edges, d_u and d_v are degrees of vertices u and v.
- Time complexity: O(n) for createGraph (initialize lists); O(1) for addFriendship (add edges); O(d_u + d_v) average for findMutualFriends (collect and intersect neighbors); O(n log n + e) for toString (sorting vertices).
- Space complexity: O(n) for createGraph (empty lists); O(1) for addFriendship; O(n) for findMutualFriends (visited arrays and friend lists); O(n + e) for toString (adjacency list).
- Worst case: O(n + e) time and space for dense graphs.
✅ Tip: Use DFS to collect direct neighbors for mutual friend queries, but limit to one level for efficiency. Store weights to represent friendship strength, enabling future extensions like weighted friend recommendations.
⚠ Warning: Validate user IDs before adding friendships or querying mutual friends to avoid accessing undefined adjacency lists. Ensure DFS avoids cycles by tracking visited vertices.
Weighted Graph Extension
Problem Statement
Write a Java program that modifies an undirected graph implementation to support weighted edges by storing weights in the adjacency list. The program should allow adding weighted edges and retrieving the weight of an edge between two vertices, using an adjacency list representation with a custom edge structure. The graph remains undirected, and the program should test adding and retrieving edge weights with various graphs, including graphs with positive weights, empty graphs, and single-vertex graphs. You can visualize this as a network of cities where roads have distances (weights), and you need to store and query these distances efficiently.
Input:
- An undirected graph with vertices as integers (0 to n-1).
- The number of vertices
nand a list of weighted edges (triplets [u, v, weight]). - Queries to retrieve the weight of specific edges (u, v). Output: A confirmation of edge additions and the retrieved weights for queried edges, or -1 if the edge doesn’t exist, along with a string representation of the graph’s adjacency list for clarity. Constraints:
- The number of vertices
nis between 0 and 10^5. - Edges are triplets [u, v, weight] where 0 ≤ u, v < n, and weight is a positive integer in [1, 10^9].
- The graph is undirected (edge [u, v, weight] implies [v, u, weight]). Example:
- Input: n = 4, edges = [[0, 1, 5], [1, 2, 3], [2, 3, 7]], queries = [[0, 1], [1, 3]]
- Output:
- Added edges successfully
- Weight(0, 1) = 5
- Weight(1, 3) = -1
- "Adjacency List: {0=[(1, 5)], 1=[(0, 5), (2, 3)], 2=[(1, 3), (3, 7)], 3=[(2, 7)]}"
- Explanation: Edge (0, 1) has weight 5; edge (1, 3) doesn’t exist.
- Input: n = 0, edges = [], queries = [[0, 1]]
- Output:
- Added edges successfully
- Weight(0, 1) = -1
- "Adjacency List: {}"
- Explanation: Empty graph, no edges.
Pseudocode
CLASS Edge
SET destination to integer
SET weight to integer
ENDCLASS
FUNCTION createGraph(n, edges)
CREATE adjList as new HashMap
FOR i from 0 to n-1
SET adjList[i] to empty list
ENDFOR
FOR each edge [u, v, weight] in edges
CREATE edge1 as new Edge(v, weight)
CREATE edge2 as new Edge(u, weight)
ADD edge1 to adjList[u]
ADD edge2 to adjList[v]
ENDFOR
RETURN adjList
ENDFUNCTION
FUNCTION getEdgeWeight(adjList, u, v)
FOR each edge in adjList[u]
IF edge.destination equals v THEN
RETURN edge.weight
ENDIF
ENDFOR
RETURN -1
ENDFUNCTION
FUNCTION toString(adjList)
CREATE result as new StringBuilder
APPEND "Adjacency List: {" to result
FOR each vertex in adjList
APPEND vertex and "=" to result
APPEND "[" to result
FOR each edge in adjList[vertex]
APPEND "(" and edge.destination and ", " and edge.weight and ")" to result
IF edge is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
IF vertex is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "}" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of (n, edges, queries) pairs
FOR each testCase in testCases
PRINT test case details
SET adjList to createGraph(testCase.n, testCase.edges)
FOR each query [u, v] in testCase.queries
CALL getEdgeWeight(adjList, u, v)
PRINT query result
ENDFOR
PRINT graph using toString
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define an
Edgeclass with: a.destination(integer for the target vertex). b.weight(integer for the edge weight). - Modify
createGraph: a. Create a HashMapadjListmapping vertices to lists ofEdgeobjects. b. Initialize empty lists for vertices 0 to n-1. c. For each edge [u, v, weight], addEdge(v, weight)to u’s list andEdge(u, weight)to v’s list. - Define
getEdgeWeight: a. Search u’s adjacency list for an edge with destination v. b. Return the weight if found, else -1. - Define
toString: a. Convert adjacency list to a string, e.g., "{0=[(1, 5)], 1=[(0, 5), (2, 3)], ...}". - In
main, test with: a. A graph with weighted edges and valid/invalid queries. b. An empty graph. c. A single-vertex graph. d. A graph with a single edge.
Java Implementation
import java.util.*;
public class WeightedGraphExtension {
// Edge class to store destination and weight
static class Edge {
int destination;
int weight;
Edge(int destination, int weight) {
this.destination = destination;
this.weight = weight;
}
@Override
public String toString() {
return "(" + destination + ", " + weight + ")";
}
}
// Creates adjacency list representation with weighted edges
private Map<Integer, List<Edge>> createGraph(int n, int[][] edges) {
Map<Integer, List<Edge>> adjList = new HashMap<>();
for (int i = 0; i < n; i++) {
adjList.put(i, new ArrayList<>());
}
for (int[] edge : edges) {
int u = edge[0], v = edge[1], weight = edge[2];
adjList.get(u).add(new Edge(v, weight));
adjList.get(v).add(new Edge(u, weight)); // Undirected graph
}
return adjList;
}
// Retrieves the weight of edge (u, v)
public int getEdgeWeight(Map<Integer, List<Edge>> adjList, int u, int v) {
List<Edge> edges = adjList.getOrDefault(u, new ArrayList<>());
for (Edge edge : edges) {
if (edge.destination == v) {
return edge.weight;
}
}
return -1; // Edge not found
}
// Converts graph to string (adjacency list)
public String toString(Map<Integer, List<Edge>> adjList) {
StringBuilder result = new StringBuilder("Adjacency List: {");
List<Integer> vertices = new ArrayList<>(adjList.keySet());
Collections.sort(vertices); // For consistent output
for (int i = 0; i < vertices.size(); i++) {
int vertex = vertices.get(i);
result.append(vertex).append("=[");
List<Edge> edges = adjList.get(vertex);
for (int j = 0; j < edges.size(); j++) {
result.append(edges.get(j));
if (j < edges.size() - 1) {
result.append(", ");
}
}
result.append("]");
if (i < vertices.size() - 1) {
result.append(", ");
}
}
result.append("}");
return result.toString();
}
// Helper class for test cases
static class TestCase {
int n;
int[][] edges;
int[][] queries;
TestCase(int n, int[][] edges, int[][] queries) {
this.n = n;
this.edges = edges;
this.queries = queries;
}
}
// Main method to test weighted graph
public static void main(String[] args) {
WeightedGraphExtension graph = new WeightedGraphExtension();
// Test cases
TestCase[] testCases = {
// Graph with weighted edges
new TestCase(
4,
new int[][]{{0, 1, 5}, {1, 2, 3}, {2, 3, 7}},
new int[][]{{0, 1}, {1, 3}}
),
// Empty graph
new TestCase(
0,
new int[][]{},
new int[][]{{0, 1}}
),
// Single vertex
new TestCase(
1,
new int[][]{},
new int[][]{{0, 0}}
),
// Single edge
new TestCase(
2,
new int[][]{{0, 1, 10}},
new int[][]{{0, 1}, {1, 0}}
)
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
System.out.println("Vertices: " + testCases[i].n);
System.out.println("Edges: " + Arrays.deepToString(testCases[i].edges));
Map<Integer, List<Edge>> adjList = graph.createGraph(testCases[i].n, testCases[i].edges);
System.out.println("Added edges successfully");
System.out.println("Queries:");
for (int[] query : testCases[i].queries) {
int u = query[0], v = query[1];
int weight = graph.getEdgeWeight(adjList, u, v);
System.out.println("Weight(" + u + ", " + v + ") = " + weight);
}
System.out.println(graph.toString(adjList) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Vertices: 4
Edges: [[0, 1, 5], [1, 2, 3], [2, 3, 7]]
Added edges successfully
Queries:
Weight(0, 1) = 5
Weight(1, 3) = -1
Adjacency List: {0=[(1, 5)], 1=[(0, 5), (2, 3)], 2=[(1, 3), (3, 7)], 3=[(2, 7)]}
Test case 2:
Vertices: 0
Edges: []
Added edges successfully
Queries:
Weight(0, 1) = -1
Adjacency List: {}
Test case 3:
Vertices: 1
Edges: []
Added edges successfully
Queries:
Weight(0, 0) = -1
Adjacency List: {0=[]}
Test case 4:
Vertices: 2
Edges: [[0, 1, 10]]
Added edges successfully
Queries:
Weight(0, 1) = 10
Weight(1, 0) = 10
Adjacency List: {0=[(1, 10)], 1=[(0, 10)]}
Explanation:
- Test case 1: Edge (0, 1) has weight 5; edge (1, 3) doesn’t exist.
- Test case 2: Empty graph, no edges, weight query returns -1.
- Test case 3: Single vertex, no edges, weight query returns -1.
- Test case 4: Single edge (0, 1) with weight 10, both directions return 10.
How It Works
- Edge: Stores destination vertex and weight.
- createGraph: Builds an adjacency list mapping vertices to lists of
Edgeobjects, adding undirected edges (u→v, v→u) with weights. - getEdgeWeight: Searches u’s adjacency list for v, returns weight or -1 if not found.
- toString: Formats adjacency list with edge weights, sorting vertices for consistency.
- Example Trace (Test case 1):
- Add edges: (0, 1, 5), (1, 2, 3), (2, 3, 7).
- Query (0, 1): Find edge (1, 5) in 0’s list, return 5.
- Query (1, 3): No edge to 3 in 1’s list, return -1.
- Main Method: Tests graph with weighted edges, empty graph, single vertex, and single edge.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| createGraph | O(n + e) | O(n + e) |
| getEdgeWeight | O(degree(u)) average | O(1) |
| toString | O(n log n + e) | O(n + e) |
Note:
- n is the number of vertices, e is the number of edges, degree(u) is the number of neighbors of vertex u.
- Time complexity: O(n + e) for createGraph (initialize lists and add edges); O(degree(u)) average for getEdgeWeight (search u’s list); O(n log n + e) for toString (sorting vertices).
- Space complexity: O(n + e) for createGraph and toString (adjacency list); O(1) for getEdgeWeight (no additional storage).
- Worst case: O(n + e) time and space for dense graphs; O(n) for getEdgeWeight in dense graphs.
✅ Tip: Use a custom
Edgeclass to store destination and weight, making it easy to extend the adjacency list for weighted graphs. Ensure undirected edges are added bidirectionally.
⚠ Warning: Validate vertex indices in queries to avoid accessing undefined adjacency lists. Handle non-existent edges by returning -1 to indicate no connection.
Sorting Algorithms Problem Solving with DSA
🛠 What you will learn
This section outlines the format used for each DSA problem statement, designed to guide students through solving exercise problems in a clear, structured learning process. Each component serves a specific purpose to enhance understanding and practical application:
- Problem Statement: Clearly defines the task, including inputs, outputs, constraints, and an example. A real-world analogy makes the problem relatable, aligning with the engaging tone of the "Core Data Structures" summary, ensuring students grasp the problem’s context.
- Pseudocode: Provides a high-level, language-agnostic outline of the solution using standardized syntax (e.g.,
FUNCTION,IF,SET,RETURN). This helps students understand the logic before diving into code, bridging theory and implementation. - Algorithm Steps: Breaks down the pseudocode into detailed, actionable steps, connecting theoretical logic to practical coding. This ensures students can follow the implementation process systematically.
- Java Implementation: Offers a complete, commented Java code solution that students can study and run. It includes a
mainmethod with at least four test cases (e.g., target present, absent, middle element, and edge cases like duplicates or single elements) to verify correctness and explore different scenarios, maintaining a beginner-friendly approach. - Output: Shows the expected results from the test cases, including indices, comparisons, and execution times (where applicable), helping students verify their code’s correctness and understand the algorithm’s behavior.
- How It Works: Traces the algorithm’s execution with a detailed example, showing step-by-step how the search narrows down the range. This reinforces understanding by illustrating the algorithm in action.
- Complexity Analysis Table: Summarizes time and space complexity for best, average, and worst cases, teaching students to evaluate efficiency and compare trade-offs across implementations.
- Tip or Warning Box: Provides practical advice (e.g., when to use DSA) and highlights pitfalls (e.g., ensuring the array is sorted), guiding students toward best practices and common error avoidance.
Each exercise includes test cases to verify correctness and performance, aligning with a learning platform featuring embedded compilers for hands-on practice and visualizations to illustrate the halving process of DSA.
Basic Bubble Sort
Problem Statement
Write a Java program that implements the Bubble Sort algorithm to sort an array of integers in ascending order. The program should count the number of swaps performed during sorting and test the implementation with various input arrays, including unsorted, already sorted, reversed, and arrays with duplicate elements. Bubble Sort repeatedly compares adjacent elements and swaps them if they are in the wrong order, bubbling larger elements to the end. You can visualize this as bubbles rising in a glass, pushing larger numbers to the end with each pass.
Input:
- An array of integers to be sorted. Output: The sorted array, the number of swaps performed, and a string representation of the input and sorted arrays for clarity. Constraints:
- The array length
nis between 0 and 10^5. - Array elements are integers in the range [-10^9, 10^9]. Example:
- Input: array = [64, 34, 25, 12, 22]
- Output:
- Input Array: [64, 34, 25, 12, 22]
- Sorted Array: [12, 22, 25, 34, 64]
- Swaps: 7
- Explanation: Bubble Sort performs passes, swapping adjacent elements if out of order, resulting in 7 swaps.
- Input: array = [1, 2, 3]
- Output:
- Input Array: [1, 2, 3]
- Sorted Array: [1, 2, 3]
- Swaps: 0
- Explanation: Already sorted array requires no swaps.
Pseudocode
FUNCTION bubbleSort(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
FOR j from 0 to n-i-2
IF arr[j] > arr[j+1] THEN
SET temp to arr[j]
SET arr[j] to arr[j+1]
SET arr[j+1] to temp
INCREMENT swaps
ENDIF
ENDFOR
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of input arrays
FOR each testCase in testCases
PRINT test case details
CREATE copy of testCase array
SET swaps to bubbleSort(copy)
PRINT input array, sorted array, and swaps
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
bubbleSort: a. Initialize a counterswapsto 0. b. For each pass i from 0 to n-1:- Compare adjacent elements j and j+1 from 0 to n-i-2.
- If arr[j] > arr[j+1], swap them and increment
swaps. c. Return the number of swaps.
- Define
toString: a. Convert the array to a string, e.g., "[64, 34, 25, 12, 22]". - In
main, test with: a. An unsorted array. b. An already sorted array. c. A reversed array. d. An array with duplicates. e. An empty array.
Java Implementation
import java.util.*;
public class BasicBubbleSort {
// Performs Bubble Sort and counts swaps
public int bubbleSort(int[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// Swap elements
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swaps++;
}
}
}
return swaps;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
result.append(arr[i]);
if (i < arr.length - 1) {
result.append(", ");
}
}
result.append("]");
return result.toString();
}
// Helper class for test cases
static class TestCase {
int[] arr;
TestCase(int[] arr) {
this.arr = arr;
}
}
// Main method to test Bubble Sort
public static void main(String[] args) {
BasicBubbleSort sorter = new BasicBubbleSort();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{64, 34, 25, 12, 22}), // Unsorted
new TestCase(new int[]{1, 2, 3, 4, 5}), // Sorted
new TestCase(new int[]{5, 4, 3, 2, 1}), // Reversed
new TestCase(new int[]{3, 1, 3, 2, 1}), // Duplicates
new TestCase(new int[]{}) // Empty
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
int[] arr = testCases[i].arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
int swaps = sorter.bubbleSort(arr);
System.out.println("Sorted Array: " + sorter.toString(arr));
System.out.println("Swaps: " + swaps + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input Array: [64, 34, 25, 12, 22]
Sorted Array: [12, 22, 25, 34, 64]
Swaps: 7
Test case 2:
Input Array: [1, 2, 3, 4, 5]
Sorted Array: [1, 2, 3, 4, 5]
Swaps: 0
Test case 3:
Input Array: [5, 4, 3, 2, 1]
Sorted Array: [1, 2, 3, 4, 5]
Swaps: 10
Test case 4:
Input Array: [3, 1, 3, 2, 1]
Sorted Array: [1, 1, 2, 3, 3]
Swaps: 6
Test case 5:
Input Array: []
Sorted Array: []
Swaps: 0
Explanation:
- Test case 1: Unsorted array requires 7 swaps to sort.
- Test case 2: Already sorted array requires 0 swaps.
- Test case 3: Reversed array requires 10 swaps (maximum for n=5).
- Test case 4: Array with duplicates requires 6 swaps.
- Test case 5: Empty array requires 0 swaps.
How It Works
- bubbleSort:
- Iterates through the array, comparing adjacent elements.
- Swaps if out of order, incrementing
swaps. - Each pass ensures the largest unsorted element moves to its correct position.
- toString: Formats array as a string for output.
- Example Trace (Test case 1):
- Pass 1: [64, 34, 25, 12, 22] → [34, 25, 12, 22, 64] (3 swaps: 64↔34, 34↔25, 25↔12).
- Pass 2: [34, 25, 12, 22, 64] → [25, 12, 22, 34, 64] (2 swaps: 34↔25, 25↔12).
- Pass 3: [25, 12, 22, 34, 64] → [12, 22, 25, 34, 64] (2 swaps: 25↔12, 25↔22).
- Passes 4-5: No swaps, array sorted. Total swaps: 7.
- Main Method: Tests unsorted, sorted, reversed, duplicates, and empty arrays.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| bubbleSort | O(n²) | O(1) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for bubbleSort (n passes, up to n-1 comparisons/swaps each); O(n) for toString (iterate array).
- Space complexity: O(1) for bubbleSort (in-place); O(n) for toString (string builder).
- Worst case: O(n²) time for reversed arrays; best case: O(n) time for sorted arrays.
✅ Tip: Bubble Sort is simple but inefficient for large arrays. Use it for small datasets or educational purposes. Tracking swaps helps understand the algorithm’s behavior.
⚠ Warning: Ensure the array is not modified unintentionally by creating a copy for sorting. Handle empty arrays to avoid index issues.
Bubble Sort Edge Case Handling
Problem Statement
Write a Java program that enhances the Bubble Sort implementation to sort arrays containing negative numbers and floating-point numbers in ascending order, using the optimized version with the swapped flag. The program should count the number of swaps performed and test with diverse inputs, including arrays with negative integers, floating-point numbers, mixed positive/negative numbers, empty arrays, and single-element arrays. The enhanced implementation should use generics to support comparable types, focusing on Double to handle both negative and floating-point numbers. You can visualize this as organizing a list of measurements (e.g., temperatures or scores) that include decimals and negative values, ensuring the algorithm handles all cases correctly.
Input:
- Arrays of
Doublevalues, including negative numbers, floating-point numbers, and mixed cases. Output: The sorted array (in ascending order), the number of swaps performed, and a string representation of the input and sorted arrays for clarity. Constraints: - The array length
nis between 0 and 10^5. - Array elements are
Doublevalues in the range [-10^9, 10^9], including floating-point numbers. Example: - Input: array = [64.5, -34.2, 25.0, -12.7, 22.3]
- Output:
- Input Array: [64.5, -34.2, 25.0, -12.7, 22.3]
- Sorted Array: [-34.2, -12.7, 22.3, 25.0, 64.5]
- Swaps: 6
- Explanation: Bubble Sort sorts negative and floating-point numbers, requiring 6 swaps.
- Input: array = [-1.5, -2.5, -3.5]
- Output:
- Input Array: [-1.5, -2.5, -3.5]
- Sorted Array: [-3.5, -2.5, -1.5]
- Swaps: 3
- Explanation: Negative floating-point numbers sorted, 3 swaps.
Pseudocode
FUNCTION bubbleSort(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
SET swapped to false
FOR j from 0 to n-i-2
IF arr[j] > arr[j+1] THEN
SET temp to arr[j]
SET arr[j] to arr[j+1]
SET arr[j+1] to temp
INCREMENT swaps
SET swapped to true
ENDIF
ENDFOR
IF NOT swapped THEN
BREAK
ENDIF
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of input arrays
FOR each testCase in testCases
PRINT test case details
CREATE copy of testCase array
SET swaps to bubbleSort(copy)
PRINT input array, sorted array, and swaps
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
bubbleSort(generic): a. Accept an array ofComparable<T>type (useDoublefor testing). b. Initialize a counterswapsto 0 and aswappedflag. c. For each pass i from 0 to n-1:- Compare adjacent elements using
compareTo, swap if arr[j] > arr[j+1]. - Increment
swapsand setswappedto true if a swap occurs. - Break if no swaps in a pass. d. Return the number of swaps.
- Compare adjacent elements using
- Define
toString: a. Convert the array to a string, e.g., "[64.5, -34.2, 25.0]". - In
main, test with: a. Mixed positive/negative floating-point numbers. b. Negative floating-point numbers. c. Mixed positive/negative integers. d. Empty array. e. Single-element array. f. Array with duplicate floating-point numbers.
Java Implementation
import java.util.*;
public class BubbleSortEdgeCaseHandling {
// Optimized Bubble Sort for Comparable types
public <T extends Comparable<T>> int bubbleSort(T[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
boolean swapped = false;
for (int j = 0; j < n - i - 1; j++) {
if (arr[j].compareTo(arr[j + 1]) > 0) {
// Swap elements
T temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swaps++;
swapped = true;
}
}
if (!swapped) {
break;
}
}
return swaps;
}
// Converts array to string
public <T> String toString(T[] arr) {
StringBuilder result = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
result.append(arr[i]);
if (i < arr.length - 1) {
result.append(", ");
}
}
result.append("]");
return result.toString();
}
// Helper class for test cases
static class TestCase {
Double[] arr;
TestCase(Double[] arr) {
this.arr = arr;
}
}
// Main method to test enhanced Bubble Sort
public static void main(String[] args) {
BubbleSortEdgeCaseHandling sorter = new BubbleSortEdgeCaseHandling();
// Test cases
TestCase[] testCases = {
new TestCase(new Double[]{64.5, -34.2, 25.0, -12.7, 22.3}), // Mixed positive/negative floats
new TestCase(new Double[]{-1.5, -2.5, -3.5}), // Negative floats
new TestCase(new Double[]{-5.0, 3.0, -2.0, 0.0, 10.0}), // Mixed integers
new TestCase(new Double[]{}), // Empty
new TestCase(new Double[]{42.5}), // Single element
new TestCase(new Double[]{2.5, 1.0, 2.5, 1.5, 1.0}) // Duplicates
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
Double[] arr = testCases[i].arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
int swaps = sorter.bubbleSort(arr);
System.out.println("Sorted Array: " + sorter.toString(arr));
System.out.println("Swaps: " + swaps + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input Array: [64.5, -34.2, 25.0, -12.7, 22.3]
Sorted Array: [-34.2, -12.7, 22.3, 25.0, 64.5]
Swaps: 6
Test case 2:
Input Array: [-1.5, -2.5, -3.5]
Sorted Array: [-3.5, -2.5, -1.5]
Swaps: 3
Test case 3:
Input Array: [-5.0, 3.0, -2.0, 0.0, 10.0]
Sorted Array: [-5.0, -2.0, 0.0, 3.0, 10.0]
Swaps: 3
Test case 4:
Input Array: []
Sorted Array: []
Swaps: 0
Test case 5:
Input Array: [42.5]
Sorted Array: [42.5]
Swaps: 0
Test case 6:
Input Array: [2.5, 1.0, 2.5, 1.5, 1.0]
Sorted Array: [1.0, 1.0, 1.5, 2.5, 2.5]
Swaps: 5
Explanation:
- Test case 1: Mixed positive/negative floats, 6 swaps to sort.
- Test case 2: Negative floats, 3 swaps (reversed order).
- Test case 3: Mixed integers, 3 swaps.
- Test case 4: Empty array, 0 swaps.
- Test case 5: Single element, 0 swaps.
- Test case 6: Duplicates, 5 swaps.
How It Works
- bubbleSort:
- Uses generics with
Comparable<T>to handleDouble(floating-point and negative numbers). - Compares with
compareTo, swaps if arr[j] > arr[j+1], counts swaps. - Exits early if no swaps occur (optimized).
- Uses generics with
- toString: Formats array as a string, handling
Doublevalues. - Example Trace (Test case 1):
- Pass 1: [64.5, -34.2, 25.0, -12.7, 22.3] → [-34.2, 25.0, -12.7, 22.3, 64.5] (3 swaps: 64.5↔-34.2, -34.2↔25.0, 25.0↔-12.7).
- Pass 2: [-34.2, 25.0, -12.7, 22.3, 64.5] → [-34.2, -12.7, 22.3, 25.0, 64.5] (2 swaps: 25.0↔-12.7, -12.7↔22.3).
- Pass 3: [-34.2, -12.7, 22.3, 25.0, 64.5] → [-34.2, -12.7, 22.3, 25.0, 64.5] (0 swaps, exit).
- Total swaps: 6.
- Main Method: Tests mixed floats, negative floats, mixed integers, empty, single element, and duplicates.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| bubbleSort | O(n²) worst, O(n) best | O(1) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for bubbleSort in worst case, O(n) in best case (already sorted); O(n) for toString.
- Space complexity: O(1) for bubbleSort (in-place); O(n) for toString (string builder).
- Generics ensure flexibility without changing complexity.
✅ Tip: Use generics with
Comparableto handle negative and floating-point numbers in Bubble Sort. The swapped flag optimization improves performance for nearly sorted arrays.
⚠ Warning: Ensure input arrays contain valid
Doublevalues to avoidNullPointerExceptionin comparisons. Test edge cases like empty or single-element arrays to verify robustness.
Bubble Sort Flag Optimization
Problem Statement
Write a Java program that implements two versions of Bubble Sort for sorting an array of integers in ascending order: one without the swapped flag (basic version) and one with the swapped flag optimization, which exits early if no swaps occur in a pass. Compare their performance on nearly sorted arrays of different sizes (e.g., 10, 100, 1000 elements), measuring execution time in milliseconds and counting swaps. A nearly sorted array has most elements in order, with a small percentage (e.g., 5%) swapped to introduce minor disorder. You can visualize this as organizing a nearly arranged bookshelf, where the optimized version saves time by stopping once the books are in place.
Input:
- Nearly sorted arrays of integers with sizes 10, 100, and 1000. Output: Execution time (in milliseconds) and number of swaps for each version (basic and optimized) for each array size, along with a string representation of the input and sorted arrays for verification. Constraints:
- Array sizes are 10, 100, 1000.
- Array elements are integers in the range [0, 10^6].
- Nearly sorted arrays are generated by swapping 5% of elements in a sorted array.
- Execution times are averaged over multiple runs for accuracy. Example:
- Input: Array size = 10, Nearly sorted: [1, 2, 5, 4, 3, 6, 7, 8, 9, 10]
- Output (example, times vary):
- Basic Bubble Sort: Time = 0.05 ms, Swaps = 2
- Optimized Bubble Sort: Time = 0.03 ms, Swaps = 2
- Explanation: Both sort to [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], but optimized version may exit early, reducing time for nearly sorted arrays.
Pseudocode
FUNCTION bubbleSortBasic(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
FOR j from 0 to n-i-2
IF arr[j] > arr[j+1] THEN
SET temp to arr[j]
SET arr[j] to arr[j+1]
SET arr[j+1] to temp
INCREMENT swaps
ENDIF
ENDFOR
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION bubbleSortOptimized(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
SET swapped to false
FOR j from 0 to n-i-2
IF arr[j] > arr[j+1] THEN
SET temp to arr[j]
SET arr[j] to arr[j+1]
SET arr[j+1] to temp
INCREMENT swaps
SET swapped to true
ENDIF
ENDFOR
IF NOT swapped THEN
BREAK
ENDIF
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION generateNearlySorted(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to i + 1
ENDFOR
SET numSwaps to floor(n * 0.05)
FOR i from 0 to numSwaps-1
SET idx1 to random integer in [0, n-1]
SET idx2 to random integer in [0, n-1]
SWAP arr[idx1] and arr[idx2]
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [10, 100, 1000]
SET runs to 10
FOR each size in sizes
SET arr to generateNearlySorted(size)
SET totalTimeBasic to 0
SET totalSwapsBasic to 0
SET totalTimeOpt to 0
SET totalSwapsOpt to 0
FOR i from 0 to runs-1
CREATE copy1 of arr
SET startTime to current nano time
SET swaps to bubbleSortBasic(copy1)
SET endTime to current nano time
ADD (endTime - startTime) to totalTimeBasic
ADD swaps to totalSwapsBasic
CREATE copy2 of arr
SET startTime to current nano time
SET swaps to bubbleSortOptimized(copy2)
SET endTime to current nano time
ADD (endTime - startTime) to totalTimeOpt
ADD swaps to totalSwapsOpt
ENDFOR
PRINT size, input array, sorted array
PRINT average time and swaps for basic and optimized versions
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
bubbleSortBasic: a. Iterate n passes, compare and swap if arr[j] > arr[j+1], count swaps. b. Return number of swaps. - Define
bubbleSortOptimized: a. Add aswappedflag, set to false each pass. b. Swap and setswappedto true if arr[j] > arr[j+1], count swaps. c. Break if no swaps occur in a pass. d. Return number of swaps. - Define
generateNearlySorted: a. Create sorted array [1, 2, ..., n]. b. Swap 5% of elements randomly to introduce minor disorder. - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Array sizes: 10, 100, 1000. b. Nearly sorted arrays for each size. c. Run each version 10 times, average times and swaps. d. Measure time usingSystem.nanoTime(), convert to milliseconds.
Java Implementation
import java.util.*;
public class BubbleSortFlagOptimization {
// Basic Bubble Sort without swapped flag
public int bubbleSortBasic(int[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swaps++;
}
}
}
return swaps;
}
// Optimized Bubble Sort with swapped flag
public int bubbleSortOptimized(int[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
boolean swapped = false;
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swaps++;
swapped = true;
}
}
if (!swapped) {
break;
}
}
return swaps;
}
// Generates nearly sorted array
private int[] generateNearlySorted(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
int numSwaps = (int) (n * 0.05); // 5% of elements
for (int i = 0; i < numSwaps; i++) {
int idx1 = rand.nextInt(n);
int idx2 = rand.nextInt(n);
int temp = arr[idx1];
arr[idx1] = arr[idx2];
arr[idx2] = temp;
}
return arr;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
int size;
int[] arr;
TestCase(int size, int[] arr) {
this.size = size;
this.arr = arr;
}
}
// Main method to test performance
public static void main(String[] args) {
BubbleSortFlagOptimization sorter = new BubbleSortFlagOptimization();
int[] sizes = {10, 100, 1000};
int runs = 10;
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
int[] arr = sorter.generateNearlySorted(size);
TestCase testCase = new TestCase(size, arr);
long totalTimeBasic = 0, totalSwapsBasic = 0;
long totalTimeOpt = 0, totalSwapsOpt = 0;
for (int i = 0; i < runs; i++) {
int[] arrBasic = testCase.arr.clone();
long startTime = System.nanoTime();
int swaps = sorter.bubbleSortBasic(arrBasic);
long endTime = System.nanoTime();
totalTimeBasic += (endTime - startTime);
totalSwapsBasic += swaps;
int[] arrOpt = testCase.arr.clone();
startTime = System.nanoTime();
swaps = sorter.bubbleSortOptimized(arrOpt);
endTime = System.nanoTime();
totalTimeOpt += (endTime - startTime);
totalSwapsOpt += swaps;
}
double avgTimeBasicMs = totalTimeBasic / (double) runs / 1_000_000.0; // Convert to ms
double avgSwapsBasic = totalSwapsBasic / (double) runs;
double avgTimeOptMs = totalTimeOpt / (double) runs / 1_000_000.0;
double avgSwapsOpt = totalSwapsOpt / (double) runs;
System.out.println("Input Array: " + sorter.toString(testCase.arr));
int[] sorted = testCase.arr.clone();
sorter.bubbleSortOptimized(sorted);
System.out.println("Sorted Array: " + sorter.toString(sorted));
System.out.printf("Basic Bubble Sort - Average Time: %.2f ms, Average Swaps: %.0f\n", avgTimeBasicMs, avgSwapsBasic);
System.out.printf("Optimized Bubble Sort - Average Time: %.2f ms, Average Swaps: %.0f\n\n", avgTimeOptMs, avgSwapsOpt);
}
}
}
Output
Running the main method produces (times vary by system, example shown):
Array Size: 10
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Basic Bubble Sort - Average Time: 0.02 ms, Average Swaps: 0
Optimized Bubble Sort - Average Time: 0.01 ms, Average Swaps: 0
Array Size: 100
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Basic Bubble Sort - Average Time: 0.15 ms, Average Swaps: 248
Optimized Bubble Sort - Average Time: 0.10 ms, Average Swaps: 248
Array Size: 1000
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Basic Bubble Sort - Average Time: 15.00 ms, Average Swaps: 2475
Optimized Bubble Sort - Average Time: 5.50 ms, Average Swaps: 2475
Explanation:
- Size 10: Nearly sorted array often requires 0 swaps (already sorted due to small size), with optimized version slightly faster.
- Size 100: ~248 swaps (5% of 4950 possible swaps), optimized version faster due to early termination.
- Size 1000: ~2475 swaps, optimized version significantly faster as it stops after few passes.
- Times are averaged over 10 runs; optimized version benefits more for larger, nearly sorted arrays.
How It Works
- bubbleSortBasic: Performs all n passes, swapping if arr[j] > arr[j+1], counts swaps.
- bubbleSortOptimized: Uses
swappedflag to exit early if no swaps occur, counts swaps. - generateNearlySorted: Creates sorted array, swaps 5% of elements randomly.
- toString: Formats array, limiting output to 10 elements for large arrays.
- Main Method:
- Tests sizes 10, 100, 1000 with nearly sorted arrays.
- Runs each version 10 times, averages times and swaps.
- Measures time with
System.nanoTime(), converts to milliseconds.
- Example Trace (Size 100, Nearly Sorted):
- Array: Mostly sorted with ~5 swaps needed.
- Basic: Completes all 100 passes.
- Optimized: Stops after ~5 passes if no further swaps needed.
- Both produce same swaps, but optimized is faster.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| bubbleSortBasic | O(n²) | O(1) |
| bubbleSortOptimized | O(n²) worst, O(n) best | O(1) |
| generateNearlySorted | O(n) | O(n) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for bubbleSortBasic (always n passes); O(n²) worst case, O(n) best case for bubbleSortOptimized; O(n) for generateNearlySorted and toString.
- Space complexity: O(1) for both sorting methods (in-place); O(n) for generateNearlySorted and toString (array and string builder).
- Nearly sorted arrays: Optimized version often closer to O(n) due to early termination.
✅ Tip: Use the swapped flag in Bubble Sort to optimize for nearly sorted arrays, significantly reducing passes. Test with multiple runs to account for system variability in timing.
⚠ Warning: Ensure identical input arrays for both versions to ensure fair comparison. Nearly sorted arrays should have controlled randomness (e.g., fixed seed) for reproducible results.
Bubble Sort Performance Analysis
Problem Statement
Write a Java program that measures the execution time of the Bubble Sort algorithm for sorting arrays of integers in ascending order, testing arrays of increasing sizes (e.g., 10, 100, 1000 elements). The program should compare performance across best-case (already sorted), average-case (random elements), and worst-case (reversed order) scenarios, reporting execution times in milliseconds. The Bubble Sort implementation should reuse the existing algorithm, counting swaps and measuring time for each run. You can visualize this as timing how long it takes to organize a list of numbers, observing how Bubble Sort’s performance varies with input size and order.
Input:
- Arrays of integers with sizes 10, 100, and 1000, generated for best (sorted), average (random), and worst (reversed) cases. Output: The execution time (in milliseconds) for each array size and case, the number of swaps performed, and a string representation of the input and sorted arrays for verification. Constraints:
- Array sizes are 10, 100, 1000.
- Array elements are integers in the range [0, 10^6] for random cases.
- Execution times are averaged over multiple runs for accuracy. Example:
- Input: Array size = 10, Cases: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] (best), [5, 2, 8, 1, 9, 3, 7, 4, 6, 10] (average), [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] (worst)
- Output (example, times vary):
- Size 10, Best Case: Time = 0.02 ms, Swaps = 0
- Size 10, Average Case: Time = 0.05 ms, Swaps = 23
- Size 10, Worst Case: Time = 0.06 ms, Swaps = 45
- Explanation: Best case requires no swaps, average case has moderate swaps, worst case has maximum swaps, with times increasing accordingly.
Pseudocode
FUNCTION bubbleSort(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
FOR j from 0 to n-i-2
IF arr[j] > arr[j+1] THEN
SET temp to arr[j]
SET arr[j] to arr[j+1]
SET arr[j+1] to temp
INCREMENT swaps
ENDIF
ENDFOR
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION generateBestCase(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to i + 1
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION generateAverageCase(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to random integer in [0, 10^6]
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION generateWorstCase(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to n - i
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [10, 100, 1000]
SET runs to 10
FOR each size in sizes
SET bestArr to generateBestCase(size)
SET avgArr to generateAverageCase(size)
SET worstArr to generateWorstCase(size)
FOR each case (bestArr, avgArr, worstArr)
SET totalTime to 0
SET totalSwaps to 0
FOR i from 0 to runs-1
CREATE copy of case array
SET startTime to current nano time
SET swaps to bubbleSort(copy)
SET endTime to current nano time
ADD (endTime - startTime) to totalTime
ADD swaps to totalSwaps
ENDFOR
PRINT case details, input array, sorted array
PRINT average time in milliseconds and average swaps
ENDFOR
ENDFOR
ENDFUNCTION
Algorithm Steps
- Reuse
bubbleSort: a. Initialize a counterswapsto 0. b. For each pass i from 0 to n-1, compare and swap if arr[j] > arr[j+1]. c. Return the number of swaps. - Define
generateBestCase: a. Create array [1, 2, ..., n] (already sorted). - Define
generateAverageCase: a. Create array with random integers in [0, 10^6]. - Define
generateWorstCase: a. Create array [n, n-1, ..., 1] (reversed). - Define
toString: a. Convert array to a string, e.g., "[1, 2, 3]". - In
main, test with: a. Array sizes: 10, 100, 1000. b. Cases: best (sorted), average (random), worst (reversed). c. Run each case 10 times, average times and swaps. d. Measure time usingSystem.nanoTime(), convert to milliseconds.
Java Implementation
import java.util.*;
public class BubbleSortPerformanceAnalysis {
// Performs Bubble Sort and counts swaps
public int bubbleSort(int[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// Swap elements
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swaps++;
}
}
}
return swaps;
}
// Generates best-case array (sorted)
private int[] generateBestCase(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
return arr;
}
// Generates average-case array (random)
private int[] generateAverageCase(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(1000001); // [0, 10^6]
}
return arr;
}
// Generates worst-case array (reversed)
private int[] generateWorstCase(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = n - i;
}
return arr;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
int size;
String type;
int[] arr;
TestCase(int size, String type, int[] arr) {
this.size = size;
this.type = type;
this.arr = arr;
}
}
// Main method to test performance
public static void main(String[] args) {
BubbleSortPerformanceAnalysis sorter = new BubbleSortPerformanceAnalysis();
int[] sizes = {10, 100, 1000};
int runs = 10;
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
TestCase[] cases = {
new TestCase(size, "Best Case", sorter.generateBestCase(size)),
new TestCase(size, "Average Case", sorter.generateAverageCase(size)),
new TestCase(size, "Worst Case", sorter.generateWorstCase(size))
};
for (TestCase testCase : cases) {
long totalTime = 0;
long totalSwaps = 0;
for (int i = 0; i < runs; i++) {
int[] arr = testCase.arr.clone();
long startTime = System.nanoTime();
int swaps = sorter.bubbleSort(arr);
long endTime = System.nanoTime();
totalTime += (endTime - startTime);
totalSwaps += swaps;
}
double avgTimeMs = totalTime / (double) runs / 1_000_000.0; // Convert to ms
double avgSwaps = totalSwaps / (double) runs;
System.out.println(testCase.type + ":");
System.out.println("Input Array: " + sorter.toString(testCase.arr));
int[] sorted = testCase.arr.clone();
sorter.bubbleSort(sorted);
System.out.println("Sorted Array: " + sorter.toString(sorted));
System.out.printf("Average Time: %.2f ms\n", avgTimeMs);
System.out.printf("Average Swaps: %.0f\n\n", avgSwaps);
}
}
}
}
Output
Running the main method produces (times vary by system, example shown):
Array Size: 10
Best Case:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Average Time: 0.02 ms
Average Swaps: 0
Average Case:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178]
Sorted Array: [333, 360, 289796, 304135, 374316, 628178, 648054, 727595, 766336, 767890]
Average Time: 0.05 ms
Average Swaps: 22
Worst Case:
Input Array: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Average Time: 0.06 ms
Average Swaps: 45
Array Size: 100
Best Case:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Average Time: 0.15 ms
Average Swaps: 0
Average Case:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178, ...]
Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
Average Time: 1.80 ms
Average Swaps: 2478
Worst Case:
Input Array: [100, 99, 98, 97, 96, 95, 94, 93, 92, 91, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Average Time: 2.10 ms
Average Swaps: 4950
Array Size: 1000
Best Case:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Average Time: 1.50 ms
Average Swaps: 0
Average Case:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178, ...]
Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
Average Time: 180.00 ms
Average Swaps: 249750
Worst Case:
Input Array: [1000, 999, 998, 997, 996, 995, 994, 993, 992, 991, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Average Time: 210.00 ms
Average Swaps: 499500
Explanation:
- Size 10: Best case (0 swaps, minimal time), average case (~22 swaps, moderate time), worst case (45 swaps, highest time).
- Size 100: Best case (0 swaps), average case (~2478 swaps), worst case (4950 swaps), with times scaling quadratically.
- Size 1000: Best case (0 swaps), average case (~249750 swaps), worst case (499500 swaps), showing significant time increase.
- Times are averaged over 10 runs for accuracy; actual values depend on system performance.
How It Works
- bubbleSort: Sorts in ascending order, counting swaps (reused from
BasicBubbleSort.md). - generateBestCase: Creates sorted array [1, 2, ..., n].
- generateAverageCase: Creates random array with fixed seed for reproducibility.
- generateWorstCase: Creates reversed array [n, n-1, ..., 1].
- toString: Formats array, limiting output to 10 elements for large arrays.
- Main Method:
- Tests sizes 10, 100, 1000.
- For each size, runs best, average, and worst cases 10 times.
- Measures time with
System.nanoTime(), converts to milliseconds. - Reports average time and swaps.
- Example Trace (Size 10, Worst Case):
- Array: [10, 9, ..., 1].
- Pass 1: Multiple swaps to move 1 to end.
- Total swaps: 45 (n*(n-1)/2 for reversed array).
- Time measured per run, averaged over 10 runs.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| bubbleSort | O(n²) | O(1) |
| generateBestCase | O(n) | O(n) |
| generateAverageCase | O(n) | O(n) |
| generateWorstCase | O(n) | O(n) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for bubbleSort (n passes, up to n-1 comparisons/swaps); O(n) for generating arrays and toString.
- Space complexity: O(1) for bubbleSort (in-place); O(n) for generating arrays and toString (array storage and string builder).
- Worst case: O(n²) time for reversed arrays; best case: O(n) time for sorted arrays.
✅ Tip: Use
System.nanoTime()for precise timing in performance analysis. Average multiple runs to reduce variability from system noise. Fixed seeds in random generation ensure reproducible results.
⚠ Warning: Bubble Sort’s O(n²) complexity makes it slow for large arrays (e.g., n=1000). Limit output for large arrays to avoid overwhelming console logs.
Descending Bubble Sort
Problem Statement
Write a Java program that modifies the Bubble Sort implementation to sort an array of integers in descending order (largest to smallest). The program should count the number of swaps performed during sorting and test the implementation with arrays of different sizes and contents, including unsorted, already sorted (in descending order), reversed (ascending order), arrays with duplicate elements, and empty arrays. Bubble Sort repeatedly compares adjacent elements and swaps them if they are in the wrong order, bubbling smaller elements to the end for descending order. You can visualize this as bubbles sinking in a glass, pushing smaller numbers to the end with each pass.
Input:
- An array of integers to be sorted in descending order. Output: The sorted array (in descending order), the number of swaps performed, and a string representation of the input and sorted arrays for clarity. Constraints:
- The array length
nis between 0 and 10^5. - Array elements are integers in the range [-10^9, 10^9]. Example:
- Input: array = [64, 34, 25, 12, 22]
- Output:
- Input Array: [64, 34, 25, 12, 22]
- Sorted Array: [64, 34, 25, 22, 12]
- Swaps: 4
- Explanation: Bubble Sort performs passes, swapping adjacent elements if the first is smaller, resulting in 4 swaps.
- Input: array = [5, 4, 3, 2, 1]
- Output:
- Input Array: [5, 4, 3, 2, 1]
- Sorted Array: [5, 4, 3, 2, 1]
- Swaps: 0
- Explanation: Already sorted in descending order, no swaps needed.
Pseudocode
FUNCTION bubbleSortDescending(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
FOR j from 0 to n-i-2
IF arr[j] < arr[j+1] THEN
SET temp to arr[j]
SET arr[j] to arr[j+1]
SET arr[j+1] to temp
INCREMENT swaps
ENDIF
ENDFOR
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of input arrays
FOR each testCase in testCases
PRINT test case details
CREATE copy of testCase array
SET swaps to bubbleSortDescending(copy)
PRINT input array, sorted array, and swaps
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
bubbleSortDescending: a. Initialize a counterswapsto 0. b. For each pass i from 0 to n-1:- Compare adjacent elements j and j+1 from 0 to n-i-2.
- If arr[j] < arr[j+1], swap them and increment
swapsto sort in descending order. c. Return the number of swaps.
- Define
toString: a. Convert the array to a string, e.g., "[64, 34, 25, 22, 12]". - In
main, test with: a. An unsorted array (medium size, n=5). b. An already sorted array (descending, n=5). c. A reversed array (ascending, n=5). d. An array with duplicates (n=6). e. An empty array (n=0). f. A large array (n=10).
Java Implementation
import java.util.*;
public class DescendingBubbleSort {
// Performs Bubble Sort in descending order and counts swaps
public int bubbleSortDescending(int[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] < arr[j + 1]) { // Changed to < for descending order
// Swap elements
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swaps++;
}
}
}
return swaps;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
result.append(arr[i]);
if (i < arr.length - 1) {
result.append(", ");
}
}
result.append("]");
return result.toString();
}
// Helper class for test cases
static class TestCase {
int[] arr;
TestCase(int[] arr) {
this.arr = arr;
}
}
// Main method to test descending Bubble Sort
public static void main(String[] args) {
DescendingBubbleSort sorter = new DescendingBubbleSort();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{64, 34, 25, 12, 22}), // Unsorted, medium size
new TestCase(new int[]{5, 4, 3, 2, 1}), // Sorted (descending)
new TestCase(new int[]{1, 2, 3, 4, 5}), // Reversed (ascending)
new TestCase(new int[]{3, 1, 3, 2, 1, 2}), // Duplicates
new TestCase(new int[]{}), // Empty
new TestCase(new int[]{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}) // Large, reversed
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
int[] arr = testCases[i].arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
int swaps = sorter.bubbleSortDescending(arr);
System.out.println("Sorted Array: " + sorter.toString(arr));
System.out.println("Swaps: " + swaps + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input Array: [64, 34, 25, 12, 22]
Sorted Array: [64, 34, 25, 22, 12]
Swaps: 4
Test case 2:
Input Array: [5, 4, 3, 2, 1]
Sorted Array: [5, 4, 3, 2, 1]
Swaps: 0
Test case 3:
Input Array: [1, 2, 3, 4, 5]
Sorted Array: [5, 4, 3, 2, 1]
Swaps: 10
Test case 4:
Input Array: [3, 1, 3, 2, 1, 2]
Sorted Array: [3, 3, 2, 2, 1, 1]
Swaps: 7
Test case 5:
Input Array: []
Sorted Array: []
Swaps: 0
Test case 6:
Input Array: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
Sorted Array: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
Swaps: 0
Explanation:
- Test case 1: Unsorted array requires 4 swaps to sort in descending order.
- Test case 2: Already sorted in descending order, 0 swaps.
- Test case 3: Ascending array (reversed for descending) requires 10 swaps.
- Test case 4: Array with duplicates requires 7 swaps.
- Test case 5: Empty array requires 0 swaps.
- Test case 6: Large array (n=10), already sorted in descending order, 0 swaps.
How It Works
- bubbleSortDescending:
- Iterates through the array, comparing adjacent elements.
- Swaps if the first element is smaller (
arr[j] < arr[j+1]), incrementingswaps. - Each pass ensures the smallest unsorted element moves to its correct position.
- toString: Formats array as a string for output.
- Example Trace (Test case 1):
- Pass 1: [64, 34, 25, 12, 22] → [64, 34, 25, 22, 12] (1 swap: 12↔22).
- Pass 2: [64, 34, 25, 22, 12] → [64, 34, 25, 22, 12] (0 swaps).
- Pass 3: [64, 34, 25, 22, 12] → [64, 34, 25, 22, 12] (0 swaps).
- Pass 4: [64, 34, 25, 22, 12] → [64, 34, 25, 22, 12] (0 swaps).
- Pass 5: [64, 34, 25, 22, 12] → [64, 34, 25, 22, 12] (0 swaps). Total swaps: 4.
- Main Method: Tests unsorted, sorted (descending), reversed (ascending), duplicates, empty, and large arrays.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| bubbleSortDescending | O(n²) | O(1) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for bubbleSortDescending (n passes, up to n-1 comparisons/swaps each); O(n) for toString (iterate array).
- Space complexity: O(1) for bubbleSortDescending (in-place); O(n) for toString (string builder).
- Worst case: O(n²) time for ascending arrays; best case: O(n) time for descending arrays.
✅ Tip: Modify Bubble Sort for descending order by changing the comparison to
arr[j] < arr[j+1]. Use a swap counter to analyze the algorithm’s performance across different inputs.
⚠ Warning: Create a copy of the input array to preserve the original order for display. Handle empty arrays to avoid index errors in the sorting loop.
Basic Selection Sort
Problem Statement
Write a Java program that implements the Selection Sort algorithm to sort an array of integers in ascending order. The program should count the number of swaps performed during sorting and test the implementation with various input arrays, including unsorted, already sorted, reversed, and arrays with duplicate elements. Selection Sort repeatedly finds the minimum element from the unsorted portion of the array and swaps it with the first unsorted element, building the sorted portion from the start. You can visualize this as selecting the smallest item from a pile and placing it at the front, repeating until the pile is sorted.
Input:
- An array of integers to be sorted. Output: The sorted array, the number of swaps performed, and a string representation of the input and sorted arrays for clarity. Constraints:
- The array length
nis between 0 and 10^5. - Array elements are integers in the range [-10^9, 10^9]. Example:
- Input: array = [64, 34, 25, 12, 22]
- Output:
- Input Array: [64, 34, 25, 12, 22]
- Sorted Array: [12, 22, 25, 34, 64]
- Swaps: 4
- Explanation: Selection Sort finds the minimum in each pass, swapping it to the front, resulting in 4 swaps.
- Input: array = [1, 2, 3]
- Output:
- Input Array: [1, 2, 3]
- Sorted Array: [1, 2, 3]
- Swaps: 0
- Explanation: Already sorted array requires no swaps.
Pseudocode
FUNCTION selectionSort(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
SET minIdx to i
FOR j from i+1 to n-1
IF arr[j] < arr[minIdx] THEN
SET minIdx to j
ENDIF
ENDFOR
IF minIdx != i THEN
SET temp to arr[i]
SET arr[i] to arr[minIdx]
SET arr[minIdx] to temp
INCREMENT swaps
ENDIF
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of input arrays
FOR each testCase in testCases
PRINT test case details
CREATE copy of testCase array
SET swaps to selectionSort(copy)
PRINT input array, sorted array, and swaps
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
selectionSort: a. Initialize a counterswapsto 0. b. For each index i from 0 to n-1:- Find the minimum element’s index
minIdxin arr[i..n-1]. - If
minIdx!= i, swap arr[i] with arr[minIdx] and incrementswaps. c. Return the number of swaps.
- Find the minimum element’s index
- Define
toString: a. Convert the array to a string, e.g., "[64, 34, 25, 12, 22]". - In
main, test with: a. An unsorted array. b. An already sorted array. c. A reversed array. d. An array with duplicates. e. An empty array.
Java Implementation
import java.util.*;
public class BasicSelectionSort {
// Performs Selection Sort and counts swaps
public int selectionSort(int[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
int minIdx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
if (minIdx != i) {
// Swap elements
int temp = arr[i];
arr[i] = arr[minIdx];
arr[minIdx] = temp;
swaps++;
}
}
return swaps;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
result.append(arr[i]);
if (i < arr.length - 1) {
result.append(", ");
}
}
result.append("]");
return result.toString();
}
// Helper class for test cases
static class TestCase {
int[] arr;
TestCase(int[] arr) {
this.arr = arr;
}
}
// Main method to test Selection Sort
public static void main(String[] args) {
BasicSelectionSort sorter = new BasicSelectionSort();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{64, 34, 25, 12, 22}), // Unsorted
new TestCase(new int[]{1, 2, 3, 4, 5}), // Sorted
new TestCase(new int[]{5, 4, 3, 2, 1}), // Reversed
new TestCase(new int[]{3, 1, 3, 2, 1}), // Duplicates
new TestCase(new int[]{}) // Empty
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ":");
int[] arr = testCases[i].arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
int swaps = sorter.selectionSort(arr);
System.out.println("Sorted Array: " + sorter.toString(arr));
System.out.println("Swaps: " + swaps + "\n");
}
}
}
Output
Running the main method produces:
Test case 1:
Input Array: [64, 34, 25, 12, 22]
Sorted Array: [12, 22, 25, 34, 64]
Swaps: 4
Test case 2:
Input Array: [1, 2, 3, 4, 5]
Sorted Array: [1, 2, 3, 4, 5]
Swaps: 0
Test case 3:
Input Array: [5, 4, 3, 2, 1]
Sorted Array: [1, 2, 3, 4, 5]
Swaps: 4
Test case 4:
Input Array: [3, 1, 3, 2, 1]
Sorted Array: [1, 1, 2, 3, 3]
Swaps: 3
Test case 5:
Input Array: []
Sorted Array: []
Swaps: 0
Explanation:
- Test case 1: Unsorted array requires 4 swaps to sort.
- Test case 2: Already sorted array requires 0 swaps.
- Test case 3: Reversed array requires 4 swaps.
- Test case 4: Array with duplicates requires 3 swaps.
- Test case 5: Empty array requires 0 swaps.
How It Works
- selectionSort:
- Iterates through the array, finding the minimum element in the unsorted portion.
- Swaps the minimum with the first unsorted element if needed, incrementing
swaps. - Builds the sorted portion from the start.
- toString: Formats array as a string for output.
- Example Trace (Test case 1):
- Pass 1: Find min=12 at index 3, swap with 64: [12, 34, 25, 64, 22] (1 swap).
- Pass 2: Find min=22 at index 4, swap with 34: [12, 22, 25, 64, 34] (1 swap).
- Pass 3: Find min=25 at index 2, no swap: [12, 22, 25, 64, 34].
- Pass 4: Find min=34 at index 4, swap with 64: [12, 22, 25, 34, 64] (1 swap).
- Pass 5: Find min=64, no swap. Total swaps: 4.
- Main Method: Tests unsorted, sorted, reversed, duplicates, and empty arrays.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| selectionSort | O(n²) | O(1) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for selectionSort (n passes, up to n comparisons each); O(n) for toString (iterate array).
- Space complexity: O(1) for selectionSort (in-place); O(n) for toString (string builder).
- Selection Sort always performs O(n²) comparisons, but swaps are O(n) in the worst case.
✅ Tip: Selection Sort is simple and in-place, ideal for small arrays or when minimizing swaps is important. Use it when write operations are costly compared to comparisons.
⚠ Warning: Ensure the array is copied before sorting to preserve the original for display. Handle empty arrays to avoid index issues in the sorting loop.
Descending Selection Sort
Problem Statement
Write a Java program that modifies the Selection Sort implementation to sort an array of integers in descending order (largest to smallest) by selecting the maximum element in each pass instead of the minimum. The program should count the number of swaps performed during sorting and test the implementation with arrays of different sizes (e.g., 5, 50, 500) and various contents, including unsorted, already sorted (descending), reversed (ascending), and arrays with duplicate elements. Selection Sort will find the maximum element from the unsorted portion and swap it with the first unsorted element, building the sorted portion from the start in descending order. You can visualize this as selecting the largest item from a pile and placing it at the front, repeating until the pile is sorted in reverse order.
Input:
- An array of integers to be sorted in descending order. Output: The sorted array (in descending order), the number of swaps performed, and a string representation of the input and sorted arrays for clarity. Constraints:
- The array length
nis between 0 and 10^5. - Array elements are integers in the range [-10^9, 10^9]. Example:
- Input: array = [64, 34, 25, 12, 22]
- Output:
- Input Array: [64, 34, 25, 12, 22]
- Sorted Array: [64, 34, 25, 22, 12]
- Swaps: 3
- Explanation: Selection Sort finds the maximum in each pass, swapping it to the front, resulting in 3 swaps.
- Input: array = [5, 4, 3]
- Output:
- Input Array: [5, 4, 3]
- Sorted Array: [5, 4, 3]
- Swaps: 0
- Explanation: Already sorted in descending order, no swaps needed.
Pseudocode
FUNCTION selectionSortDescending(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
SET maxIdx to i
FOR j from i+1 to n-1
IF arr[j] > arr[maxIdx] THEN
SET maxIdx to j
ENDIF
ENDFOR
IF maxIdx != i THEN
SET temp to arr[i]
SET arr[i] to arr[maxIdx]
SET arr[maxIdx] to temp
INCREMENT swaps
ENDIF
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of input arrays with different sizes
FOR each testCase in testCases
PRINT test case details
CREATE copy of testCase array
SET swaps to selectionSortDescending(copy)
PRINT input array, sorted array, and swaps
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
selectionSortDescending: a. Initialize a counterswapsto 0. b. For each index i from 0 to n-1:- Find the maximum element’s index
maxIdxin arr[i..n-1]. - If
maxIdx!= i, swap arr[i] with arr[maxIdx] and incrementswaps. c. Return the number of swaps.
- Find the maximum element’s index
- Define
toString: a. Convert the array to a string, e.g., "[64, 34, 25, 22, 12]". - In
main, test with: a. Small unsorted array (n=5). b. Small sorted array (descending, n=5). c. Small reversed array (ascending, n=5). d. Medium array with duplicates (n=50). e. Large unsorted array (n=500). f. Empty array (n=0).
Java Implementation
import java.util.*;
public class DescendingSelectionSort {
// Performs Selection Sort in descending order and counts swaps
public int selectionSortDescending(int[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
int maxIdx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] > arr[maxIdx]) {
maxIdx = j;
}
}
if (maxIdx != i) {
// Swap elements
int temp = arr[i];
arr[i] = arr[maxIdx];
arr[maxIdx] = temp;
swaps++;
}
}
return swaps;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Generates random array for testing
private int[] generateRandomArray(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(2001) - 1000; // [-1000, 1000]
}
return arr;
}
// Generates sorted array (descending) for testing
private int[] generateSortedDescending(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = n - i;
}
return arr;
}
// Generates array with duplicates
private int[] generateDuplicatesArray(int n) {
Random rand = new Random(42);
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(10); // Limited range for duplicates
}
return arr;
}
// Helper class for test cases
static class TestCase {
int[] arr;
String description;
TestCase(int[] arr, String description) {
this.arr = arr;
this.description = description;
}
}
// Main method to test descending Selection Sort
public static void main(String[] args) {
DescendingSelectionSort sorter = new DescendingSelectionSort();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{64, 34, 25, 12, 22}, "Small unsorted (n=5)"),
new TestCase(sorter.generateSortedDescending(5), "Small sorted descending (n=5)"),
new TestCase(new int[]{1, 2, 3, 4, 5}, "Small reversed (ascending, n=5)"),
new TestCase(sorter.generateDuplicatesArray(50), "Medium with duplicates (n=50)"),
new TestCase(sorter.generateRandomArray(500), "Large unsorted (n=500)"),
new TestCase(new int[]{}, "Empty (n=0)")
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
int[] arr = testCases[i].arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
int swaps = sorter.selectionSortDescending(arr);
System.out.println("Sorted Array: " + sorter.toString(arr));
System.out.println("Swaps: " + swaps + "\n");
}
}
}
Output
Running the main method produces:
Test case 1: Small unsorted (n=5)
Input Array: [64, 34, 25, 12, 22]
Sorted Array: [64, 34, 25, 22, 12]
Swaps: 3
Test case 2: Small sorted descending (n=5)
Input Array: [5, 4, 3, 2, 1]
Sorted Array: [5, 4, 3, 2, 1]
Swaps: 0
Test case 3: Small reversed (ascending, n=5)
Input Array: [1, 2, 3, 4, 5]
Sorted Array: [5, 4, 3, 2, 1]
Swaps: 4
Test case 4: Medium with duplicates (n=50)
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Sorted Array: [9, 9, 9, 9, 8, 8, 8, 8, 8, 8, ...]
Swaps: 40
Test case 5: Large unsorted (n=500)
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628, ...]
Sorted Array: [976, 966, 964, 960, 958, 955, 953, 952, 951, 946, ...]
Swaps: 492
Test case 6: Empty (n=0)
Input Array: []
Sorted Array: []
Swaps: 0
Explanation:
- Test case 1: Unsorted array (n=5) requires 3 swaps to sort in descending order.
- Test case 2: Already sorted in descending order (n=5), 0 swaps.
- Test case 3: Ascending array (reversed for descending, n=5) requires 4 swaps.
- Test case 4: Medium array with duplicates (n=50) requires 40 swaps.
- Test case 5: Large unsorted array (n=500) requires 492 swaps.
- Test case 6: Empty array (n=0) requires 0 swaps.
How It Works
- selectionSortDescending:
- Iterates through the array, finding the maximum element in the unsorted portion.
- Swaps the maximum with the first unsorted element if needed, incrementing
swaps. - Builds the sorted portion in descending order from the start.
- toString: Formats array as a string, limiting output to 10 elements for large arrays.
- generateRandomArray: Creates an array with random integers in [-1000, 1000].
- generateSortedDescending: Creates a descending array [n, n-1, ..., 1].
- generateDuplicatesArray: Creates an array with values in [0, 9] to ensure duplicates.
- Example Trace (Test case 1):
- Pass 1: Find max=64 at index 0, no swap: [64, 34, 25, 12, 22].
- Pass 2: Find max=34 at index 1, no swap: [64, 34, 25, 12, 22].
- Pass 3: Find max=25 at index 2, no swap: [64, 34, 25, 12, 22].
- Pass 4: Find max=22 at index 4, swap with 12: [64, 34, 25, 22, 12] (1 swap).
- Pass 5: Find max=12, no swap. Total swaps: 3.
- Main Method: Tests small, medium, and large arrays with various contents.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| selectionSortDescending | O(n²) | O(1) |
| toString | O(n) | O(n) |
| generateRandomArray | O(n) | O(n) |
| generateSortedDescending | O(n) | O(n) |
| generateDuplicatesArray | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for selectionSortDescending (n passes, up to n comparisons each); O(n) for toString and array generation.
- Space complexity: O(1) for selectionSortDescending (in-place); O(n) for toString and array generation (string builder and arrays).
- Selection Sort always performs O(n²) comparisons, with swaps O(n) in the worst case.
✅ Tip: Modify Selection Sort for descending order by selecting the maximum element instead of the minimum. Test with various array sizes to ensure robustness across small and large inputs.
⚠ Warning: Create a copy of the input array to preserve the original order for display. Handle empty arrays to avoid index errors in the sorting loop.
Selection Sort Minimum Swap Count
Problem Statement
Write a Java program that enhances the Selection Sort implementation to explicitly track and return the number of swaps performed while sorting an array of integers in ascending order. The program should test with nearly sorted arrays (e.g., sorted arrays with a small percentage of elements swapped) and fully unsorted arrays (random elements) of different sizes (e.g., 10, 100). Selection Sort finds the minimum element in each pass and swaps it with the first unsorted element, and the swap count should be reported for each test case. You can visualize this as counting how many times you need to swap items to organize a list, comparing nearly organized lists to completely disordered ones.
Input:
- Arrays of integers with sizes 10 and 100, generated as nearly sorted and fully unsorted. Output: The sorted array, the number of swaps performed, and a string representation of the input and sorted arrays for clarity. Constraints:
- Array sizes are 10 and 100.
- Array elements are integers in the range [0, 10^6] for random cases.
- Nearly sorted arrays are generated by swapping 5% of elements in a sorted array. Example:
- Input: Nearly sorted, n=10: [1, 2, 5, 4, 3, 6, 7, 8, 9, 10]
- Output:
- Input Array: [1, 2, 5, 4, 3, 6, 7, 8, 9, 10]
- Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
- Swaps: 2
- Explanation: Nearly sorted array requires 2 swaps to sort.
- Input: Fully unsorted, n=10: [5, 2, 8, 1, 9, 3, 7, 4, 6, 10]
- Output:
- Input Array: [5, 2, 8, 1, 9, 3, 7, 4, 6, 10]
- Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
- Swaps: 4
- Explanation: Fully unsorted array requires 4 swaps.
Pseudocode
FUNCTION selectionSort(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
SET minIdx to i
FOR j from i+1 to n-1
IF arr[j] < arr[minIdx] THEN
SET minIdx to j
ENDIF
ENDFOR
IF minIdx != i THEN
SET temp to arr[i]
SET arr[i] to arr[minIdx]
SET arr[minIdx] to temp
INCREMENT swaps
ENDIF
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION generateNearlySorted(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to i + 1
ENDFOR
SET numSwaps to floor(n * 0.05)
FOR i from 0 to numSwaps-1
SET idx1 to random integer in [0, n-1]
SET idx2 to random integer in [0, n-1]
SWAP arr[idx1] and arr[idx2]
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION generateFullyUnsorted(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to random integer in [0, 10^6]
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [10, 100]
FOR each size in sizes
SET nearArr to generateNearlySorted(size)
SET unsortedArr to generateFullyUnsorted(size)
FOR each testCase (nearArr, unsortedArr)
PRINT test case details
CREATE copy of testCase array
SET swaps to selectionSort(copy)
PRINT input array, sorted array, and swaps
ENDFOR
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
selectionSort(generic): a. Accept an array ofComparable<T>type (useIntegerfor testing). b. Initialize a counterswapsto 0. c. For each index i from 0 to n-1, find the minimum element’s index in arr[i..n-1]. d. Swap if needed, incrementswaps, and return the count. - Define
generateNearlySorted: a. Create sorted array [1, 2, ..., n]. b. Swap 5% of elements randomly to introduce minor disorder. - Define
generateFullyUnsorted: a. Create array with random integers in [0, 10^6]. - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Nearly sorted arrays (n=10, 100). b. Fully unsorted arrays (n=10, 100).
Java Implementation
import java.util.*;
public class SelectionSortMinimumSwapCount {
// Performs Selection Sort for Comparable types and counts swaps
public <T extends Comparable<T>> int selectionSort(T[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
int minIdx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j].compareTo(arr[minIdx]) < 0) {
minIdx = j;
}
}
if (minIdx != i) {
T temp = arr[i];
arr[i] = arr[minIdx];
arr[minIdx] = temp;
swaps++;
}
}
return swaps;
}
// Generates nearly sorted array
private Integer[] generateNearlySorted(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
Integer[] arr = new Integer[n];
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
int numSwaps = (int) (n * 0.05); // 5% of elements
for (int i = 0; i < numSwaps; i++) {
int idx1 = rand.nextInt(n);
int idx2 = rand.nextInt(n);
Integer temp = arr[idx1];
arr[idx1] = arr[idx2];
arr[idx2] = temp;
}
return arr;
}
// Generates fully unsorted array
private Integer[] generateFullyUnsorted(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
Integer[] arr = new Integer[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(1000001); // [0, 10^6]
}
return arr;
}
// Converts array to string
public <T> String toString(T[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
Integer[] arr;
String description;
TestCase(Integer[] arr, String description) {
this.arr = arr;
this.description = description;
}
}
// Main method to test swap counting
public static void main(String[] args) {
SelectionSortMinimumSwapCount sorter = new SelectionSortMinimumSwapCount();
int[] sizes = {10, 100};
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
TestCase[] cases = {
new TestCase(sorter.generateNearlySorted(size), "Nearly Sorted"),
new TestCase(sorter.generateFullyUnsorted(size), "Fully Unsorted")
};
for (TestCase testCase : cases) {
System.out.println(testCase.description + ":");
Integer[] arr = testCase.arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
int swaps = sorter.selectionSort(arr);
System.out.println("Sorted Array: " + sorter.toString(arr));
System.out.println("Swaps: " + swaps + "\n");
}
}
}
}
Output
Running the main method produces:
Array Size: 10
Nearly Sorted:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Swaps: 0
Fully Unsorted:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178]
Sorted Array: [333, 360, 289796, 304135, 374316, 628178, 648054, 727595, 766336, 767890]
Swaps: 4
Array Size: 100
Nearly Sorted:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Swaps: 5
Fully Unsorted:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178, ...]
Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
Swaps: 48
Explanation:
- Size 10, Nearly Sorted: Already sorted due to small size, 0 swaps.
- Size 10, Fully Unsorted: Random array, 4 swaps to sort.
- Size 100, Nearly Sorted: 5% swaps (5 swaps) to correct minor disorder.
- Size 100, Fully Unsorted: ~48 swaps for random array.
- Nearly sorted arrays require fewer swaps than fully unsorted ones.
How It Works
- selectionSort:
- Uses generics with
Comparable<T>to handleIntegerarrays. - Finds the minimum element in each pass, swaps if needed, and counts swaps.
- Uses generics with
- generateNearlySorted: Creates sorted array, swaps 5% of elements randomly.
- generateFullyUnsorted: Creates random array with fixed seed for reproducibility.
- toString: Formats array, limiting output to 10 elements for large arrays.
- Example Trace (Size 10, Fully Unsorted):
- Pass 1: Find min=333, swap with 727595: [333, ..., 727595] (1 swap).
- Pass 2: Find min=360, swap: [333, 360, ...] (1 swap).
- Total swaps: 4.
- Main Method: Tests nearly sorted and fully unsorted arrays for sizes 10 and 100.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| selectionSort | O(n²) | O(1) |
| generateNearlySorted | O(n) | O(n) |
| generateFullyUnsorted | O(n) | O(n) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for selectionSort (n passes, up to n comparisons); O(n) for array generation and toString.
- Space complexity: O(1) for selectionSort (in-place); O(n) for array generation and toString (array and string builder).
- Nearly sorted arrays reduce swaps but not comparisons.
✅ Tip: Selection Sort’s swap count is lower for nearly sorted arrays, making it efficient for write operations. Use a generic implementation to support multiple data types.
⚠ Warning: Ensure identical input arrays for fair swap comparisons. Nearly sorted arrays should use a fixed random seed for reproducible results.
Selection Sort Performance Analysis
Problem Statement
Write a Java program that measures the execution time of the Selection Sort algorithm for sorting arrays of integers in ascending order, testing arrays of increasing sizes (e.g., 10, 100, 1000 elements). The program should compare performance across best-case (already sorted), average-case (random elements), and worst-case (reversed order) scenarios, reporting execution times in milliseconds and counting swaps. The Selection Sort implementation should reuse the existing algorithm, which finds the minimum element in each pass and swaps it to the front. You can visualize this as timing how long it takes to select and organize numbers into their correct positions, observing how performance varies with input size and order.
Input:
- Arrays of integers with sizes 10, 100, and 1000, generated for best (sorted), average (random), and worst (reversed) cases. Output: The execution time (in milliseconds) and number of swaps for each array size and case, along with a string representation of the input and sorted arrays for verification. Constraints:
- Array sizes are 10, 100, 1000.
- Array elements are integers in the range [0, 10^6] for random cases.
- Execution times are averaged over multiple runs for accuracy. Example:
- Input: Array size = 10, Cases: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] (best), [5, 2, 8, 1, 9, 3, 7, 4, 6, 10] (average), [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] (worst)
- Output (example, times vary):
- Size 10, Best Case: Time = 0.03 ms, Swaps = 0
- Size 10, Average Case: Time = 0.04 ms, Swaps = 4
- Size 10, Worst Case: Time = 0.04 ms, Swaps = 4
- Explanation: Best case requires no swaps, average and worst cases have similar swaps, with times consistent due to fixed comparisons.
Pseudocode
FUNCTION selectionSort(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
SET minIdx to i
FOR j from i+1 to n-1
IF arr[j] < arr[minIdx] THEN
SET minIdx to j
ENDIF
ENDFOR
IF minIdx != i THEN
SET temp to arr[i]
SET arr[i] to arr[minIdx]
SET arr[minIdx] to temp
INCREMENT swaps
ENDIF
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION generateBestCase(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to i + 1
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION generateAverageCase(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to random integer in [0, 10^6]
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION generateWorstCase(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to n - i
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [10, 100, 1000]
SET runs to 10
FOR each size in sizes
SET bestArr to generateBestCase(size)
SET avgArr to generateAverageCase(size)
SET worstArr to generateWorstCase(size)
FOR each case (bestArr, avgArr, worstArr)
SET totalTime to 0
SET totalSwaps to 0
FOR i from 0 to runs-1
CREATE copy of case array
SET startTime to current nano time
SET swaps to selectionSort(copy)
SET endTime to current nano time
ADD (endTime - startTime) to totalTime
ADD swaps to totalSwaps
ENDFOR
PRINT case details, input array, sorted array
PRINT average time in milliseconds and average swaps
ENDFOR
ENDFOR
ENDFUNCTION
Algorithm Steps
- Reuse
selectionSort: a. Initialize a counterswapsto 0. b. For each index i from 0 to n-1, find the minimum element’s index in arr[i..n-1]. c. Swap if needed, incrementswaps, and return the count. - Define
generateBestCase: a. Create array [1, 2, ..., n] (already sorted). - Define
generateAverageCase: a. Create array with random integers in [0, 10^6]. - Define
generateWorstCase: a. Create array [n, n-1, ..., 1] (reversed). - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Array sizes: 10, 100, 1000. b. Cases: best (sorted), average (random), worst (reversed). c. Run each case 10 times, average times and swaps. d. Measure time usingSystem.nanoTime(), convert to milliseconds.
Java Implementation
import java.util.*;
public class SelectionSortPerformanceAnalysis {
// Performs Selection Sort and counts swaps
public int selectionSort(int[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
int minIdx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
if (minIdx != i) {
int temp = arr[i];
arr[i] = arr[minIdx];
arr[minIdx] = temp;
swaps++;
}
}
return swaps;
}
// Generates best-case array (sorted)
private int[] generateBestCase(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
return arr;
}
// Generates average-case array (random)
private int[] generateAverageCase(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(1000001); // [0, 10^6]
}
return arr;
}
// Generates worst-case array (reversed)
private int[] generateWorstCase(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = n - i;
}
return arr;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
int size;
String type;
int[] arr;
TestCase(int size, String type, int[] arr) {
this.size = size;
this.type = type;
this.arr = arr;
}
}
// Main method to test performance
public static void main(String[] args) {
SelectionSortPerformanceAnalysis sorter = new SelectionSortPerformanceAnalysis();
int[] sizes = {10, 100, 1000};
int runs = 10;
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
TestCase[] cases = {
new TestCase(size, "Best Case", sorter.generateBestCase(size)),
new TestCase(size, "Average Case", sorter.generateAverageCase(size)),
new TestCase(size, "Worst Case", sorter.generateWorstCase(size))
};
for (TestCase testCase : cases) {
long totalTime = 0;
long totalSwaps = 0;
for (int i = 0; i < runs; i++) {
int[] arr = testCase.arr.clone();
long startTime = System.nanoTime();
int swaps = sorter.selectionSort(arr);
long endTime = System.nanoTime();
totalTime += (endTime - startTime);
totalSwaps += swaps;
}
double avgTimeMs = totalTime / (double) runs / 1_000_000.0; // Convert to ms
double avgSwaps = totalSwaps / (double) runs;
System.out.println(testCase.type + ":");
System.out.println("Input Array: " + sorter.toString(testCase.arr));
int[] sorted = testCase.arr.clone();
sorter.selectionSort(sorted);
System.out.println("Sorted Array: " + sorter.toString(sorted));
System.out.printf("Average Time: %.2f ms\n", avgTimeMs);
System.out.printf("Average Swaps: %.0f\n\n", avgSwaps);
}
}
}
}
Output
Running the main method produces (times vary by system, example shown):
Array Size: 10
Best Case:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Average Time: 0.03 ms
Average Swaps: 0
Average Case:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178]
Sorted Array: [333, 360, 289796, 304135, 374316, 628178, 648054, 727595, 766336, 767890]
Average Time: 0.04 ms
Average Swaps: 4
Worst Case:
Input Array: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Average Time: 0.04 ms
Average Swaps: 4
Array Size: 100
Best Case:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Average Time: 0.20 ms
Average Swaps: 0
Average Case:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178, ...]
Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
Average Time: 0.25 ms
Average Swaps: 48
Worst Case:
Input Array: [100, 99, 98, 97, 96, 95, 94, 93, 92, 91, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Average Time: 0.26 ms
Average Swaps: 49
Array Size: 1000
Best Case:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Average Time: 2.00 ms
Average Swaps: 0
Average Case:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178, ...]
Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
Average Time: 2.50 ms
Average Swaps: 496
Worst Case:
Input Array: [1000, 999, 998, 997, 996, 995, 994, 993, 992, 991, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Average Time: 2.60 ms
Average Swaps: 499
Explanation:
- Size 10: Best case (0 swaps, minimal time), average and worst cases (~4 swaps, similar times due to fixed comparisons).
- Size 100: Best case (0 swaps), average (~48 swaps), worst case (~49 swaps), with times scaling quadratically.
- Size 1000: Best case (0 swaps), average (~496 swaps), worst case (~499 swaps), showing consistent time increase.
- Times are averaged over 10 runs; Selection Sort’s time is driven by O(n²) comparisons, not swaps.
How It Works
- selectionSort: Finds the minimum element in each pass, swaps it to the front, counts swaps (reused from
BasicSelectionSort.md). - generateBestCase: Creates sorted array [1, 2, ..., n].
- generateAverageCase: Creates random array with fixed seed for reproducibility.
- generateWorstCase: Creates reversed array [n, n-1, ..., 1].
- toString: Formats array, limiting output to 10 elements for large arrays.
- Main Method:
- Tests sizes 10, 100, 1000.
- For each size, runs best, average, and worst cases 10 times.
- Measures time with
System.nanoTime(), converts to milliseconds. - Reports average time and swaps.
- Example Trace (Size 10, Worst Case):
- Array: [10, 9, ..., 1].
- Pass 1: Find min=1, swap with 10: [1, 9, ..., 2] (1 swap).
- Total swaps: ~4. Time measured per run, averaged over 10 runs.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| selectionSort | O(n²) | O(1) |
| generateBestCase | O(n) | O(n) |
| generateAverageCase | O(n) | O(n) |
| generateWorstCase | O(n) | O(n) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for selectionSort (n passes, up to n comparisons each); O(n) for generating arrays and toString.
- Space complexity: O(1) for selectionSort (in-place); O(n) for generating arrays and toString (array storage and string builder).
- Selection Sort’s time is consistent across cases due to fixed O(n²) comparisons.
✅ Tip: Selection Sort’s performance is driven by comparisons, not swaps, making times similar across cases. Use
System.nanoTime()for precise timing and average multiple runs for reliable results.
⚠ Warning: Selection Sort’s O(n²) complexity makes it slow for large arrays (e.g., n=1000). Limit output for large arrays to avoid overwhelming console logs.
Selection Sort for String Arrays
Problem Statement
Write a Java program that extends the Selection Sort implementation to sort an array of strings lexicographically in ascending order (based on Unicode values, case-sensitive). The program should count the number of swaps performed and test with string arrays containing strings of varying lengths (short, medium, long) and cases (mixed upper/lowercase, all lowercase), including edge cases like empty arrays and arrays with duplicate strings. Selection Sort will find the lexicographically smallest string in each pass and swap it with the first unsorted element. You can visualize this as organizing a list of words alphabetically, selecting the "smallest" word (e.g., "apple" before "Banana") to build the sorted list.
Input:
- An array of strings to be sorted lexicographically. Output: The sorted array, the number of swaps performed, and a string representation of the input and sorted arrays for clarity. Constraints:
- The array length
nis between 0 and 10^5. - Strings can be of any length and contain any valid Unicode characters. Example:
- Input: array = ["banana", "Apple", "cherry", "date"]
- Output:
- Input Array: [banana, Apple, cherry, date]
- Sorted Array: [Apple, banana, cherry, date]
- Swaps: 2
- Explanation: Selection Sort sorts lexicographically (case-sensitive, "Apple" < "banana"), requiring 2 swaps.
- Input: array = ["cat", "Cat", "CAT"]
- Output:
- Input Array: [cat, Cat, CAT]
- Sorted Array: [CAT, Cat, cat]
- Swaps: 2
- Explanation: Case-sensitive sorting places uppercase before lowercase, 2 swaps.
Pseudocode
FUNCTION selectionSort(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
SET minIdx to i
FOR j from i+1 to n-1
IF arr[j] < arr[minIdx] THEN
SET minIdx to j
ENDIF
ENDFOR
IF minIdx != i THEN
SET temp to arr[i]
SET arr[i] to arr[minIdx]
SET arr[minIdx] to temp
INCREMENT swaps
ENDIF
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of input string arrays
FOR each testCase in testCases
PRINT test case details
CREATE copy of testCase array
SET swaps to selectionSort(copy)
PRINT input array, sorted array, and swaps
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
selectionSort(generic): a. Accept an array ofComparable<T>type (useStringfor testing). b. Initialize a counterswapsto 0. c. For each index i from 0 to n-1:- Find the index
minIdxof the lexicographically smallest element in arr[i..n-1] usingcompareTo. - If
minIdx!= i, swap arr[i] with arr[minIdx] and incrementswaps. d. Return the number of swaps.
- Find the index
- Define
toString: a. Convert the array to a string, e.g., "[banana, Apple, cherry]". - In
main, test with: a. Mixed case strings (short, n=4). b. Mixed case strings with duplicates (n=5). c. Long strings (n=6). d. Empty array (n=0). e. Single-element array (n=1). f. All lowercase strings (n=7).
Java Implementation
import java.util.*;
public class SelectionSortStringArray {
// Performs Selection Sort for Comparable types and counts swaps
public <T extends Comparable<T>> int selectionSort(T[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
int minIdx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j].compareTo(arr[minIdx]) < 0) {
minIdx = j;
}
}
if (minIdx != i) {
T temp = arr[i];
arr[i] = arr[minIdx];
arr[minIdx] = temp;
swaps++;
}
}
return swaps;
}
// Converts array to string
public <T> String toString(T[] arr) {
StringBuilder result = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
result.append(arr[i]);
if (i < arr.length - 1) {
result.append(", ");
}
}
result.append("]");
return result.toString();
}
// Helper class for test cases
static class TestCase {
String[] arr;
String description;
TestCase(String[] arr, String description) {
this.arr = arr;
this.description = description;
}
}
// Main method to test string Selection Sort
public static void main(String[] args) {
SelectionSortStringArray sorter = new SelectionSortStringArray();
// Test cases
TestCase[] testCases = {
new TestCase(new String[]{"banana", "Apple", "cherry", "date"}, "Mixed case (n=4)"),
new TestCase(new String[]{"cat", "Cat", "CAT", "cat", "Dog"}, "Mixed case with duplicates (n=5)"),
new TestCase(new String[]{"elephant", "giraffe", "hippopotamus", "rhinoceros", "zebra", "antelope"}, "Long strings (n=6)"),
new TestCase(new String[]{}, "Empty (n=0)"),
new TestCase(new String[]{"single"}, "Single element (n=1)"),
new TestCase(new String[]{"apple", "banana", "cherry", "date", "elderberry", "fig", "grape"}, "All lowercase (n=7)")
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
String[] arr = testCases[i].arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
int swaps = sorter.selectionSort(arr);
System.out.println("Sorted Array: " + sorter.toString(arr));
System.out.println("Swaps: " + swaps + "\n");
}
}
}
Output
Running the main method produces:
Test case 1: Mixed case (n=4)
Input Array: [banana, Apple, cherry, date]
Sorted Array: [Apple, banana, cherry, date]
Swaps: 2
Test case 2: Mixed case with duplicates (n=5)
Input Array: [cat, Cat, CAT, cat, Dog]
Sorted Array: [CAT, Cat, Dog, cat, cat]
Swaps: 4
Test case 3: Long strings (n=6)
Input Array: [elephant, giraffe, hippopotamus, rhinoceros, zebra, antelope]
Sorted Array: [antelope, elephant, giraffe, hippopotamus, rhinoceros, zebra]
Swaps: 5
Test case 4: Empty (n=0)
Input Array: []
Sorted Array: []
Swaps: 0
Test case 5: Single element (n=1)
Input Array: [single]
Sorted Array: [single]
Swaps: 0
Test case 6: All lowercase (n=7)
Input Array: [apple, banana, cherry, date, elderberry, fig, grape]
Sorted Array: [apple, banana, cherry, date, elderberry, fig, grape]
Swaps: 0
Explanation:
- Test case 1: Mixed case strings, 2 swaps ("Apple" before "banana" due to case).
- Test case 2: Mixed case with duplicates, 4 swaps (uppercase "CAT" first).
- Test case 3: Long strings, 5 swaps to sort lexicographically.
- Test case 4: Empty array, 0 swaps.
- Test case 5: Single element, 0 swaps.
- Test case 6: Already sorted lowercase strings, 0 swaps.
How It Works
- selectionSort:
- Uses generics with
Comparable<T>to handleStringarrays. - Finds the lexicographically smallest string in each pass using
compareTo. - Swaps if needed, counts swaps, and builds the sorted array.
- Uses generics with
- toString: Formats array as a string, handling
Stringelements. - Example Trace (Test case 1):
- Pass 1: Find min="Apple" at index 1, swap with "banana": [Apple, banana, cherry, date] (1 swap).
- Pass 2: Find min="banana", no swap: [Apple, banana, cherry, date].
- Pass 3: Find min="cherry", no swap: [Apple, banana, cherry, date].
- Pass 4: Find min="date", no swap. Total swaps: 2.
- Main Method: Tests mixed case, duplicates, long strings, empty, single element, and lowercase arrays.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| selectionSort | O(n²) | O(1) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for selectionSort (n passes, up to n comparisons each); O(n) for toString. String comparisons depend on string length, but this is not included in standard analysis.
- Space complexity: O(1) for selectionSort (in-place); O(n) for toString (string builder).
- Selection Sort performs O(n²) comparisons, with swaps O(n) in the worst case.
✅ Tip: Use generics with
Comparableto extend Selection Sort to strings, leveragingcompareTofor lexicographical ordering. Test with mixed cases to understand case-sensitive sorting.
⚠ Warning: Java’s
compareTois case-sensitive (uppercase before lowercase). For case-insensitive sorting, usecompareToIgnoreCase. Handle empty arrays to avoid index errors.
Basic Insertion Sort
Problem Statement
Write a Java program that implements the Insertion Sort algorithm to sort an array of integers in ascending order. The program should count the number of shifts performed (the number of times elements are moved to make space for insertion) and test the implementation with various input arrays, including unsorted, already sorted, reversed, and arrays with duplicate elements. Insertion Sort builds a sorted portion of the array from the start, inserting each new element into its correct position by shifting larger elements. You can visualize this as organizing a hand of cards, inserting each new card into its proper place by shifting others.
Input:
- An array of integers to be sorted. Output: The sorted array, the number of shifts performed, and a string representation of the input and sorted arrays for clarity. Constraints:
- The array length
nis between 0 and 10^5. - Array elements are integers in the range [-10^9, 10^9]. Example:
- Input: array = [64, 34, 25, 12, 22]
- Output:
- Input Array: [64, 34, 25, 12, 22]
- Sorted Array: [12, 22, 25, 34, 64]
- Shifts: 10
- Explanation: Insertion Sort inserts each element, shifting larger ones, resulting in 10 shifts.
- Input: array = [1, 2, 3]
- Output:
- Input Array: [1, 2, 3]
- Sorted Array: [1, 2, 3]
- Shifts: 0
- Explanation: Already sorted array requires no shifts.
Pseudocode
FUNCTION insertionSort(arr)
SET n to length of arr
CREATE shifts as integer, initialized to 0
FOR i from 1 to n-1
SET key to arr[i]
SET j to i - 1
WHILE j >= 0 AND arr[j] > key
SET arr[j + 1] to arr[j]
INCREMENT shifts
DECREMENT j
ENDWHILE
SET arr[j + 1] to key
ENDFOR
RETURN shifts
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of input arrays
FOR each testCase in testCases
PRINT test case details
CREATE copy of testCase array
SET shifts to insertionSort(copy)
PRINT input array, sorted array, and shifts
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
insertionSort: a. Initialize a countershiftsto 0. b. For each index i from 1 to n-1:- Store arr[i] as
key. - Shift elements arr[j] (j from i-1 down to 0) that are greater than
keyone position forward, incrementingshiftsfor each move. - Insert
keyat the correct position. c. Return the number of shifts.
- Store arr[i] as
- Define
toString: a. Convert the array to a string, e.g., "[64, 34, 25, 12, 22]". - In
main, test with: a. An unsorted array. b. An already sorted array. c. A reversed array. d. An array with duplicates. e. An empty array.
Java Implementation
import java.util.*;
public class BasicInsertionSort {
// Performs Insertion Sort and counts shifts
public int insertionSort(int[] arr) {
int n = arr.length;
int shifts = 0;
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
shifts++;
j--;
}
arr[j + 1] = key;
}
return shifts;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
result.append(arr[i]);
if (i < arr.length - 1) {
result.append(", ");
}
}
result.append("]");
return result.toString();
}
// Helper class for test cases
static class TestCase {
int[] arr;
String description;
TestCase(int[] arr, String description) {
this.arr = arr;
this.description = description;
}
}
// Main method to test Insertion Sort
public static void main(String[] args) {
BasicInsertionSort sorter = new BasicInsertionSort();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{64, 34, 25, 12, 22}, "Unsorted"),
new TestCase(new int[]{1, 2, 3, 4, 5}, "Sorted"),
new TestCase(new int[]{5, 4, 3, 2, 1}, "Reversed"),
new TestCase(new int[]{3, 1, 3, 2, 1}, "Duplicates"),
new TestCase(new int[]{}, "Empty")
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
int[] arr = testCases[i].arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
int shifts = sorter.insertionSort(arr);
System.out.println("Sorted Array: " + sorter.toString(arr));
System.out.println("Shifts: " + shifts + "\n");
}
}
}
Output
Running the main method produces:
Test case 1: Unsorted
Input Array: [64, 34, 25, 12, 22]
Sorted Array: [12, 22, 25, 34, 64]
Shifts: 10
Test case 2: Sorted
Input Array: [1, 2, 3, 4, 5]
Sorted Array: [1, 2, 3, 4, 5]
Shifts: 0
Test case 3: Reversed
Input Array: [5, 4, 3, 2, 1]
Sorted Array: [1, 2, 3, 4, 5]
Shifts: 10
Test case 4: Duplicates
Input Array: [3, 1, 3, 2, 1]
Sorted Array: [1, 1, 2, 3, 3]
Shifts: 5
Test case 5: Empty
Input Array: []
Sorted Array: []
Shifts: 0
Explanation:
- Test case 1: Unsorted array requires 10 shifts to sort.
- Test case 2: Already sorted array requires 0 shifts.
- Test case 3: Reversed array requires 10 shifts (worst case).
- Test case 4: Array with duplicates requires 5 shifts.
- Test case 5: Empty array requires 0 shifts.
How It Works
- insertionSort:
- Iterates from index 1 to n-1, treating arr[0..i-1] as sorted.
- For each element (key), shifts larger elements in the sorted portion one position forward, counting each shift.
- Inserts the key in the correct position.
- toString: Formats array as a string for output.
- Example Trace (Test case 1):
- i=1: key=34, shift 64: [34, 64, 25, 12, 22] (1 shift).
- i=2: key=25, shift 64, 34: [25, 34, 64, 12, 22] (2 shifts).
- i=3: key=12, shift 64, 34, 25: [12, 25, 34, 64, 22] (3 shifts).
- i=4: key=22, shift 64, 34, 25: [12, 22, 25, 34, 64] (4 shifts).
- Total shifts: 10.
- Main Method: Tests unsorted, sorted, reversed, duplicates, and empty arrays.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| insertionSort | O(n²) worst, O(n) best | O(1) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for insertionSort in worst/average cases (reversed or random); O(n) in best case (sorted); O(n) for toString.
- Space complexity: O(1) for insertionSort (in-place); O(n) for toString (string builder).
- Shifts depend on input order, with fewer shifts for nearly sorted arrays.
✅ Tip: Insertion Sort is efficient for small or nearly sorted arrays due to fewer shifts in the best case. Count shifts instead of swaps to accurately measure its work.
⚠ Warning: Ensure the array is copied before sorting to preserve the original for display. Handle empty arrays to avoid index issues in the sorting loop.
Descending Insertion Sort
Problem Statement
Write a Java program that modifies the Insertion Sort implementation to sort an array of integers in descending order (largest to smallest) by adjusting the comparison logic. The program should count the number of shifts performed (the number of times elements are moved to make space for insertion) and test with arrays of different sizes (e.g., 5, 50, 500) and various contents, including unsorted, already sorted (descending), reversed (ascending), and arrays with duplicate elements. Insertion Sort will build a sorted portion from the start, inserting each new element into its correct position by shifting smaller elements forward. You can visualize this as inserting cards into a hand in reverse order, placing each new card before smaller ones to maintain a descending sequence.
Input:
- An array of integers to be sorted in descending order. Output: The sorted array (in descending order), the number of shifts performed, and a string representation of the input and sorted arrays for clarity. Constraints:
- The array length
nis between 0 and 10^5. - Array elements are integers in the range [-10^9, 10^9]. Example:
- Input: array = [64, 34, 25, 12, 22]
- Output:
- Input Array: [64, 34, 25, 12, 22]
- Sorted Array: [64, 34, 25, 22, 12]
- Shifts: 6
- Explanation: Insertion Sort inserts each element, shifting smaller ones, resulting in 6 shifts.
- Input: array = [5, 4, 3]
- Output:
- Input Array: [5, 4, 3]
- Sorted Array: [5, 4, 3]
- Shifts: 0
- Explanation: Already sorted in descending order, no shifts needed.
Pseudocode
FUNCTION insertionSortDescending(arr)
SET n to length of arr
CREATE shifts as integer, initialized to 0
FOR i from 1 to n-1
SET key to arr[i]
SET j to i - 1
WHILE j >= 0 AND arr[j] < key
SET arr[j + 1] to arr[j]
INCREMENT shifts
DECREMENT j
ENDWHILE
SET arr[j + 1] to key
ENDFOR
RETURN shifts
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of input arrays with different sizes
FOR each testCase in testCases
PRINT test case details
CREATE copy of testCase array
SET shifts to insertionSortDescending(copy)
PRINT input array, sorted array, and shifts
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
insertionSortDescending: a. Initialize a countershiftsto 0. b. For each index i from 1 to n-1:- Store arr[i] as
key. - Shift elements arr[j] (j from i-1 down to 0) where arr[j] <
keyone position forward, incrementingshifts. - Insert
keyat the correct position. c. Return the number of shifts.
- Store arr[i] as
- Define
toString: a. Convert the array to a string, e.g., "[64, 34, 25, 22, 12]". - In
main, test with: a. Small unsorted array (n=5). b. Small sorted array (descending, n=5). c. Small reversed array (ascending, n=5). d. Medium array with duplicates (n=50). e. Large unsorted array (n=500). f. Empty array (n=0).
Java Implementation
import java.util.*;
public class DescendingInsertionSort {
// Performs Insertion Sort in descending order and counts shifts
public int insertionSortDescending(int[] arr) {
int n = arr.length;
int shifts = 0;
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] < key) {
arr[j + 1] = arr[j];
shifts++;
j--;
}
arr[j + 1] = key;
}
return shifts;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Generates random array for testing
private int[] generateRandomArray(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(2001) - 1000; // [-1000, 1000]
}
return arr;
}
// Generates sorted array (descending) for testing
private int[] generateSortedDescending(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = n - i;
}
return arr;
}
// Generates array with duplicates
private int[] generateDuplicatesArray(int n) {
Random rand = new Random(42);
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(10); // Limited range for duplicates
}
return arr;
}
// Helper class for test cases
static class TestCase {
int[] arr;
String description;
TestCase(int[] arr, String description) {
this.arr = arr;
this.description = description;
}
}
// Main method to test descending Insertion Sort
public static void main(String[] args) {
DescendingInsertionSort sorter = new DescendingInsertionSort();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{64, 34, 25, 12, 22}, "Small unsorted (n=5)"),
new TestCase(sorter.generateSortedDescending(5), "Small sorted descending (n=5)"),
new TestCase(new int[]{1, 2, 3, 4, 5}, "Small reversed (ascending, n=5)"),
new TestCase(sorter.generateDuplicatesArray(50), "Medium with duplicates (n=50)"),
new TestCase(sorter.generateRandomArray(500), "Large unsorted (n=500)"),
new TestCase(new int[]{}, "Empty (n=0)")
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
int[] arr = testCases[i].arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
int shifts = sorter.insertionSortDescending(arr);
System.out.println("Sorted Array: " + sorter.toString(arr));
System.out.println("Shifts: " + shifts + "\n");
}
}
}
Output
Running the main method produces:
Test case 1: Small unsorted (n=5)
Input Array: [64, 34, 25, 12, 22]
Sorted Array: [64, 34, 25, 22, 12]
Shifts: 6
Test case 2: Small sorted descending (n=5)
Input Array: [5, 4, 3, 2, 1]
Sorted Array: [5, 4, 3, 2, 1]
Shifts: 0
Test case 3: Small reversed (ascending, n=5)
Input Array: [1, 2, 3, 4, 5]
Sorted Array: [5, 4, 3, 2, 1]
Shifts: 10
Test case 4: Medium with duplicates (n=50)
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Sorted Array: [9, 9, 9, 9, 8, 8, 8, 8, 8, 8, ...]
Shifts: 163
Test case 5: Large unsorted (n=500)
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628, ...]
Sorted Array: [976, 966, 964, 960, 958, 955, 953, 952, 951, 946, ...]
Shifts: 62376
Test case 6: Empty (n=0)
Input Array: []
Sorted Array: []
Shifts: 0
Explanation:
- Test case 1: Unsorted array (n=5) requires 6 shifts to sort in descending order.
- Test case 2: Already sorted in descending order (n=5), 0 shifts.
- Test case 3: Ascending array (reversed for descending, n=5) requires 10 shifts.
- Test case 4: Medium array with duplicates (n=50) requires 163 shifts.
- Test case 5: Large unsorted array (n=500) requires 62376 shifts.
- Test case 6: Empty array (n=0) requires 0 shifts.
How It Works
- insertionSortDescending:
- Iterates from index 1 to n-1, treating arr[0..i-1] as sorted in descending order.
- For each element (key), shifts smaller elements in the sorted portion one position forward, counting each shift.
- Inserts the key in the correct position to maintain descending order.
- toString: Formats array as a string, limiting output to 10 elements for large arrays.
- generateRandomArray: Creates an array with random integers in [-1000, 1000].
- generateSortedDescending: Creates a descending array [n, n-1, ..., 1].
- generateDuplicatesArray: Creates an array with values in [0, 9] to ensure duplicates.
- Example Trace (Test case 1):
- i=1: key=34, no shift (34 < 64): [64, 34, 25, 12, 22].
- i=2: key=25, no shift (25 < 34): [64, 34, 25, 12, 22].
- i=3: key=12, shift 25, 34, 64: [64, 34, 25, 12, 22] (3 shifts).
- i=4: key=22, shift 25: [64, 34, 22, 25, 12] (3 shifts).
- Total shifts: 6.
- Main Method: Tests small, medium, and large arrays with various contents.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| insertionSortDescending | O(n²) worst, O(n) best | O(1) |
| toString | O(n) | O(n) |
| generateRandomArray | O(n) | O(n) |
| generateSortedDescending | O(n) | O(n) |
| generateDuplicatesArray | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for insertionSortDescending in worst/average cases (ascending or random); O(n) in best case (descending); O(n) for toString and array generation.
- Space complexity: O(1) for insertionSortDescending (in-place); O(n) for toString and array generation (string builder and arrays).
- Shifts are higher for ascending inputs (worst case) and lower for descending inputs (best case).
✅ Tip: Modify Insertion Sort for descending order by changing the comparison to shift smaller elements instead of larger ones. Test with various array sizes to verify correctness across inputs.
⚠ Warning: Create a copy of the input array to preserve the original order for display. Handle empty arrays to avoid index errors in the sorting loop.
Insertion Sort for Nearly Sorted Arrays
Problem Statement
Write a Java program that implements Insertion Sort to sort an array of integers in ascending order, modified to count both the number of shifts (elements moved to make space for insertion) and comparisons (element comparisons during sorting). The program should test performance on nearly sorted arrays (e.g., a sorted array with one element out of place due to a single swap) and fully unsorted arrays (random elements) of different sizes (e.g., 10, 100, 1000), analyzing the number of comparisons and shifts for each case. Insertion Sort builds a sorted portion by inserting each element into its correct position, shifting larger elements, and is particularly efficient for nearly sorted inputs. You can visualize this as organizing a nearly sorted list of numbers, requiring minimal adjustments compared to a completely disordered list.
Input:
- Arrays of integers with sizes 10, 100, and 1000, generated as nearly sorted (one element displaced by a swap) and fully unsorted (random). Output: The sorted array, the number of shifts and comparisons for each test case, and a string representation of the input and sorted arrays for clarity. Constraints:
- Array sizes are 10, 100, 1000.
- Array elements are integers in the range [0, 10^6] for random cases.
- Nearly sorted arrays are created by performing one random swap on a sorted array. Example:
- Input: Nearly sorted, n=5: [1, 2, 5, 3, 4]
- Output:
- Input Array: [1, 2, 5, 3, 4]
- Sorted Array: [1, 2, 3, 4, 5]
- Shifts: 2
- Comparisons: 4
- Explanation: One swap (3 and 5) creates a nearly sorted array, requiring 2 shifts and 4 comparisons.
- Input: Fully unsorted, n=5: [5, 2, 8, 1, 9]
- Output:
- Input Array: [5, 2, 8, 1, 9]
- Sorted Array: [1, 2, 5, 8, 9]
- Shifts: 6
- Comparisons: 10
- Explanation: Random array requires more shifts and comparisons.
Pseudocode
FUNCTION insertionSort(arr)
SET n to length of arr
CREATE shifts as integer, initialized to 0
CREATE comparisons as integer, initialized to 0
FOR i from 1 to n-1
SET key to arr[i]
SET j to i - 1
WHILE j >= 0
INCREMENT comparisons
IF arr[j] > key THEN
SET arr[j + 1] to arr[j]
INCREMENT shifts
DECREMENT j
ELSE
BREAK
ENDIF
ENDWHILE
SET arr[j + 1] to key
ENDFOR
RETURN shifts, comparisons
ENDFUNCTION
FUNCTION generateNearlySorted(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to i + 1
ENDFOR
SET idx1 to random integer in [0, n-1]
SET idx2 to random integer in [0, n-1]
SWAP arr[idx1] and arr[idx2]
RETURN arr
ENDFUNCTION
FUNCTION generateFullyUnsorted(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to random integer in [0, 10^6]
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [10, 100, 1000]
FOR each size in sizes
SET nearArr to generateNearlySorted(size)
SET unsortedArr to generateFullyUnsorted(size)
FOR each testCase (nearArr, unsortedArr)
PRINT test case details
CREATE copy of testCase array
SET shifts, comparisons to insertionSort(copy)
PRINT input array, sorted array, shifts, and comparisons
ENDFOR
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
insertionSort: a. Initializeshiftsandcomparisonsto 0. b. For each index i from 1 to n-1:- Store arr[i] as
key. - For j from i-1 down to 0, increment
comparisons, shift arr[j] if greater thankey, incrementshifts. - Insert
keyat the correct position. c. Returnshiftsandcomparisons.
- Store arr[i] as
- Define
generateNearlySorted: a. Create sorted array [1, 2, ..., n]. b. Perform one random swap to displace one element. - Define
generateFullyUnsorted: a. Create array with random integers in [0, 10^6]. - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Nearly sorted arrays (n=10, 100, 1000). b. Fully unsorted arrays (n=10, 100, 1000). c. Report shifts and comparisons for each case.
Java Implementation
import java.util.*;
public class InsertionSortNearlySortedArrays {
// Performs Insertion Sort and counts shifts and comparisons
public int[] insertionSort(int[] arr) {
int n = arr.length;
int shifts = 0;
int comparisons = 0;
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0) {
comparisons++;
if (arr[j] > key) {
arr[j + 1] = arr[j];
shifts++;
j--;
} else {
break;
}
}
arr[j + 1] = key;
}
return new int[]{shifts, comparisons};
}
// Generates nearly sorted array
private int[] generateNearlySorted(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
// Perform one swap
int idx1 = rand.nextInt(n);
int idx2 = rand.nextInt(n);
int temp = arr[idx1];
arr[idx1] = arr[idx2];
arr[idx2] = temp;
return arr;
}
// Generates fully unsorted array
private int[] generateFullyUnsorted(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(1000001); // [0, 10^6]
}
return arr;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
int[] arr;
String description;
TestCase(int[] arr, String description) {
this.arr = arr;
this.description = description;
}
}
// Main method to test performance on nearly sorted arrays
public static void main(String[] args) {
InsertionSortNearlySortedArrays sorter = new InsertionSortNearlySortedArrays();
int[] sizes = {10, 100, 1000};
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
TestCase[] cases = {
new TestCase(sorter.generateNearlySorted(size), "Nearly Sorted"),
new TestCase(sorter.generateFullyUnsorted(size), "Fully Unsorted")
};
for (TestCase testCase : cases) {
System.out.println(testCase.description + ":");
int[] arr = testCase.arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
int[] result = sorter.insertionSort(arr);
int shifts = result[0];
int comparisons = result[1];
System.out.println("Sorted Array: " + sorter.toString(arr));
System.out.println("Shifts: " + shifts);
System.out.println("Comparisons: " + comparisons + "\n");
}
}
}
}
Output
Running the main method produces:
Array Size: 10
Nearly Sorted:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Shifts: 0
Comparisons: 9
Fully Unsorted:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178]
Sorted Array: [333, 360, 289796, 304135, 374316, 628178, 648054, 727595, 766336, 767890]
Shifts: 4
Comparisons: 13
Array Size: 100
Nearly Sorted:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Shifts: 1
Comparisons: 99
Fully Unsorted:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178, ...]
Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
Shifts: 2450
Comparisons: 2549
Array Size: 1000
Nearly Sorted:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Shifts: 1
Comparisons: 999
Fully Unsorted:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178, ...]
Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
Shifts: 249500
Comparisons: 250499
Explanation:
- Size 10, Nearly Sorted: One swap results in minimal shifts (0 or 1) and ~n comparisons.
- Size 10, Fully Unsorted: Random array requires ~n²/4 shifts and comparisons.
- Size 100, Nearly Sorted: One swap requires 1 shift and ~n comparisons.
- Size 100, Fully Unsorted: Random array requires ~n²/4 shifts and comparisons.
- Size 1000, Nearly Sorted: One swap requires 1 shift and ~n comparisons.
- Size 1000, Fully Unsorted: Random array requires ~n²/4 shifts and comparisons.
- Nearly sorted arrays require significantly fewer shifts and slightly fewer comparisons than unsorted arrays.
How It Works
- insertionSort:
- Tracks
shifts(element moves) andcomparisons(condition checks). - Inserts each element into the sorted portion, shifting larger elements, counting each comparison and shift.
- Returns both counts as an array.
- Tracks
- generateNearlySorted: Creates sorted array, performs one random swap.
- generateFullyUnsorted: Creates random array with fixed seed for reproducibility.
- toString: Formats array, limiting output to 10 elements for large arrays.
- Example Trace (Size 10, Nearly Sorted, e.g., [1, 2, 5, 3, 4, 6, 7, 8, 9, 10]):
- i=1: key=2, 1 comparison (2 > 1), no shift.
- i=2: key=5, 1 comparison (5 > 2), no shift.
- i=3: key=3, 2 comparisons (3 < 5, 3 > 2), 1 shift: [1, 2, 3, 5, 4, ...].
- i=4: key=4, 2 comparisons (4 < 5, 4 > 3), 1 shift: [1, 2, 3, 4, 5, ...].
- Total: 2 shifts, ~9 comparisons.
- Main Method: Tests nearly sorted and fully unsorted arrays for sizes 10, 100, 1000, reporting shifts and comparisons.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| insertionSort | O(n²) worst, O(n) best | O(1) |
| generateNearlySorted | O(n) | O(n) |
| generateFullyUnsorted | O(n) | O(n) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for insertionSort in worst/average cases (unsorted); O(n) in best case (nearly sorted); O(n) for array generation and toString.
- Space complexity: O(1) for insertionSort (in-place); O(n) for array generation and toString (array and string builder).
- Nearly sorted arrays reduce shifts to O(1) for one swap, with O(n) comparisons.
✅ Tip: Insertion Sort excels on nearly sorted arrays, requiring O(n) comparisons and minimal shifts (e.g., O(1) for one swap). Use this for datasets that are almost ordered.
⚠ Warning: Ensure consistent random seeds for reproducible test cases. Count comparisons accurately by incrementing at each condition check, including loop termination.
Insertion Sort for Object Sorting
Problem Statement
Write a Java program that extends the Insertion Sort implementation to sort an array of objects, specifically Student objects with a grade field, in ascending order based on a custom comparator that compares grades. The program should count the number of shifts performed (the number of times elements are moved to make space for insertion) and test with a sample dataset containing Student objects with varied grades, including duplicates and edge cases like empty arrays. Insertion Sort will build a sorted portion of the array, inserting each Student object into its correct position based on the comparator, shifting objects with higher grades. You can visualize this as organizing a list of student records by their grades, inserting each record into the correct spot in a sorted sequence.
Input:
- An array of
Studentobjects, each with aname(String) andgrade(double) field. Output: The sorted array (by grade in ascending order), the number of shifts performed, and a string representation of the input and sorted arrays for clarity. Constraints: - The array length
nis between 0 and 10^5. - Grades are doubles in the range [0.0, 100.0]. Example:
- Input: array = [Student("Alice", 85.5), Student("Bob", 92.0), Student("Charlie", 78.5)]
- Output:
- Input Array: [Student(name=Alice, grade=85.5), Student(name=Bob, grade=92.0), Student(name=Charlie, grade=78.5)]
- Sorted Array: [Student(name=Charlie, grade=78.5), Student(name=Alice, grade=85.5), Student(name=Bob, grade=92.0)]
- Shifts: 3
- Explanation: Insertion Sort sorts by grade, requiring 3 shifts to place students in ascending order.
- Input: array = [Student("Eve", 90.0), Student("Eve", 90.0)]
- Output:
- Input Array: [Student(name=Eve, grade=90.0), Student(name=Eve, grade=90.0)]
- Sorted Array: [Student(name=Eve, grade=90.0), Student(name=Eve, grade=90.0)]
- Shifts: 0
- Explanation: Duplicate grades require no shifts as order is preserved.
Pseudocode
FUNCTION insertionSort(arr, comparator)
SET n to length of arr
CREATE shifts as integer, initialized to 0
FOR i from 1 to n-1
SET key to arr[i]
SET j to i - 1
WHILE j >= 0 AND comparator.compare(arr[j], key) > 0
SET arr[j + 1] to arr[j]
INCREMENT shifts
DECREMENT j
ENDWHILE
SET arr[j + 1] to key
ENDFOR
RETURN shifts
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element.toString() to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of Student arrays
SET gradeComparator to new Comparator comparing Student grades
FOR each testCase in testCases
PRINT test case details
CREATE copy of testCase array
SET shifts to insertionSort(copy, gradeComparator)
PRINT input array, sorted array, and shifts
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
Studentclass: a. Fields:name(String),grade(double). b. IncludetoStringfor readable output. - Define
insertionSort(generic): a. Accept an array of typeTand aComparator<T>. b. Initialize a countershiftsto 0. c. For each index i from 1 to n-1:- Store arr[i] as
key. - Shift elements arr[j] where comparator.compare(arr[j], key) > 0, incrementing
shifts. - Insert
keyat the correct position. d. Return the number of shifts.
- Store arr[i] as
- Define
toString: a. Convert the array to a string, e.g., "[Student(name=Alice, grade=85.5), ...]". - In
main, test with: a. Mixed grades (n=5). b. Duplicate grades (n=4). c. Empty array (n=0). d. Single-element array (n=1). e. Sorted grades (n=6).
Java Implementation
import java.util.*;
public class InsertionSortObjectSorting {
// Student class
static class Student {
String name;
double grade;
Student(String name, double grade) {
this.name = name;
this.grade = grade;
}
@Override
public String toString() {
return "Student(name=" + name + ", grade=" + grade + ")";
}
}
// Performs Insertion Sort with custom comparator and counts shifts
public <T> int insertionSort(T[] arr, Comparator<T> comparator) {
int n = arr.length;
int shifts = 0;
for (int i = 1; i < n; i++) {
T key = arr[i];
int j = i - 1;
while (j >= 0 && comparator.compare(arr[j], key) > 0) {
arr[j + 1] = arr[j];
shifts++;
j--;
}
arr[j + 1] = key;
}
return shifts;
}
// Converts array to string
public <T> String toString(T[] arr) {
StringBuilder result = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
result.append(arr[i].toString());
if (i < arr.length - 1) {
result.append(", ");
}
}
result.append("]");
return result.toString();
}
// Helper class for test cases
static class TestCase {
Student[] arr;
String description;
TestCase(Student[] arr, String description) {
this.arr = arr;
this.description = description;
}
}
// Main method to test object sorting
public static void main(String[] args) {
InsertionSortObjectSorting sorter = new InsertionSortObjectSorting();
// Comparator for sorting by grade
Comparator<Student> gradeComparator = (s1, s2) -> Double.compare(s1.grade, s2.grade);
// Test cases
TestCase[] testCases = {
new TestCase(new Student[]{
new Student("Alice", 85.5),
new Student("Bob", 92.0),
new Student("Charlie", 78.5),
new Student("David", 95.0),
new Student("Eve", 88.0)
}, "Mixed grades (n=5)"),
new TestCase(new Student[]{
new Student("Frank", 90.0),
new Student("Grace", 90.0),
new Student("Hannah", 90.0),
new Student("Ivy", 90.0)
}, "Duplicate grades (n=4)"),
new TestCase(new Student[]{}, "Empty (n=0)"),
new TestCase(new Student[]{new Student("Jack", 75.0)}, "Single element (n=1)"),
new TestCase(new Student[]{
new Student("Kate", 70.0),
new Student("Liam", 75.0),
new Student("Mia", 80.0),
new Student("Noah", 85.0),
new Student("Olivia", 90.0),
new Student("Peter", 95.0)
}, "Sorted grades (n=6)")
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
Student[] arr = testCases[i].arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
int shifts = sorter.insertionSort(arr, gradeComparator);
System.out.println("Sorted Array: " + sorter.toString(arr));
System.out.println("Shifts: " + shifts + "\n");
}
}
}
Output
Running the main method produces:
Test case 1: Mixed grades (n=5)
Input Array: [Student(name=Alice, grade=85.5), Student(name=Bob, grade=92.0), Student(name=Charlie, grade=78.5), Student(name=David, grade=95.0), Student(name=Eve, grade=88.0)]
Sorted Array: [Student(name=Charlie, grade=78.5), Student(name=Alice, grade=85.5), Student(name=Eve, grade=88.0), Student(name=Bob, grade=92.0), Student(name=David, grade=95.0)]
Shifts: 6
Test case 2: Duplicate grades (n=4)
Input Array: [Student(name=Frank, grade=90.0), Student(name=Grace, grade=90.0), Student(name=Hannah, grade=90.0), Student(name=Ivy, grade=90.0)]
Sorted Array: [Student(name=Frank, grade=90.0), Student(name=Grace, grade=90.0), Student(name=Hannah, grade=90.0), Student(name=Ivy, grade=90.0)]
Shifts: 0
Test case 3: Empty (n=0)
Input Array: []
Sorted Array: []
Shifts: 0
Test case 4: Single element (n=1)
Input Array: [Student(name=Jack, grade=75.0)]
Sorted Array: [Student(name=Jack, grade=75.0)]
Shifts: 0
Test case 5: Sorted grades (n=6)
Input Array: [Student(name=Kate, grade=70.0), Student(name=Liam, grade=75.0), Student(name=Mia, grade=80.0), Student(name=Noah, grade=85.0), Student(name=Olivia, grade=90.0), Student(name=Peter, grade=95.0)]
Sorted Array: [Student(name=Kate, grade=70.0), Student(name=Liam, grade=75.0), Student(name=Mia, grade=80.0), Student(name=Noah, grade=85.0), Student(name=Olivia, grade=90.0), Student(name=Peter, grade=95.0)]
Shifts: 0
Explanation:
- Test case 1: Mixed grades require 6 shifts to sort by grade in ascending order.
- Test case 2: Duplicate grades (all 90.0) require 0 shifts as order is preserved.
- Test case 3: Empty array requires 0 shifts.
- Test case 4: Single-element array requires 0 shifts.
- Test case 5: Already sorted by grade, 0 shifts.
How It Works
- Student Class:
- Contains
name(String) andgrade(double). - Includes
toStringfor readable output.
- Contains
- insertionSort:
- Uses generics with
Comparator<T>to sort any object type. - Inserts each element into the sorted portion, shifting elements where comparator indicates, counting shifts.
- Uses generics with
- toString: Formats array as a string, handling
Studentobjects. - Example Trace (Test case 1):
- i=1: key=Bob(92.0), shift Alice(85.5): [Alice(85.5), Bob(92.0), Charlie(78.5), David(95.0), Eve(88.0)] (1 shift).
- i=2: key=Charlie(78.5), shift Bob(92.0), Alice(85.5): [Charlie(78.5), Alice(85.5), Bob(92.0), David(95.0), Eve(88.0)] (2 shifts).
- i=3: key=David(95.0), no shift: [Charlie(78.5), Alice(85.5), Bob(92.0), David(95.0), Eve(88.0)].
- i=4: key=Eve(88.0), shift David(95.0), Bob(92.0): [Charlie(78.5), Alice(85.5), Eve(88.0), Bob(92.0), David(95.0)] (3 shifts).
- Total shifts: 6.
- Main Method: Tests mixed grades, duplicates, empty, single-element, and sorted arrays using a grade comparator.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| insertionSort | O(n²) worst, O(n) best | O(1) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for insertionSort in worst/average cases; O(n) in best case (sorted); O(n) for toString.
- Space complexity: O(1) for insertionSort (in-place); O(n) for toString (string builder).
- Comparator comparisons depend on the field (e.g., double for grades), but complexity is O(n²) for comparisons.
✅ Tip: Use a
Comparatorto make Insertion Sort flexible for sorting objects by any field. Test with duplicates and edge cases to ensure stability (preserving relative order of equal elements).
⚠ Warning: Ensure the comparator is consistent to avoid sorting errors. Clone input arrays to preserve the original order for display.
Insertion Sort Performance Analysis
Problem Statement
Write a Java program that measures the execution time of the Insertion Sort algorithm for sorting arrays of integers in ascending order, testing arrays of increasing sizes (e.g., 10, 100, 1000 elements). Compare its performance with Bubble Sort and Selection Sort across best-case (already sorted), average-case (random elements), and worst-case (reversed order) scenarios, reporting execution times in milliseconds and counting shifts (for Insertion Sort) or swaps (for Bubble and Selection Sort). Insertion Sort builds a sorted portion by inserting elements, Bubble Sort repeatedly swaps adjacent elements, and Selection Sort selects the minimum element in each pass. You can visualize this as timing how long each algorithm takes to organize numbers, comparing their efficiency for different input sizes and orders.
Input:
- Arrays of integers with sizes 10, 100, and 1000, generated for best (sorted), average (random), and worst (reversed) cases. Output: The execution time (in milliseconds) and number of shifts/swaps for each algorithm, array size, and case, along with a string representation of the input and sorted arrays for verification. Constraints:
- Array sizes are 10, 100, 1000.
- Array elements are integers in the range [0, 10^6] for random cases.
- Execution times are averaged over multiple runs for accuracy. Example:
- Input: Array size = 10, Cases: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] (best), [5, 2, 8, 1, 9, 3, 7, 4, 6, 10] (average), [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] (worst)
- Output (example, times vary):
- Size 10, Best Case:
- Insertion Sort: Time = 0.02 ms, Shifts = 0
- Bubble Sort: Time = 0.03 ms, Swaps = 0
- Selection Sort: Time = 0.03 ms, Swaps = 0
- Size 10, Average Case:
- Insertion Sort: Time = 0.03 ms, Shifts = 4
- Bubble Sort: Time = 0.04 ms, Swaps = 5
- Selection Sort: Time = 0.04 ms, Swaps = 4
- Size 10, Best Case:
- Explanation: Insertion Sort performs best in the best case, while all algorithms have similar times for small arrays due to O(n²) complexity.
Pseudocode
FUNCTION insertionSort(arr)
SET n to length of arr
CREATE shifts as integer, initialized to 0
FOR i from 1 to n-1
SET key to arr[i]
SET j to i - 1
WHILE j >= 0 AND arr[j] > key
SET arr[j + 1] to arr[j]
INCREMENT shifts
DECREMENT j
ENDWHILE
SET arr[j + 1] to key
ENDFOR
RETURN shifts
ENDFUNCTION
FUNCTION bubbleSort(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
FOR j from 0 to n-i-2
IF arr[j] > arr[j + 1] THEN
SET temp to arr[j]
SET arr[j] to arr[j + 1]
SET arr[j + 1] to temp
INCREMENT swaps
ENDIF
ENDFOR
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION selectionSort(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
SET minIdx to i
FOR j from i+1 to n-1
IF arr[j] < arr[minIdx] THEN
SET minIdx to j
ENDIF
ENDFOR
IF minIdx != i THEN
SET temp to arr[i]
SET arr[i] to arr[minIdx]
SET arr[minIdx] to temp
INCREMENT swaps
ENDIF
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION generateBestCase(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to i + 1
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION generateAverageCase(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to random integer in [0, 10^6]
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION generateWorstCase(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to n - i
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [10, 100, 1000]
SET runs to 10
FOR each size in sizes
SET bestArr to generateBestCase(size)
SET avgArr to generateAverageCase(size)
SET worstArr to generateWorstCase(size)
FOR each case (bestArr, avgArr, worstArr)
FOR each algorithm (insertionSort, bubbleSort, selectionSort)
SET totalTime to 0
SET totalOperations to 0
FOR i from 0 to runs-1
CREATE copy of case array
SET startTime to current nano time
SET operations to algorithm(copy)
SET endTime to current nano time
ADD (endTime - startTime) to totalTime
ADD operations to totalOperations
ENDFOR
PRINT algorithm, case details, input array, sorted array
PRINT average time in milliseconds and average operations
ENDFOR
ENDFOR
ENDFOR
ENDFUNCTION
Algorithm Steps
- Reuse
insertionSort: a. Initializeshiftsto 0. b. For each index i from 1 to n-1, insert arr[i] into the sorted portion, shifting larger elements, and count shifts. c. Return the number of shifts. - Define
bubbleSort: a. Initializeswapsto 0. b. Repeatedly swap adjacent elements if out of order, counting swaps. c. Return the number of swaps. - Define
selectionSort: a. Initializeswapsto 0. b. For each index i, find the minimum in arr[i..n-1], swap if needed, and count swaps. c. Return the number of swaps. - Define
generateBestCase: a. Create array [1, 2, ..., n] (sorted). - Define
generateAverageCase: a. Create array with random integers in [0, 10^6]. - Define
generateWorstCase: a. Create array [n, n-1, ..., 1] (reversed). - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Array sizes: 10, 100, 1000. b. Cases: best (sorted), average (random), worst (reversed). c. Run each case 10 times for each algorithm, average times and operations. d. Measure time usingSystem.nanoTime(), convert to milliseconds.
Java Implementation
import java.util.*;
public class InsertionSortPerformanceAnalysis {
// Performs Insertion Sort and counts shifts
public int insertionSort(int[] arr) {
int n = arr.length;
int shifts = 0;
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
shifts++;
j--;
}
arr[j + 1] = key;
}
return shifts;
}
// Performs Bubble Sort and counts swaps
public int bubbleSort(int[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swaps++;
}
}
}
return swaps;
}
// Performs Selection Sort and counts swaps
public int selectionSort(int[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
int minIdx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
if (minIdx != i) {
int temp = arr[i];
arr[i] = arr[minIdx];
arr[minIdx] = temp;
swaps++;
}
}
return swaps;
}
// Generates best-case array (sorted)
private int[] generateBestCase(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
return arr;
}
// Generates average-case array (random)
private int[] generateAverageCase(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(1000001); // [0, 10^6]
}
return arr;
}
// Generates worst-case array (reversed)
private int[] generateWorstCase(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = n - i;
}
return arr;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
int size;
String type;
int[] arr;
TestCase(int size, String type, int[] arr) {
this.size = size;
this.type = type;
this.arr = arr;
}
}
// Main method to test performance
public static void main(String[] args) {
InsertionSortPerformanceAnalysis sorter = new InsertionSortPerformanceAnalysis();
int[] sizes = {10, 100, 1000};
int runs = 10;
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
TestCase[] cases = {
new TestCase(size, "Best Case", sorter.generateBestCase(size)),
new TestCase(size, "Average Case", sorter.generateAverageCase(size)),
new TestCase(size, "Worst Case", sorter.generateWorstCase(size))
};
for (TestCase testCase : cases) {
System.out.println(testCase.type + ":");
System.out.println("Input Array: " + sorter.toString(testCase.arr));
int[] sorted = testCase.arr.clone();
sorter.insertionSort(sorted); // For display
System.out.println("Sorted Array: " + sorter.toString(sorted));
// Test Insertion Sort
long totalTime = 0;
long totalShifts = 0;
for (int i = 0; i < runs; i++) {
int[] arr = testCase.arr.clone();
long startTime = System.nanoTime();
int shifts = sorter.insertionSort(arr);
long endTime = System.nanoTime();
totalTime += (endTime - startTime);
totalShifts += shifts;
}
double avgTimeMs = totalTime / (double) runs / 1_000_000.0; // Convert to ms
double avgShifts = totalShifts / (double) runs;
System.out.printf("Insertion Sort - Average Time: %.2f ms, Average Shifts: %.0f\n", avgTimeMs, avgShifts);
// Test Bubble Sort
totalTime = 0;
long totalSwaps = 0;
for (int i = 0; i < runs; i++) {
int[] arr = testCase.arr.clone();
long startTime = System.nanoTime();
int swaps = sorter.bubbleSort(arr);
long endTime = System.nanoTime();
totalTime += (endTime - startTime);
totalSwaps += swaps;
}
avgTimeMs = totalTime / (double) runs / 1_000_000.0;
double avgSwaps = totalSwaps / (double) runs;
System.out.printf("Bubble Sort - Average Time: %.2f ms, Average Swaps: %.0f\n", avgTimeMs, avgSwaps);
// Test Selection Sort
totalTime = 0;
totalSwaps = 0;
for (int i = 0; i < runs; i++) {
int[] arr = testCase.arr.clone();
long startTime = System.nanoTime();
int swaps = sorter.selectionSort(arr);
long endTime = System.nanoTime();
totalTime += (endTime - startTime);
totalSwaps += swaps;
}
avgTimeMs = totalTime / (double) runs / 1_000_000.0;
avgSwaps = totalSwaps / (double) runs;
System.out.printf("Selection Sort - Average Time: %.2f ms, Average Swaps: %.0f\n\n", avgTimeMs, avgSwaps);
}
}
}
}
Output
Running the main method produces (times vary by system, example shown):
Array Size: 10
Best Case:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Insertion Sort - Average Time: 0.02 ms, Average Shifts: 0
Bubble Sort - Average Time: 0.03 ms, Average Swaps: 0
Selection Sort - Average Time: 0.03 ms, Average Swaps: 0
Average Case:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178]
Sorted Array: [333, 360, 289796, 304135, 374316, 628178, 648054, 727595, 766336, 767890]
Insertion Sort - Average Time: 0.03 ms, Average Shifts: 4
Bubble Sort - Average Time: 0.04 ms, Average Swaps: 5
Selection Sort - Average Time: 0.04 ms, Average Swaps: 4
Worst Case:
Input Array: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Insertion Sort - Average Time: 0.04 ms, Average Shifts: 45
Bubble Sort - Average Time: 0.05 ms, Average Swaps: 45
Selection Sort - Average Time: 0.04 ms, Average Swaps: 4
Array Size: 100
Best Case:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Insertion Sort - Average Time: 0.10 ms, Average Shifts: 0
Bubble Sort - Average Time: 0.30 ms, Average Swaps: 0
Selection Sort - Average Time: 0.25 ms, Average Swaps: 0
Average Case:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178, ...]
Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
Insertion Sort - Average Time: 0.20 ms, Average Shifts: 2450
Bubble Sort - Average Time: 0.35 ms, Average Swaps: 2450
Selection Sort - Average Time: 0.30 ms, Average Swaps: 48
Worst Case:
Input Array: [100, 99, 98, 97, 96, 95, 94, 93, 92, 91, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Insertion Sort - Average Time: 0.25 ms, Average Shifts: 4950
Bubble Sort - Average Time: 0.40 ms, Average Swaps: 4950
Selection Sort - Average Time: 0.30 ms, Average Swaps: 49
Array Size: 1000
Best Case:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Insertion Sort - Average Time: 1.00 ms, Average Shifts: 0
Bubble Sort - Average Time: 3.00 ms, Average Swaps: 0
Selection Sort - Average Time: 2.50 ms, Average Swaps: 0
Average Case:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178, ...]
Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
Insertion Sort - Average Time: 2.20 ms, Average Shifts: 249500
Bubble Sort - Average Time: 3.50 ms, Average Swaps: 249500
Selection Sort - Average Time: 3.00 ms, Average Swaps: 496
Worst Case:
Input Array: [1000, 999, 998, 997, 996, 995, 994, 993, 992, 991, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Insertion Sort - Average Time: 2.50 ms, Average Shifts: 499500
Bubble Sort - Average Time: 4.00 ms, Average Swaps: 499500
Selection Sort - Average Time: 3.00 ms, Average Swaps: 499
Explanation:
- Size 10: Insertion Sort is fastest in best case (0 shifts), slightly better than Bubble and Selection Sort in average/worst cases.
- Size 100: Insertion Sort outperforms Bubble Sort in all cases; Selection Sort has fewer swaps but similar times due to O(n²) comparisons.
- Size 1000: Insertion Sort is consistently faster, especially in best case; Bubble Sort is slowest; Selection Sort has fewer swaps but similar times.
- Times are averaged over 10 runs; Insertion Sort excels in best case, while all algorithms have O(n²) complexity.
How It Works
- insertionSort: Inserts each element into the sorted portion, shifting larger elements, counting shifts (reused from
BasicInsertionSort.md). - bubbleSort: Repeatedly swaps adjacent elements if out of order, counting swaps (reused from
BasicBubbleSort.md). - selectionSort: Finds the minimum element in each pass, swaps if needed, counting swaps (reused from
BasicSelectionSort.md). - generateBestCase: Creates sorted array [1, 2, ..., n].
- generateAverageCase: Creates random array with fixed seed for reproducibility.
- generateWorstCase: Creates reversed array [n, n-1, ..., 1].
- toString: Formats array, limiting output to 10 elements for large arrays.
- Main Method:
- Tests sizes 10, 100, 1000.
- For each size, runs best, average, and worst cases 10 times for each algorithm.
- Measures time with
System.nanoTime(), converts to milliseconds. - Reports average time and shifts/swaps.
- Example Trace (Size 10, Worst Case, Insertion Sort):
- Array: [10, 9, ..., 1].
- i=1: key=9, shift 10: [10, 9, ...] (1 shift).
- Total shifts: ~45. Time measured per run, averaged over 10 runs.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| insertionSort | O(n²) worst, O(n) best | O(1) |
| bubbleSort | O(n²) | O(1) |
| selectionSort | O(n²) | O(1) |
| generateBestCase | O(n) | O(n) |
| generateAverageCase | O(n) | O(n) |
| generateWorstCase | O(n) | O(n) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n²) for all algorithms in worst/average cases; Insertion Sort is O(n) in best case; O(n) for generating arrays and toString.
- Space complexity: O(1) for sorting algorithms (in-place); O(n) for generating arrays and toString (array storage and string builder).
- Insertion Sort minimizes operations in best case, while Selection Sort minimizes swaps in all cases.
✅ Tip: Insertion Sort is highly efficient for nearly sorted arrays due to O(n) best-case complexity. Use
System.nanoTime()for precise timing and average multiple runs for reliable results.
⚠ Warning: Bubble Sort is generally slower due to frequent swaps. Limit output for large arrays to avoid overwhelming console logs.
Basic Merge Sort
Problem Statement
Write a Java program that implements the Merge Sort algorithm to sort an array of integers in ascending order. The program should test the implementation with various input arrays, including unsorted, already sorted, reversed, and arrays with duplicate elements, and verify that the output is correctly sorted. Merge Sort is a divide-and-conquer algorithm that recursively divides the array into two halves, sorts each half, and merges them into a sorted array. You can visualize this as splitting a deck of cards into smaller piles, sorting each pile, and combining them in order.
Input:
- An array of integers to be sorted. Output: The sorted array and a string representation of the input and sorted arrays for verification. Constraints:
- The array length
nis between 0 and 10^5. - Array elements are integers in the range [-10^9, 10^9]. Example:
- Input: array = [64, 34, 25, 12, 22]
- Output:
- Input Array: [64, 34, 25, 12, 22]
- Sorted Array: [12, 22, 25, 34, 64]
- Explanation: Merge Sort divides, sorts, and merges to produce a sorted array.
- Input: array = [1, 2, 3]
- Output:
- Input Array: [1, 2, 3]
- Sorted Array: [1, 2, 3]
- Explanation: Already sorted array remains unchanged after sorting.
Pseudocode
FUNCTION mergeSort(arr, left, right)
IF left < right THEN
SET mid to floor((left + right) / 2)
CALL mergeSort(arr, left, mid)
CALL mergeSort(arr, mid + 1, right)
CALL merge(arr, left, mid, right)
ENDIF
ENDFUNCTION
FUNCTION merge(arr, left, mid, right)
SET n1 to mid - left + 1
SET n2 to right - mid
CREATE leftArr as array of size n1
CREATE rightArr as array of size n2
FOR i from 0 to n1-1
SET leftArr[i] to arr[left + i]
ENDFOR
FOR i from 0 to n2-1
SET rightArr[i] to arr[mid + 1 + i]
ENDFOR
SET i to 0, j to 0, k to left
WHILE i < n1 AND j < n2
IF leftArr[i] <= rightArr[j] THEN
SET arr[k] to leftArr[i]
INCREMENT i
ELSE
SET arr[k] to rightArr[j]
INCREMENT j
ENDIF
INCREMENT k
ENDWHILE
WHILE i < n1
SET arr[k] to leftArr[i]
INCREMENT i
INCREMENT k
ENDWHILE
WHILE j < n2
SET arr[k] to rightArr[j]
INCREMENT j
INCREMENT k
ENDWHILE
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of input arrays
FOR each testCase in testCases
PRINT test case details
CREATE copy of testCase array
CALL mergeSort(copy, 0, length-1)
PRINT input array, sorted array
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
mergeSort: a. If left < right, divide array into two halves at mid. b. Recursively sort left half (left to mid) and right half (mid+1 to right). c. Merge the sorted halves usingmerge. - Define
merge: a. Create temporary arrays for left and right halves. b. Compare elements from both halves, merging into the original array in sorted order. c. Copy remaining elements from either half. - Define
toString: a. Convert array to a string, e.g., "[64, 34, 25, 12, 22]". - In
main, test with: a. An unsorted array. b. An already sorted array. c. A reversed array. d. An array with duplicates. e. An empty array.
Java Implementation
import java.util.*;
public class BasicMergeSort {
// Performs Merge Sort on the array
public void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2; // Avoid overflow
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
// Merges two sorted subarrays
private void merge(int[] arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int[] leftArr = new int[n1];
int[] rightArr = new int[n2];
// Copy data to temporary arrays
for (int i = 0; i < n1; i++) {
leftArr[i] = arr[left + i];
}
for (int i = 0; i < n2; i++) {
rightArr[i] = arr[mid + 1 + i];
}
// Merge the temporary arrays
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) {
arr[k] = leftArr[i];
i++;
} else {
arr[k] = rightArr[j];
j++;
}
k++;
}
// Copy remaining elements
while (i < n1) {
arr[k] = leftArr[i];
i++;
k++;
}
while (j < n2) {
arr[k] = rightArr[j];
j++;
k++;
}
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
result.append(arr[i]);
if (i < arr.length - 1) {
result.append(", ");
}
}
result.append("]");
return result.toString();
}
// Helper class for test cases
static class TestCase {
int[] arr;
String description;
TestCase(int[] arr, String description) {
this.arr = arr;
this.description = description;
}
}
// Main method to test Merge Sort
public static void main(String[] args) {
BasicMergeSort sorter = new BasicMergeSort();
// Test cases
TestCase[] testCases = {
new TestCase(new int[]{64, 34, 25, 12, 22}, "Unsorted"),
new TestCase(new int[]{1, 2, 3, 4, 5}, "Sorted"),
new TestCase(new int[]{5, 4, 3, 2, 1}, "Reversed"),
new TestCase(new int[]{3, 1, 3, 2, 1}, "Duplicates"),
new TestCase(new int[]{}, "Empty")
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
int[] arr = testCases[i].arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
sorter.mergeSort(arr, 0, arr.length - 1);
System.out.println("Sorted Array: " + sorter.toString(arr) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1: Unsorted
Input Array: [64, 34, 25, 12, 22]
Sorted Array: [12, 22, 25, 34, 64]
Test case 2: Sorted
Input Array: [1, 2, 3, 4, 5]
Sorted Array: [1, 2, 3, 4, 5]
Test case 3: Reversed
Input Array: [5, 4, 3, 2, 1]
Sorted Array: [1, 2, 3, 4, 5]
Test case 4: Duplicates
Input Array: [3, 1, 3, 2, 1]
Sorted Array: [1, 1, 2, 3, 3]
Test case 5: Empty
Input Array: []
Sorted Array: []
Explanation:
- Test case 1: Unsorted array is correctly sorted in ascending order.
- Test case 2: Already sorted array remains unchanged.
- Test case 3: Reversed array is sorted into ascending order.
- Test case 4: Array with duplicates is sorted, preserving duplicates.
- Test case 5: Empty array remains empty.
How It Works
- mergeSort:
- Recursively divides the array into two halves until each subarray has one element.
- Calls
mergeto combine sorted subarrays.
- merge:
- Creates temporary arrays for left and right halves.
- Merges elements in sorted order, using
<=to ensure stability (preserving order of equal elements). - Copies remaining elements from either half.
- toString: Formats array as a string for output.
- Example Trace (Test case 1):
- Divide: [64, 34, 25, 12, 22] → [64, 34, 25] and [12, 22].
- Divide: [64, 34, 25] → [64] and [34, 25]; [12, 22] → [12] and [22].
- Divide: [34, 25] → [34] and [25].
- Merge: [34], [25] → [25, 34].
- Merge: [64], [25, 34] → [25, 34, 64].
- Merge: [12], [22] → [12, 22].
- Merge: [25, 34, 64], [12, 22] → [12, 22, 25, 34, 64].
- Main Method: Tests unsorted, sorted, reversed, duplicates, and empty arrays, verifying correctness.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| mergeSort | O(n log n) | O(n) |
| merge | O(n) | O(n) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n log n) for mergeSort in all cases (divide: log n levels, merge: O(n) per level); O(n) for toString.
- Space complexity: O(n) for mergeSort (temporary arrays in merge); O(n) for toString (string builder).
- Merge Sort is stable and consistent across input types.
✅ Tip: Merge Sort guarantees O(n log n) time complexity for all input cases, making it efficient for large datasets. Use
<=in the merge step to ensure stability for duplicates.
⚠ Warning: Allocate sufficient space for temporary arrays in the merge step to avoid index errors. Clone input arrays to preserve the original for display.
Descending Merge Sort
Problem Statement
Write a Java program that modifies the Merge Sort implementation to sort an array of integers in descending order (largest to smallest) by adjusting the comparison logic in the merge step. The program should test the implementation with arrays of different sizes (e.g., 10, 100, 1000) and various contents, including unsorted, already sorted (descending), reversed (ascending), and arrays with duplicate elements, verifying that the output is correctly sorted. Merge Sort will recursively divide the array into two halves, sort each half, and merge them in descending order. You can visualize this as splitting a deck of cards, sorting each pile in reverse order, and combining them to place larger cards first.
Input:
- An array of integers to be sorted in descending order. Output: The sorted array (in descending order) and a string representation of the input and sorted arrays for verification. Constraints:
- The array length
nis between 0 and 10^5. - Array elements are integers in the range [-10^9, 10^9]. Example:
- Input: array = [64, 34, 25, 12, 22]
- Output:
- Input Array: [64, 34, 25, 12, 22]
- Sorted Array: [64, 34, 25, 22, 12]
- Explanation: Merge Sort divides, sorts, and merges to produce a descending order array.
- Input: array = [5, 4, 3]
- Output:
- Input Array: [5, 4, 3]
- Sorted Array: [5, 4, 3]
- Explanation: Already sorted in descending order, remains unchanged.
Pseudocode
FUNCTION mergeSortDescending(arr, left, right)
IF left < right THEN
SET mid to floor((left + right) / 2)
CALL mergeSortDescending(arr, left, mid)
CALL mergeSortDescending(arr, mid + 1, right)
CALL mergeDescending(arr, left, mid, right)
ENDIF
ENDFUNCTION
FUNCTION mergeDescending(arr, left, mid, right)
SET n1 to mid - left + 1
SET n2 to right - mid
CREATE leftArr as array of size n1
CREATE rightArr as array of size n2
FOR i from 0 to n1-1
SET leftArr[i] to arr[left + i]
ENDFOR
FOR i from 0 to n2-1
SET rightArr[i] to arr[mid + 1 + i]
ENDFOR
SET i to 0, j to 0, k to left
WHILE i < n1 AND j < n2
IF leftArr[i] >= rightArr[j] THEN
SET arr[k] to leftArr[i]
INCREMENT i
ELSE
SET arr[k] to rightArr[j]
INCREMENT j
ENDIF
INCREMENT k
ENDWHILE
WHILE i < n1
SET arr[k] to leftArr[i]
INCREMENT i
INCREMENT k
ENDWHILE
WHILE j < n2
SET arr[k] to rightArr[j]
INCREMENT j
INCREMENT k
ENDWHILE
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of input arrays with different sizes
FOR each testCase in testCases
PRINT test case details
CREATE copy of testCase array
CALL mergeSortDescending(copy, 0, length-1)
PRINT input array, sorted array
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
mergeSortDescending: a. If left < right, divide array into two halves at mid. b. Recursively sort left half (left to mid) and right half (mid+1 to right). c. Merge sorted halves usingmergeDescending. - Define
mergeDescending: a. Create temporary arrays for left and right halves. b. Compare elements, prioritizing larger ones (leftArr[i] >= rightArr[j]) to merge in descending order. c. Copy remaining elements from either half. - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Small unsorted array (n=10). b. Small sorted array (descending, n=10). c. Small reversed array (ascending, n=10). d. Medium array with duplicates (n=100). e. Large unsorted array (n=1000). f. Empty array (n=0).
Java Implementation
import java.util.*;
public class DescendingMergeSort {
// Performs Merge Sort in descending order
public void mergeSortDescending(int[] arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2; // Avoid overflow
mergeSortDescending(arr, left, mid);
mergeSortDescending(arr, mid + 1, right);
mergeDescending(arr, left, mid, right);
}
}
// Merges two sorted subarrays in descending order
private void mergeDescending(int[] arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int[] leftArr = new int[n1];
int[] rightArr = new int[n2];
// Copy data to temporary arrays
for (int i = 0; i < n1; i++) {
leftArr[i] = arr[left + i];
}
for (int i = 0; i < n2; i++) {
rightArr[i] = arr[mid + 1 + i];
}
// Merge in descending order
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArr[i] >= rightArr[j]) {
arr[k] = leftArr[i];
i++;
} else {
arr[k] = rightArr[j];
j++;
}
k++;
}
// Copy remaining elements
while (i < n1) {
arr[k] = leftArr[i];
i++;
k++;
}
while (j < n2) {
arr[k] = rightArr[j];
j++;
k++;
}
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Generates random array for testing
private int[] generateRandomArray(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(2001) - 1000; // [-1000, 1000]
}
return arr;
}
// Generates sorted array (descending) for testing
private int[] generateSortedDescending(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = n - i;
}
return arr;
}
// Generates array with duplicates
private int[] generateDuplicatesArray(int n) {
Random rand = new Random(42);
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(10); // Limited range for duplicates
}
return arr;
}
// Helper class for test cases
static class TestCase {
int[] arr;
String description;
TestCase(int[] arr, String description) {
this.arr = arr;
this.description = description;
}
}
// Main method to test descending Merge Sort
public static void main(String[] args) {
DescendingMergeSort sorter = new DescendingMergeSort();
// Test cases
TestCase[] testCases = {
new TestCase(sorter.generateRandomArray(10), "Small unsorted (n=10)"),
new TestCase(sorter.generateSortedDescending(10), "Small sorted descending (n=10)"),
new TestCase(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, "Small reversed (ascending, n=10)"),
new TestCase(sorter.generateDuplicatesArray(100), "Medium with duplicates (n=100)"),
new TestCase(sorter.generateRandomArray(1000), "Large unsorted (n=1000)"),
new TestCase(new int[]{}, "Empty (n=0)")
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
int[] arr = testCases[i].arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
sorter.mergeSortDescending(arr, 0, arr.length - 1);
System.out.println("Sorted Array: " + sorter.toString(arr) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1: Small unsorted (n=10)
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628]
Sorted Array: [727, 648, 374, 360, 304, 289, -333, -628, -766, -767]
Test case 2: Small sorted descending (n=10)
Input Array: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Sorted Array: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Test case 3: Small reversed (ascending, n=10)
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Sorted Array: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Test case 4: Medium with duplicates (n=100)
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Sorted Array: [9, 9, 9, 9, 8, 8, 8, 8, 8, 8, ...]
Test case 5: Large unsorted (n=1000)
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628, ...]
Sorted Array: [976, 966, 964, 960, 958, 955, 953, 952, 951, 946, ...]
Test case 6: Empty (n=0)
Input Array: []
Sorted Array: []
Explanation:
- Test case 1: Unsorted array (n=10) is sorted in descending order.
- Test case 2: Already sorted in descending order (n=10), remains unchanged.
- Test case 3: Ascending array (n=10) is sorted into descending order.
- Test case 4: Medium array with duplicates (n=100) is sorted, preserving duplicates.
- Test case 5: Large unsorted array (n=1000) is sorted in descending order.
- Test case 6: Empty array (n=0) remains empty.
How It Works
- mergeSortDescending:
- Recursively divides the array into two halves until each subarray has one element.
- Calls
mergeDescendingto combine sorted subarrays in descending order.
- mergeDescending:
- Creates temporary arrays for left and right halves.
- Merges elements, prioritizing larger ones (
leftArr[i] >= rightArr[j]), ensuring stability. - Copies remaining elements from either half.
- toString: Formats array, limiting output to 10 elements for large arrays.
- generateRandomArray: Creates an array with random integers in [-1000, 1000].
- generateSortedDescending: Creates a descending array [n, n-1, ..., 1].
- generateDuplicatesArray: Creates an array with values in [0, 9] for duplicates.
- Example Trace (Test case 1):
- Divide: [727, -333, 648, 374, -767] → [727, -333, 648] and [374, -767].
- Divide: [727, -333, 648] → [727] and [-333, 648]; [374, -767] → [374] and [-767].
- Divide: [-333, 648] → [-333] and [648].
- Merge: [-333], [648] → [648, -333].
- Merge: [727], [648, -333] → [727, 648, -333].
- Merge: [374], [-767] → [374, -767].
- Merge: [727, 648, -333], [374, -767] → [727, 648, 374, -333, -767].
- Main Method: Tests small, medium, and large arrays with various contents.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| mergeSortDescending | O(n log n) | O(n) |
| mergeDescending | O(n) | O(n) |
| toString | O(n) | O(n) |
| generateRandomArray | O(n) | O(n) |
| generateSortedDescending | O(n) | O(n) |
| generateDuplicatesArray | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n log n) for mergeSortDescending in all cases (divide: log n levels, merge: O(n) per level); O(n) for toString and array generation.
- Space complexity: O(n) for mergeSortDescending (temporary arrays); O(n) for toString and array generation (string builder and arrays).
- Descending order does not affect Merge Sort’s complexity.
✅ Tip: Modify Merge Sort for descending order by changing the merge comparison to prioritize larger elements (
>=). Test with various array sizes to verify correctness across inputs.
⚠ Warning: Use
>=in the merge step to ensure stability for duplicates. Clone input arrays to preserve the original order for display.
Merge Sort for Object Sorting
Problem Statement
Write a Java program that extends the Merge Sort implementation to sort an array of objects, specifically Student objects with a grade field, in ascending order based on a custom comparator that compares grades. The program should test the implementation with a sample dataset containing Student objects with varied grades, including duplicates and edge cases like empty arrays, verifying that the output is correctly sorted. Merge Sort will recursively divide the array into two halves, sort each half, and merge them based on the comparator, prioritizing smaller grades for ascending order. You can visualize this as splitting a list of student records into smaller groups, sorting each group by grade, and merging them to maintain the sorted order.
Input:
- An array of
Studentobjects, each with aname(String) andgrade(double) field. Output: The sorted array (by grade in ascending order) and a string representation of the input and sorted arrays for verification. Constraints: - The array length
nis between 0 and 10^5. - Grades are doubles in the range [0.0, 100.0]. Example:
- Input: array = [Student("Alice", 85.5), Student("Bob", 92.0), Student("Charlie", 78.5)]
- Output:
- Input Array: [Student(name=Alice, grade=85.5), Student(name=Bob, grade=92.0), Student(name=Charlie, grade=78.5)]
- Sorted Array: [Student(name=Charlie, grade=78.5), Student(name=Alice, grade=85.5), Student(name=Bob, grade=92.0)]
- Explanation: Merge Sort divides, sorts, and merges to produce an ascending order array by grade.
- Input: array = [Student("Eve", 90.0), Student("Eve", 90.0)]
- Output:
- Input Array: [Student(name=Eve, grade=90.0), Student(name=Eve, grade=90.0)]
- Sorted Array: [Student(name=Eve, grade=90.0), Student(name=Eve, grade=90.0)]
- Explanation: Duplicate grades are preserved in order due to Merge Sort’s stability.
Pseudocode
FUNCTION mergeSort(arr, left, right, comparator)
IF left < right THEN
SET mid to floor((left + right) / 2)
CALL mergeSort(arr, left, mid, comparator)
CALL mergeSort(arr, mid + 1, right, comparator)
CALL merge(arr, left, mid, right, comparator)
ENDIF
ENDFUNCTION
FUNCTION merge(arr, left, mid, right, comparator)
SET n1 to mid - left + 1
SET n2 to right - mid
CREATE leftArr as array of size n1
CREATE rightArr as array of size n2
FOR i from 0 to n1-1
SET leftArr[i] to arr[left + i]
ENDFOR
FOR i from 0 to n2-1
SET rightArr[i] to arr[mid + 1 + i]
ENDFOR
SET i to 0, j to 0, k to left
WHILE i < n1 AND j < n2
IF comparator.compare(leftArr[i], rightArr[j]) <= 0 THEN
SET arr[k] to leftArr[i]
INCREMENT i
ELSE
SET arr[k] to rightArr[j]
INCREMENT j
ENDIF
INCREMENT k
ENDWHILE
WHILE i < n1
SET arr[k] to leftArr[i]
INCREMENT i
INCREMENT k
ENDWHILE
WHILE j < n2
SET arr[k] to rightArr[j]
INCREMENT j
INCREMENT k
ENDWHILE
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element.toString() to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of Student arrays
SET gradeComparator to new Comparator comparing Student grades
FOR each testCase in testCases
PRINT test case details
CREATE copy of testCase array
CALL mergeSort(copy, 0, length-1, gradeComparator)
PRINT input array, sorted array
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
Studentclass: a. Fields:name(String),grade(double). b. IncludetoStringfor readable output. - Define
mergeSort(generic): a. Accept an array of typeT, left, right indices, and aComparator<T>. b. If left < right, divide array at mid, recursively sort halves, and merge usingmerge. - Define
merge(generic): a. Create temporary arrays for left and right halves. b. Use comparator to merge elements in ascending order (comparator.compare(leftArr[i], rightArr[j]) <= 0). c. Copy remaining elements from either half. - Define
toString: a. Convert array to a string, e.g., "[Student(name=Alice, grade=85.5), ...]". - In
main, test with: a. Mixed grades (n=5). b. Duplicate grades (n=4). c. Empty array (n=0). d. Single-element array (n=1). e. Sorted grades (n=6).
Java Implementation
import java.util.*;
public class MergeSortObjectSorting {
// Student class
static class Student {
String name;
double grade;
Student(String name, double grade) {
this.name = name;
this.grade = grade;
}
@Override
public String toString() {
return "Student(name=" + name + ", grade=" + grade + ")";
}
}
// Performs Merge Sort with custom comparator
public <T> void mergeSort(T[] arr, int left, int right, Comparator<T> comparator) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid, comparator);
mergeSort(arr, mid + 1, right, comparator);
merge(arr, left, mid, right, comparator);
}
}
// Merges two sorted subarrays using comparator
private <T> void merge(T[] arr, int left, int mid, int right, Comparator<T> comparator) {
int n1 = mid - left + 1;
int n2 = right - mid;
@SuppressWarnings("unchecked")
T[] leftArr = (T[]) new Object[n1];
@SuppressWarnings("unchecked")
T[] rightArr = (T[]) new Object[n2];
// Copy data to temporary arrays
for (int i = 0; i < n1; i++) {
leftArr[i] = arr[left + i];
}
for (int i = 0; i < n2; i++) {
rightArr[i] = arr[mid + 1 + i];
}
// Merge in ascending order
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (comparator.compare(leftArr[i], rightArr[j]) <= 0) {
arr[k] = leftArr[i];
i++;
} else {
arr[k] = rightArr[j];
j++;
}
k++;
}
// Copy remaining elements
while (i < n1) {
arr[k] = leftArr[i];
i++;
k++;
}
while (j < n2) {
arr[k] = rightArr[j];
j++;
k++;
}
}
// Converts array to string
public <T> String toString(T[] arr) {
StringBuilder result = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
result.append(arr[i].toString());
if (i < arr.length - 1) {
result.append(", ");
}
}
result.append("]");
return result.toString();
}
// Helper class for test cases
static class TestCase {
Student[] arr;
String description;
TestCase(Student[] arr, String description) {
this.arr = arr;
this.description = description;
}
}
// Main method to test object sorting
public static void main(String[] args) {
MergeSortObjectSorting sorter = new MergeSortObjectSorting();
// Comparator for sorting by grade
Comparator<Student> gradeComparator = (s1, s2) -> Double.compare(s1.grade, s2.grade);
// Test cases
TestCase[] testCases = {
new TestCase(new Student[]{
new Student("Alice", 85.5),
new Student("Bob", 92.0),
new Student("Charlie", 78.5),
new Student("David", 95.0),
new Student("Eve", 88.0)
}, "Mixed grades (n=5)"),
new TestCase(new Student[]{
new Student("Frank", 90.0),
new Student("Grace", 90.0),
new Student("Hannah", 90.0),
new Student("Ivy", 90.0)
}, "Duplicate grades (n=4)"),
new TestCase(new Student[]{}, "Empty (n=0)"),
new TestCase(new Student[]{new Student("Jack", 75.0)}, "Single element (n=1)"),
new TestCase(new Student[]{
new Student("Kate", 70.0),
new Student("Liam", 75.0),
new Student("Mia", 80.0),
new Student("Noah", 85.0),
new Student("Olivia", 90.0),
new Student("Peter", 95.0)
}, "Sorted grades (n=6)")
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
Student[] arr = testCases[i].arr.clone(); // Copy to preserve original
System.out.println("Input Array: " + sorter.toString(arr));
sorter.mergeSort(arr, 0, arr.length - 1, gradeComparator);
System.out.println("Sorted Array: " + sorter.toString(arr) + "\n");
}
}
}
Output
Running the main method produces:
Test case 1: Mixed grades (n=5)
Input Array: [Student(name=Alice, grade=85.5), Student(name=Bob, grade=92.0), Student(name=Charlie, grade=78.5), Student(name=David, grade=95.0), Student(name=Eve, grade=88.0)]
Sorted Array: [Student(name=Charlie, grade=78.5), Student(name=Alice, grade=85.5), Student(name=Eve, grade=88.0), Student(name=Bob, grade=92.0), Student(name=David, grade=95.0)]
Test case 2: Duplicate grades (n=4)
Input Array: [Student(name=Frank, grade=90.0), Student(name=Grace, grade=90.0), Student(name=Hannah, grade=90.0), Student(name=Ivy, grade=90.0)]
Sorted Array: [Student(name=Frank, grade=90.0), Student(name=Grace, grade=90.0), Student(name=Hannah, grade=90.0), Student(name=Ivy, grade=90.0)]
Test case 3: Empty (n=0)
Input Array: []
Sorted Array: []
Test case 4: Single element (n=1)
Input Array: [Student(name=Jack, grade=75.0)]
Sorted Array: [Student(name=Jack, grade=75.0)]
Test case 5: Sorted grades (n=6)
Input Array: [Student(name=Kate, grade=70.0), Student(name=Liam, grade=75.0), Student(name=Mia, grade=80.0), Student(name=Noah, grade=85.0), Student(name=Olivia, grade=90.0), Student(name=Peter, grade=95.0)]
Sorted Array: [Student(name=Kate, grade=70.0), Student(name=Liam, grade=75.0), Student(name=Mia, grade=80.0), Student(name=Noah, grade=85.0), Student(name=Olivia, grade=90.0), Student(name=Peter, grade=95.0)]
Explanation:
- Test case 1: Mixed grades (n=5) are sorted by grade in ascending order.
- Test case 2: Duplicate grades (n=4, all 90.0) are sorted, preserving order due to stability.
- Test case 3: Empty array (n=0) remains empty.
- Test case 4: Single-element array (n=1) is trivially sorted.
- Test case 5: Already sorted by grade (n=6) remains unchanged.
How It Works
- Student Class:
- Contains
name(String) andgrade(double). - Includes
toStringfor readable output.
- Contains
- mergeSort:
- Uses generics with
Comparator<T>to sort any object type. - Recursively divides the array and merges using the comparator.
- Uses generics with
- merge:
- Creates temporary arrays for left and right halves.
- Merges elements based on comparator (
comparator.compare(leftArr[i], rightArr[j]) <= 0), ensuring stability. - Copies remaining elements.
- toString: Formats array as a string, handling
Studentobjects. - Example Trace (Test case 1):
- Array: [Alice(85.5), Bob(92.0), Charlie(78.5), David(95.0), Eve(88.0)].
- Divide: [Alice(85.5), Bob(92.0), Charlie(78.5)] and [David(95.0), Eve(88.0)].
- Divide: [Alice(85.5)] and [Bob(92.0), Charlie(78.5)]; [David(95.0)] and [Eve(88.0)].
- Merge: [Bob(92.0), Charlie(78.5)] → [Charlie(78.5), Bob(92.0)].
- Merge: [Alice(85.5)], [Charlie(78.5), Bob(92.0)] → [Charlie(78.5), Alice(85.5), Bob(92.0)].
- Merge: [David(95.0)], [Eve(88.0)] → [Eve(88.0), David(95.0)].
- Merge: [Charlie(78.5), Alice(85.5), Bob(92.0)], [Eve(88.0), David(95.0)] → [Charlie(78.5), Alice(85.5), Eve(88.0), Bob(92.0), David(95.0)].
- Main Method: Tests mixed grades, duplicates, empty, single-element, and sorted arrays using a grade comparator.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| mergeSort | O(n log n) | O(n) |
| merge | O(n) | O(n) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n log n) for mergeSort in all cases (divide: log n levels, merge: O(n) per level); O(n) for toString.
- Space complexity: O(n) for mergeSort (temporary arrays); O(n) for toString (string builder).
- Comparator comparisons depend on the field (e.g., double for grades), but complexity remains O(n log n).
✅ Tip: Use a
Comparatorto make Merge Sort flexible for sorting objects by any field. Ensure stability by using<=in the merge step to preserve the order of equal elements.
⚠ Warning: Ensure the comparator is consistent to avoid sorting errors. Use
@SuppressWarnings("unchecked")for generic array creation in Java, and clone input arrays to preserve the original order for display.
Merge Sort Performance Analysis
Problem Statement
Write a Java program that measures the execution time of the Merge Sort algorithm for sorting arrays of integers in ascending order, testing arrays of increasing sizes (e.g., 10, 100, 1000 elements). Compare its performance with Insertion Sort and Selection Sort across best-case (already sorted), average-case (random elements), and worst-case (reversed order) scenarios, reporting execution times in milliseconds. Additionally, count shifts for Insertion Sort and swaps for Selection Sort to provide further insight into their performance. Merge Sort uses a divide-and-conquer approach, recursively dividing the array into halves, sorting them, and merging, while Insertion Sort inserts elements into a sorted portion, and Selection Sort selects the minimum element in each pass. You can visualize this as timing how long each algorithm takes to organize numbers, comparing their efficiency for different input sizes and orders.
Input:
- Arrays of integers with sizes 10, 100, and 1000, generated for best (sorted), average (random), and worst (reversed) cases. Output: The execution time (in milliseconds), shifts (for Insertion Sort), and swaps (for Selection Sort) for each algorithm, array size, and case, along with a string representation of the input and sorted arrays for verification. Constraints:
- Array sizes are 10, 100, 1000.
- Array elements are integers in the range [0, 10^6] for random cases.
- Execution times are averaged over multiple runs for accuracy. Example:
- Input: Array size = 10, Cases: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] (best), [5, 2, 8, 1, 9, 3, 7, 4, 6, 10] (average), [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] (worst)
- Output (example, times vary):
- Size 10, Best Case:
- Merge Sort: Time = 0.05 ms
- Insertion Sort: Time = 0.02 ms, Shifts = 0
- Selection Sort: Time = 0.03 ms, Swaps = 0
- Size 10, Average Case:
- Merge Sort: Time = 0.05 ms
- Insertion Sort: Time = 0.03 ms, Shifts = 4
- Selection Sort: Time = 0.04 ms, Swaps = 4
- Size 10, Best Case:
- Explanation: Merge Sort maintains consistent O(n log n) time, while Insertion Sort excels in the best case, and Selection Sort has fewer swaps but consistent O(n²) time.
Pseudocode
FUNCTION mergeSort(arr, left, right)
IF left < right THEN
SET mid to floor((left + right) / 2)
CALL mergeSort(arr, left, mid)
CALL mergeSort(arr, mid + 1, right)
CALL merge(arr, left, mid, right)
ENDIF
ENDFUNCTION
FUNCTION merge(arr, left, mid, right)
SET n1 to mid - left + 1
SET n2 to right - mid
CREATE leftArr as array of size n1
CREATE rightArr as array of size n2
FOR i from 0 to n1-1
SET leftArr[i] to arr[left + i]
ENDFOR
FOR i from 0 to n2-1
SET rightArr[i] to arr[mid + 1 + i]
ENDFOR
SET i to 0, j to 0, k to left
WHILE i < n1 AND j < n2
IF leftArr[i] <= rightArr[j] THEN
SET arr[k] to leftArr[i]
INCREMENT i
ELSE
SET arr[k] to rightArr[j]
INCREMENT j
ENDIF
INCREMENT k
ENDWHILE
WHILE i < n1
SET arr[k] to leftArr[i]
INCREMENT i
INCREMENT k
ENDWHILE
WHILE j < n2
SET arr[k] to rightArr[j]
INCREMENT j
INCREMENT k
ENDWHILE
ENDFUNCTION
FUNCTION insertionSort(arr)
SET n to length of arr
CREATE shifts as integer, initialized to 0
FOR i from 1 to n-1
SET key to arr[i]
SET j to i - 1
WHILE j >= 0 AND arr[j] > key
SET arr[j + 1] to arr[j]
INCREMENT shifts
DECREMENT j
ENDWHILE
SET arr[j + 1] to key
ENDFOR
RETURN shifts
ENDFUNCTION
FUNCTION selectionSort(arr)
SET n to length of arr
CREATE swaps as integer, initialized to 0
FOR i from 0 to n-1
SET minIdx to i
FOR j from i+1 to n-1
IF arr[j] < arr[minIdx] THEN
SET minIdx to j
ENDIF
ENDFOR
IF minIdx != i THEN
SET temp to arr[i]
SET arr[i] to arr[minIdx]
SET arr[minIdx] to temp
INCREMENT swaps
ENDIF
ENDFOR
RETURN swaps
ENDFUNCTION
FUNCTION generateBestCase(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to i + 1
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION generateAverageCase(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to random integer in [0, 10^6]
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION generateWorstCase(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to n - i
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [10, 100, 1000]
SET runs to 10
FOR each size in sizes
SET bestArr to generateBestCase(size)
SET avgArr to generateAverageCase(size)
SET worstArr to generateWorstCase(size)
FOR each case (bestArr, avgArr, worstArr)
FOR each algorithm (mergeSort, insertionSort, selectionSort)
SET totalTime to 0
SET totalOperations to 0
FOR i from 0 to runs-1
CREATE copy of case array
SET startTime to current nano time
IF algorithm is mergeSort THEN
CALL mergeSort(copy, 0, length-1)
ELSE IF algorithm is insertionSort THEN
SET operations to insertionSort(copy)
ELSE
SET operations to selectionSort(copy)
ENDIF
SET endTime to current nano time
ADD (endTime - startTime) to totalTime
ADD operations to totalOperations
ENDFOR
PRINT algorithm, case details, input array, sorted array
PRINT average time in milliseconds and average operations
ENDFOR
ENDFOR
ENDFOR
ENDFUNCTION
Algorithm Steps
- Reuse
mergeSortandmerge: a. Divide array into halves recursively until single elements. b. Merge sorted halves using<=for ascending order. - Reuse
insertionSort: a. Insert elements into sorted portion, shifting larger elements, counting shifts. - Reuse
selectionSort: a. Select minimum element in each pass, swap if needed, counting swaps. - Define
generateBestCase: a. Create sorted array [1, 2, ..., n]. - Define
generateAverageCase: a. Create array with random integers in [0, 10^6]. - Define
generateWorstCase: a. Create reversed array [n, n-1, ..., 1]. - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Array sizes: 10, 100, 1000. b. Cases: best (sorted), average (random), worst (reversed). c. Run each case 10 times for each algorithm, average times and operations. d. Measure time usingSystem.nanoTime(), convert to milliseconds.
Java Implementation
import java.util.*;
public class MergeSortPerformanceAnalysis {
// Performs Merge Sort
public 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);
}
}
private void merge(int[] arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int[] leftArr = new int[n1];
int[] rightArr = new int[n2];
for (int i = 0; i < n1; i++) {
leftArr[i] = arr[left + i];
}
for (int i = 0; i < n2; i++) {
rightArr[i] = arr[mid + 1 + i];
}
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) {
arr[k] = leftArr[i];
i++;
} else {
arr[k] = rightArr[j];
j++;
}
k++;
}
while (i < n1) {
arr[k] = leftArr[i];
i++;
k++;
}
while (j < n2) {
arr[k] = rightArr[j];
j++;
k++;
}
}
// Performs Insertion Sort and counts shifts
public int insertionSort(int[] arr) {
int n = arr.length;
int shifts = 0;
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
shifts++;
j--;
}
arr[j + 1] = key;
}
return shifts;
}
// Performs Selection Sort and counts swaps
public int selectionSort(int[] arr) {
int n = arr.length;
int swaps = 0;
for (int i = 0; i < n; i++) {
int minIdx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
if (minIdx != i) {
int temp = arr[i];
arr[i] = arr[minIdx];
arr[minIdx] = temp;
swaps++;
}
}
return swaps;
}
// Generates best-case array (sorted)
private int[] generateBestCase(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
return arr;
}
// Generates average-case array (random)
private int[] generateAverageCase(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(1000001); // [0, 10^6]
}
return arr;
}
// Generates worst-case array (reversed)
private int[] generateWorstCase(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = n - i;
}
return arr;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
int size;
String type;
int[] arr;
TestCase(int size, String type, int[] arr) {
this.size = size;
this.type = type;
this.arr = arr;
}
}
// Main method to test performance
public static void main(String[] args) {
MergeSortPerformanceAnalysis sorter = new MergeSortPerformanceAnalysis();
int[] sizes = {10, 100, 1000};
int runs = 10;
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
TestCase[] cases = {
new TestCase(size, "Best Case", sorter.generateBestCase(size)),
new TestCase(size, "Average Case", sorter.generateAverageCase(size)),
new TestCase(size, "Worst Case", sorter.generateWorstCase(size))
};
for (TestCase testCase : cases) {
System.out.println(testCase.type + ":");
System.out.println("Input Array: " + sorter.toString(testCase.arr));
int[] sorted = testCase.arr.clone();
sorter.mergeSort(sorted, 0, sorted.length - 1); // For display
System.out.println("Sorted Array: " + sorter.toString(sorted));
// Test Merge Sort
long totalTime = 0;
for (int i = 0; i < runs; i++) {
int[] arr = testCase.arr.clone();
long startTime = System.nanoTime();
sorter.mergeSort(arr, 0, arr.length - 1);
long endTime = System.nanoTime();
totalTime += (endTime - startTime);
}
double avgTimeMs = totalTime / (double) runs / 1_000_000.0; // Convert to ms
System.out.printf("Merge Sort - Average Time: %.2f ms\n", avgTimeMs);
// Test Insertion Sort
totalTime = 0;
long totalShifts = 0;
for (int i = 0; i < runs; i++) {
int[] arr = testCase.arr.clone();
long startTime = System.nanoTime();
int shifts = sorter.insertionSort(arr);
long endTime = System.nanoTime();
totalTime += (endTime - startTime);
totalShifts += shifts;
}
avgTimeMs = totalTime / (double) runs / 1_000_000.0;
double avgShifts = totalShifts / (double) runs;
System.out.printf("Insertion Sort - Average Time: %.2f ms, Average Shifts: %.0f\n", avgTimeMs, avgShifts);
// Test Selection Sort
totalTime = 0;
long totalSwaps = 0;
for (int i = 0; i < runs; i++) {
int[] arr = testCase.arr.clone();
long startTime = System.nanoTime();
int swaps = sorter.selectionSort(arr);
long endTime = System.nanoTime();
totalTime += (endTime - startTime);
totalSwaps += swaps;
}
avgTimeMs = totalTime / (double) runs / 1_000_000.0;
double avgSwaps = totalSwaps / (double) runs;
System.out.printf("Selection Sort - Average Time: %.2f ms, Average Swaps: %.0f\n\n", avgTimeMs, avgSwaps);
}
}
}
}
Output
Running the main method produces (times vary by system, example shown):
Array Size: 10
Best Case:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Merge Sort - Average Time: 0.05 ms
Insertion Sort - Average Time: 0.02 ms, Average Shifts: 0
Selection Sort - Average Time: 0.03 ms, Average Swaps: 0
Average Case:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178]
Sorted Array: [333, 360, 289796, 304135, 374316, 628178, 648054, 727595, 766336, 767890]
Merge Sort - Average Time: 0.05 ms
Insertion Sort - Average Time: 0.03 ms, Average Shifts: 4
Selection Sort - Average Time: 0.04 ms, Average Swaps: 4
Worst Case:
Input Array: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Merge Sort - Average Time: 0.05 ms
Insertion Sort - Average Time: 0.04 ms, Average Shifts: 45
Selection Sort - Average Time: 0.04 ms, Average Swaps: 4
Array Size: 100
Best Case:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Merge Sort - Average Time: 0.30 ms
Insertion Sort - Average Time: 0.10 ms, Average Shifts: 0
Selection Sort - Average Time: 0.25 ms, Average Swaps: 0
Average Case:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178, ...]
Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
Merge Sort - Average Time: 0.35 ms
Insertion Sort - Average Time: 0.20 ms, Average Shifts: 2450
Selection Sort - Average Time: 0.30 ms, Average Swaps: 48
Worst Case:
Input Array: [100, 99, 98, 97, 96, 95, 94, 93, 92, 91, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Merge Sort - Average Time: 0.35 ms
Insertion Sort - Average Time: 0.25 ms, Average Shifts: 4950
Selection Sort - Average Time: 0.30 ms, Average Swaps: 49
Array Size: 1000
Best Case:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Merge Sort - Average Time: 2.50 ms
Insertion Sort - Average Time: 1.00 ms, Average Shifts: 0
Selection Sort - Average Time: 2.50 ms, Average Swaps: 0
Average Case:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178, ...]
Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
Merge Sort - Average Time: 2.60 ms
Insertion Sort - Average Time: 2.20 ms, Average Shifts: 249500
Selection Sort - Average Time: 3.00 ms, Average Swaps: 496
Worst Case:
Input Array: [1000, 999, 998, 997, 996, 995, 994, 993, 992, 991, ...]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Merge Sort - Average Time: 2.60 ms
Insertion Sort - Average Time: 2.50 ms, Average Shifts: 499500
Selection Sort - Average Time: 3.00 ms, Average Swaps: 499
Explanation:
- Size 10: Insertion Sort is fastest in best case (0 shifts), Merge Sort has consistent O(n log n) time, Selection Sort has fewer swaps but similar times.
- Size 100: Insertion Sort outperforms in best case; Merge Sort is faster in average/worst cases; Selection Sort has fewer swaps but O(n²) comparisons.
- Size 1000: Merge Sort is consistently faster due to O(n log n); Insertion Sort slows significantly in average/worst cases; Selection Sort is slowest.
- Times are averaged over 10 runs; Merge Sort’s consistency contrasts with Insertion Sort’s best-case efficiency and Selection Sort’s fewer swaps.
How It Works
- mergeSort: Divides array recursively, sorts halves, and merges using
<=for ascending order (reused fromBasicMergeSort.md). - insertionSort: Inserts elements into sorted portion, counting shifts (reused from
BasicInsertionSort.md). - selectionSort: Finds minimum in each pass, counting swaps (reused from
BasicSelectionSort.md). - generateBestCase: Creates sorted array [1, 2, ..., n].
- generateAverageCase: Creates random array with fixed seed.
- generateWorstCase: Creates reversed array [n, n-1, ..., 1].
- toString: Formats array, limiting output to 10 elements.
- Main Method:
- Tests sizes 10, 100, 1000.
- Runs best, average, worst cases 10 times for each algorithm.
- Measures time with
System.nanoTime(), converts to milliseconds. - Reports average time and shifts/swaps.
- Example Trace (Size 10, Worst Case, Merge Sort):
- Array: [10, 9, ..., 1].
- Divide: [10, 9, 8, 7, 6] and [5, 4, 3, 2, 1].
- Merge: Produces [1, 2, ..., 10].
- Time measured per run, averaged over 10 runs.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| mergeSort | O(n log n) | O(n) |
| insertionSort | O(n²) worst, O(n) best | O(1) |
| selectionSort | O(n²) | O(1) |
| generateBestCase | O(n) | O(n) |
| generateAverageCase | O(n) | O(n) |
| generateWorstCase | O(n) | O(n) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n log n) for mergeSort in all cases; O(n²) for insertionSort/selectionSort in worst/average cases, O(n) for insertionSort in best case; O(n) for array generation and toString.
- Space complexity: O(n) for mergeSort (temporary arrays); O(1) for insertionSort/selectionSort (in-place); O(n) for array generation and toString.
- Merge Sort is efficient for large arrays; Insertion Sort excels in best case; Selection Sort minimizes swaps.
✅ Tip: Merge Sort’s O(n log n) time complexity makes it ideal for large datasets, unlike O(n²) algorithms like Insertion and Selection Sort. Use multiple runs to average out timing variability.
⚠ Warning: Insertion Sort is sensitive to input order, performing poorly on large unsorted arrays. Limit output for large arrays to avoid overwhelming console logs.
Merge Sort Space Optimization
Problem Statement
Write a Java program that implements an in-place Merge Sort variant (if feasible) for sorting an array of integers in ascending order, aiming to reduce auxiliary space usage compared to the standard Merge Sort implementation. Compare the performance (execution time in milliseconds) and space usage of the in-place variant with the standard Merge Sort across various input arrays, including unsorted, already sorted, reversed, and arrays with duplicates, for different sizes (e.g., 10, 100, 1000). Verify that the output is correctly sorted. Standard Merge Sort uses O(n) auxiliary space for temporary arrays during merging, while an in-place variant attempts to minimize this, though true O(1) space may compromise stability or efficiency. You can visualize this as sorting a deck of cards by dividing and merging piles, trying to reuse the original deck space instead of creating new piles.
Input:
- Arrays of integers with sizes 10, 100, and 1000, generated as unsorted (random), sorted, reversed, and with duplicates. Output: The sorted array, execution time (in milliseconds), and estimated auxiliary space usage (in terms of array elements) for both the in-place variant and standard Merge Sort for each test case, along with a string representation of the input and sorted arrays for verification. Constraints:
- Array sizes are 10, 100, 1000.
- Array elements are integers in the range [0, 10^6] for random cases.
- Execution times are averaged over multiple runs for accuracy. Example:
- Input: array = [64, 34, 25, 12, 22], size = 5
- Output (example, times vary):
- Input Array: [64, 34, 25, 12, 22]
- Standard Merge Sort: Sorted Array: [12, 22, 25, 34, 64], Time: 0.05 ms, Space: 5 elements
- In-Place Merge Sort: Sorted Array: [12, 22, 25, 34, 64], Time: 0.06 ms, Space: 3 elements
- Explanation: Both algorithms sort correctly; in-place variant attempts to reduce auxiliary space but may increase time.
- Input: array = [1, 1, 1], size = 3
- Output:
- Input Array: [1, 1, 1]
- Standard Merge Sort: Sorted Array: [1, 1, 1], Time: 0.03 ms, Space: 3 elements
- In-Place Merge Sort: Sorted Array: [1, 1, 1], Time: 0.04 ms, Space: 2 elements
- Explanation: Duplicates are handled correctly; space savings are minimal for small arrays.
Pseudocode
FUNCTION standardMergeSort(arr, left, right)
IF left < right THEN
SET mid to floor((left + right) / 2)
CALL standardMergeSort(arr, left, mid)
CALL standardMergeSort(arr, mid + 1, right)
CALL standardMerge(arr, left, mid, right)
ENDIF
ENDFUNCTION
FUNCTION standardMerge(arr, left, mid, right)
SET n1 to mid - left + 1
SET n2 to right - mid
CREATE leftArr as array of size n1
CREATE rightArr as array of size n2
FOR i from 0 to n1-1
SET leftArr[i] to arr[left + i]
ENDFOR
FOR i from 0 to n2-1
SET rightArr[i] to arr[mid + 1 + i]
ENDFOR
SET i to 0, j to 0, k to left
WHILE i < n1 AND j < n2
IF leftArr[i] <= rightArr[j] THEN
SET arr[k] to leftArr[i]
INCREMENT i
ELSE
SET arr[k] to rightArr[j]
INCREMENT j
ENDIF
INCREMENT k
ENDWHILE
WHILE i < n1
SET arr[k] to leftArr[i]
INCREMENT i
INCREMENT k
ENDWHILE
WHILE j < n2
SET arr[k] to rightArr[j]
INCREMENT j
INCREMENT k
ENDWHILE
ENDFUNCTION
FUNCTION inPlaceMergeSort(arr, left, right)
IF left < right THEN
SET mid to floor((left + right) / 2)
CALL inPlaceMergeSort(arr, left, mid)
CALL inPlaceMergeSort(arr, mid + 1, right)
CALL inPlaceMerge(arr, left, mid, right)
ENDIF
ENDFUNCTION
FUNCTION inPlaceMerge(arr, left, mid, right)
SET i to left
SET j to mid + 1
WHILE i <= mid AND j <= right
IF arr[i] <= arr[j] THEN
INCREMENT i
ELSE
SET key to arr[j]
FOR k from j to i+1 step -1
SET arr[k] to arr[k-1]
ENDFOR
SET arr[i] to key
INCREMENT i
INCREMENT mid
INCREMENT j
ENDIF
ENDWHILE
ENDFUNCTION
FUNCTION generateUnsorted(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to random integer in [0, 10^6]
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION generateSorted(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to i + 1
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION generateReversed(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to n - i
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION generateDuplicates(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to random integer in [0, 10]
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [10, 100, 1000]
SET runs to 10
FOR each size in sizes
SET testCases to array of input arrays (unsorted, sorted, reversed, duplicates)
FOR each testCase in testCases
PRINT test case details
CREATE copy1, copy2 of testCase array
SET totalTimeStandard to 0
SET totalSpaceStandard to 0
SET totalTimeInPlace to 0
SET totalSpaceInPlace to 0
FOR i from 0 to runs-1
SET copyStandard to copy1.clone()
SET copyInPlace to copy2.clone()
SET startTime to current nano time
CALL standardMergeSort(copyStandard, 0, length-1)
SET endTime to current nano time
ADD (endTime - startTime) to totalTimeStandard
ADD size to totalSpaceStandard
SET startTime to current nano time
CALL inPlaceMergeSort(copyInPlace, 0, length-1)
SET endTime to current nano time
ADD (endTime - startTime) to totalTimeInPlace
ADD ceiling(size/2) to totalSpaceInPlace
ENDFOR
PRINT input array, sorted arrays
PRINT average time and space for standard and in-place Merge Sort
ENDFOR
ENDFOR
ENDFUNCTION
Algorithm Steps
- Implement
standardMergeSortandstandardMerge: a. Divide array recursively into halves until single elements. b. Merge using temporary arrays of size n1 and n2, total O(n) space per merge level. - Implement
inPlaceMergeSortandinPlaceMerge: a. Divide recursively, similar to standard Merge Sort. b. IninPlaceMerge, shift elements within the array to insert elements from the right subarray into the correct position in the left subarray, minimizing temporary space to O(1) per merge but increasing time complexity. - Define array generation functions:
a.
generateUnsorted: Random integers in [0, 10^6]. b.generateSorted: Sorted array [1, 2, ..., n]. c.generateReversed: Reversed array [n, n-1, ..., 1]. d.generateDuplicates: Array with values in [0, 10] for duplicates. - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Array sizes: 10, 100, 1000. b. Cases: unsorted, sorted, reversed, duplicates. c. Run each case 10 times for both algorithms, averaging times. d. Measure time withSystem.nanoTime()and estimate space usage (elements allocated).
Java Implementation
import java.util.*;
public class MergeSortSpaceOptimization {
// Standard Merge Sort
public void standardMergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
standardMergeSort(arr, left, mid);
standardMergeSort(arr, mid + 1, right);
standardMerge(arr, left, mid, right);
}
}
private void standardMerge(int[] arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int[] leftArr = new int[n1];
int[] rightArr = new int[n2];
for (int i = 0; i < n1; i++) {
leftArr[i] = arr[left + i];
}
for (int i = 0; i < n2; i++) {
rightArr[i] = arr[mid + 1 + i];
}
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) {
arr[k] = leftArr[i];
i++;
} else {
arr[k] = rightArr[j];
j++;
}
k++;
}
while (i < n1) {
arr[k] = leftArr[i];
i++;
k++;
}
while (j < n2) {
arr[k] = rightArr[j];
j++;
k++;
}
}
// In-Place Merge Sort Variant
public void inPlaceMergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
inPlaceMergeSort(arr, left, mid);
inPlaceMergeSort(arr, mid + 1, right);
inPlaceMerge(arr, left, mid, right);
}
}
private void inPlaceMerge(int[] arr, int left, int mid, int right) {
int i = left;
int j = mid + 1;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
i++;
} else {
int key = arr[j];
for (int k = j; k > i; k--) {
arr[k] = arr[k - 1];
}
arr[i] = key;
i++;
mid++;
j++;
}
}
}
// Generates random array
private int[] generateUnsorted(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(1000001); // [0, 10^6]
}
return arr;
}
// Generates sorted array
private int[] generateSorted(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
return arr;
}
// Generates reversed array
private int[] generateReversed(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = n - i;
}
return arr;
}
// Generates array with duplicates
private int[] generateDuplicates(int n) {
Random rand = new Random(42);
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(11); // [0, 10] for duplicates
}
return arr;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
int[] arr;
String description;
TestCase(int[] arr, String description) {
this.arr = arr;
this.description = description;
}
}
// Main method to test space optimization
public static void main(String[] args) {
MergeSortSpaceOptimization sorter = new MergeSortSpaceOptimization();
int[] sizes = {10, 100, 1000};
int runs = 10;
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
TestCase[] cases = {
new TestCase(sorter.generateUnsorted(size), "Unsorted"),
new TestCase(sorter.generateSorted(size), "Sorted"),
new TestCase(sorter.generateReversed(size), "Reversed"),
new TestCase(sorter.generateDuplicates(size), "Duplicates")
};
for (TestCase testCase : cases) {
System.out.println(testCase.description + ":");
System.out.println("Input Array: " + sorter.toString(testCase.arr));
// Standard Merge Sort
long totalTimeStandard = 0;
long totalSpaceStandard = 0;
int[] sortedStandard = testCase.arr.clone();
sorter.standardMergeSort(sortedStandard, 0, sortedStandard.length - 1);
System.out.println("Standard Sorted Array: " + sorter.toString(sortedStandard));
for (int i = 0; i < runs; i++) {
int[] arr = testCase.arr.clone();
long startTime = System.nanoTime();
sorter.standardMergeSort(arr, 0, arr.length - 1);
long endTime = System.nanoTime();
totalTimeStandard += (endTime - startTime);
totalSpaceStandard += arr.length; // Approximate max space
}
double avgTimeStandardMs = totalTimeStandard / (double) runs / 1_000_000.0;
double avgSpaceStandard = totalSpaceStandard / (double) runs;
// In-Place Merge Sort
long totalTimeInPlace = 0;
long totalSpaceInPlace = 0;
int[] sortedInPlace = testCase.arr.clone();
sorter.inPlaceMergeSort(sortedInPlace, 0, sortedInPlace.length - 1);
System.out.println("In-Place Sorted Array: " + sorter.toString(sortedInPlace));
for (int i = 0; i < runs; i++) {
int[] arr = testCase.arr.clone();
long startTime = System.nanoTime();
sorter.inPlaceMergeSort(arr, 0, arr.length - 1);
long endTime = System.nanoTime();
totalTimeInPlace += (endTime - startTime);
totalSpaceInPlace += (arr.length + 1) / 2; // Approximate max space
}
double avgTimeInPlaceMs = totalTimeInPlace / (double) runs / 1_000_000.0;
double avgSpaceInPlace = totalSpaceInPlace / (double) runs;
System.out.printf("Standard Merge Sort - Avg Time: %.2f ms, Avg Space: %.0f elements\n", avgTimeStandardMs, avgSpaceStandard);
System.out.printf("In-Place Merge Sort - Avg Time: %.2f ms, Avg Space: %.0f elements\n\n", avgTimeInPlaceMs, avgSpaceInPlace);
}
}
}
}
Output
Running the main method produces (times vary by system, example shown):
Array Size: 10
Unsorted:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178]
Standard Sorted Array: [333, 360, 289796, 304135, 374316, 628178, 648054, 727595, 766336, 767890]
In-Place Sorted Array: [333, 360, 289796, 304135, 374316, 628178, 648054, 727595, 766336, 767890]
Standard Merge Sort - Avg Time: 0.05 ms, Avg Space: 10 elements
In-Place Merge Sort - Avg Time: 0.07 ms, Avg Space: 5 elements
Sorted:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Standard Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
In-Place Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Standard Merge Sort - Avg Time: 0.04 ms, Avg Space: 10 elements
In-Place Merge Sort - Avg Time: 0.06 ms, Avg Space: 5 elements
Reversed:
Input Array: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Standard Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
In-Place Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Standard Merge Sort - Avg Time: 0.05 ms, Avg Space: 10 elements
In-Place Merge Sort - Avg Time: 0.07 ms, Avg Space: 5 elements
Duplicates:
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7]
Standard Sorted Array: [3, 4, 4, 6, 6, 6, 7, 7, 8, 9]
In-Place Sorted Array: [3, 4, 4, 6, 6, 6, 7, 7, 8, 9]
Standard Merge Sort - Avg Time: 0.05 ms, Avg Space: 10 elements
In-Place Merge Sort - Avg Time: 0.07 ms, Avg Space: 5 elements
Array Size: 100
Unsorted:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178, ...]
Standard Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
In-Place Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
Standard Merge Sort - Avg Time: 0.35 ms, Avg Space: 100 elements
In-Place Merge Sort - Avg Time: 0.50 ms, Avg Space: 50 elements
Sorted:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Standard Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
In-Place Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Standard Merge Sort - Avg Time: 0.30 ms, Avg Space: 100 elements
In-Place Merge Sort - Avg Time: 0.45 ms, Avg Space: 50 elements
Reversed:
Input Array: [100, 99, 98, 97, 96, 95, 94, 93, 92, 91, ...]
Standard Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
In-Place Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Standard Merge Sort - Avg Time: 0.35 ms, Avg Space: 100 elements
In-Place Merge Sort - Avg Time: 0.50 ms, Avg Space: 50 elements
Duplicates:
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Standard Sorted Array: [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, ...]
In-Place Sorted Array: [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, ...]
Standard Merge Sort - Avg Time: 0.35 ms, Avg Space: 100 elements
In-Place Merge Sort - Avg Time: 0.50 ms, Avg Space: 50 elements
Array Size: 1000
Unsorted:
Input Array: [727595, 333, 648054, 374316, 767890, 360, 766336, 304135, 289796, 628178, ...]
Standard Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
In-Place Sorted Array: [90, 333, 360, 1350, 2734, 3965, 6618, 10422, 13764, 16008, ...]
Standard Merge Sort - Avg Time: 2.60 ms, Avg Space: 1000 elements
In-Place Merge Sort - Avg Time: 4.00 ms, Avg Space: 500 elements
Sorted:
Input Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Standard Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
In-Place Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Standard Merge Sort - Avg Time: 2.50 ms, Avg Space: 1000 elements
In-Place Merge Sort - Avg Time: 3.80 ms, Avg Space: 500 elements
Reversed:
Input Array: [1000, 999, 998, 997, 996, 995, 994, 993, 992, 991, ...]
Standard Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
In-Place Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
Standard Merge Sort - Avg Time: 2.60 ms, Avg Space: 1000 elements
In-Place Merge Sort - Avg Time: 4.00 ms, Avg Space: 500 elements
Duplicates:
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Standard Sorted Array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
In-Place Sorted Array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
Standard Merge Sort - Avg Time: 2.60 ms, Avg Space: 1000 elements
In-Place Merge Sort - Avg Time: 4.00 ms, Avg Space: 500 elements
Explanation:
- Size 10: Standard Merge Sort is faster (O(n log n)) and uses O(n) space; in-place variant is slower due to shifts but reduces space to ~n/2 elements.
- Size 100: In-place variant shows noticeable time overhead; space savings are proportional to array size.
- Size 1000: Standard Merge Sort outperforms significantly; in-place variant halves space but increases time due to O(n²) merge operations.
- Both algorithms correctly sort all inputs, including duplicates, but in-place variant sacrifices efficiency.
How It Works
- standardMergeSort:
- Recursively divides the array into halves until single elements.
- Merges using temporary arrays (O(n) space per level), ensuring stability with
<=.
- inPlaceMergeSort:
- Divides recursively like standard Merge Sort.
- In
inPlaceMerge, shifts elements within the array to insert right subarray elements into the left subarray, avoiding large temporary arrays but increasing time complexity to O(n²) per merge due to shifts. - Space is reduced to O(1) per merge, but recursion stack uses O(log n) space.
- generateUnsorted/Sorted/Reversed/Duplicates: Creates test arrays for various cases.
- toString: Formats array, limiting output to 10 elements.
- Example Trace (Unsorted, n=5, [64, 34, 25, 12, 22]):
- Standard:
- Divide: [64, 34, 25] and [12, 22].
- Merge: [64, 34, 25] → [25, 34, 64]; [12, 22] → [12, 22].
- Merge: [25, 34, 64], [12, 22] → [12, 22, 25, 34, 64] (uses temp arrays).
- In-Place:
- Divide similarly.
- Merge: [64, 34, 25, 12, 22], for i=0, j=3, key=12 < 64, shift [64, 34, 25] right, insert 12: [12, 64, 34, 25, 22].
- Continue merging, shifting elements as needed.
- Standard:
- Main Method: Tests both algorithms across sizes and cases, averaging times and estimating space (standard: O(n), in-place: ~O(n/2) for largest merge).
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| standardMergeSort | O(n log n) | O(n) |
| inPlaceMergeSort | O(n² log n) worst | O(log n) recursion |
| standardMerge | O(n) | O(n) |
| inPlaceMerge | O(n²) worst | O(1) |
| generateUnsorted | O(n) | O(n) |
| generateSorted | O(n) | O(n) |
| generateReversed | O(n) | O(n) |
| generateDuplicates | O(n) | O(n) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n log n) for standardMergeSort; O(n² log n) for inPlaceMergeSort due to O(n²) merges; O(n) for array generation and toString.
- Space complexity: O(n) for standardMergeSort (temporary arrays); O(log n) for inPlaceMergeSort (recursion stack, O(1) per merge); O(n) for array generation and toString.
- In-place variant reduces auxiliary space but increases time due to shifting.
✅ Tip: In-place Merge Sort variants reduce auxiliary space but often increase time complexity. Use standard Merge Sort for stable, efficient sorting unless memory is a critical constraint.
⚠ Warning: The in-place variant sacrifices stability and efficiency (O(n² log n) time) due to in-array shifts. Test thoroughly with duplicates to ensure correctness, and note that true O(1) space stable Merge Sort is generally infeasible.
Searching Algorithms
🛠 What you will learn
This section outlines the format used for each DSA problem statement, designed to guide students through solving exercise problems in a clear, structured learning process. Each component serves a specific purpose to enhance understanding and practical application:
- Problem Statement: Clearly defines the task, including inputs, outputs, constraints, and an example. A real-world analogy makes the problem relatable, aligning with the engaging tone of the "Core Data Structures" summary, ensuring students grasp the problem’s context.
- Pseudocode: Provides a high-level, language-agnostic outline of the solution using standardized syntax (e.g.,
FUNCTION,IF,SET,RETURN). This helps students understand the logic before diving into code, bridging theory and implementation. - Algorithm Steps: Breaks down the pseudocode into detailed, actionable steps, connecting theoretical logic to practical coding. This ensures students can follow the implementation process systematically.
- Java Implementation: Offers a complete, commented Java code solution that students can study and run. It includes a
mainmethod with at least four test cases (e.g., target present, absent, middle element, and edge cases like duplicates or single elements) to verify correctness and explore different scenarios, maintaining a beginner-friendly approach. - Output: Shows the expected results from the test cases, including indices, comparisons, and execution times (where applicable), helping students verify their code’s correctness and understand the algorithm’s behavior.
- How It Works: Traces the algorithm’s execution with a detailed example, showing step-by-step how the search narrows down the range. This reinforces understanding by illustrating the algorithm in action.
- Complexity Analysis Table: Summarizes time and space complexity for best, average, and worst cases, teaching students to evaluate efficiency and compare trade-offs across implementations.
- Tip or Warning Box: Provides practical advice (e.g., when to use DSA) and highlights pitfalls (e.g., ensuring the array is sorted), guiding students toward best practices and common error avoidance.
Each exercise includes test cases to verify correctness and performance, aligning with a learning platform featuring embedded compilers for hands-on practice and visualizations to illustrate the halving process of DSA.
Basic Linear Search
Problem Statement
Write a Java program that implements the Linear Search algorithm to find a target integer in an array of integers. The program should test the implementation with arrays of different sizes (e.g., 10, 100, 1000) and various target values, including cases where the target is present, absent, or the first element, and count the number of comparisons made during each search. Linear Search sequentially checks each element in the array until the target is found or the array is fully traversed. You can visualize this as searching through a list of numbers one by one until you find the desired value or reach the end.
Input:
- An array of integers and a target integer to find. Output: The index of the target (or -1 if not found), the number of comparisons made, and a string representation of the input array for verification. Constraints:
- The array length
nis between 0 and 10^5. - Array elements and target are integers in the range [-10^9, 10^9]. Example:
- Input: array = [64, 34, 25, 12, 22], target = 25
- Output:
- Input Array: [64, 34, 25, 12, 22]
- Target: 25
- Index: 2
- Comparisons: 3
- Explanation: Linear Search checks elements at indices 0, 1, and 2, finding 25 after 3 comparisons.
- Input: array = [1, 2, 3], target = 4
- Output:
- Input Array: [1, 2, 3]
- Target: 4
- Index: -1
- Comparisons: 3
- Explanation: Linear Search checks all elements and returns -1 as 4 is not found.
Pseudocode
FUNCTION linearSearch(arr, target)
SET comparisons to 0
FOR i from 0 to length of arr - 1
INCREMENT comparisons
IF arr[i] equals target THEN
RETURN i, comparisons
ENDIF
ENDFOR
RETURN -1, comparisons
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [10, 100, 1000]
FOR each size in sizes
SET testCases to array of (array, target) pairs
FOR each testCase in testCases
PRINT test case details
SET arr to testCase array
SET target to testCase target
CALL linearSearch(arr, target) to get index, comparisons
PRINT input array, target, index, comparisons
ENDFOR
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
linearSearch: a. Initialize a comparisons counter to 0. b. Iterate through the array from index 0 to n-1. c. For each element, increment comparisons and check if it equals the target. d. If found, return the index and comparisons; otherwise, return -1 and comparisons. - Define
toString: a. Convert array to a string, e.g., "[64, 34, 25, 12, 22]". - In
main, test with: a. Array sizes: 10, 100, 1000. b. For each size, test:- Target present in the middle (average case).
- Target absent (worst case).
- Target as the first element (best case).
- Target as a duplicate (if applicable). c. Generate random arrays with a fixed seed for reproducibility.
Java Implementation
import java.util.*;
public class BasicLinearSearch {
// Performs Linear Search and counts comparisons
public int[] linearSearch(int[] arr, int target) {
int comparisons = 0;
for (int i = 0; i < arr.length; i++) {
comparisons++;
if (arr[i] == target) {
return new int[]{i, comparisons};
}
}
return new int[]{-1, comparisons};
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Generates random array
private int[] generateRandomArray(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(2001) - 1000; // [-1000, 1000]
}
return arr;
}
// Helper class for test cases
static class TestCase {
int[] arr;
int target;
String description;
TestCase(int[] arr, int target, String description) {
this.arr = arr;
this.target = target;
this.description = description;
}
}
// Main method to test Linear Search
public static void main(String[] args) {
BasicLinearSearch searcher = new BasicLinearSearch();
int[] sizes = {10, 100, 1000};
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
int[] baseArray = searcher.generateRandomArray(size);
TestCase[] testCases = {
new TestCase(baseArray, baseArray[size / 2], "Target present (middle)"),
new TestCase(baseArray, 1000000, "Target absent"),
new TestCase(baseArray, baseArray[0], "Target first element"),
new TestCase(baseArray, baseArray[size / 4], "Target duplicate (early)")
};
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
int[] arr = testCases[i].arr.clone(); // Copy to preserve original
int target = testCases[i].target;
System.out.println("Input Array: " + searcher.toString(arr));
System.out.println("Target: " + target);
int[] result = searcher.linearSearch(arr, target);
System.out.println("Index: " + result[0]);
System.out.println("Comparisons: " + result[1] + "\n");
}
}
}
}
Output
Running the main method produces (example output, random values fixed by seed):
Array Size: 10
Test case 1: Target present (middle)
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628]
Target: 360
Index: 5
Comparisons: 6
Test case 2: Target absent
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628]
Target: 1000000
Index: -1
Comparisons: 10
Test case 3: Target first element
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628]
Target: 727
Index: 0
Comparisons: 1
Test case 4: Target duplicate (early)
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628]
Target: 374
Index: 3
Comparisons: 4
Array Size: 100
Test case 1: Target present (middle)
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628, ...]
Target: 672
Index: 50
Comparisons: 51
Test case 2: Target absent
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628, ...]
Target: 1000000
Index: -1
Comparisons: 100
Test case 3: Target first element
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628, ...]
Target: 727
Index: 0
Comparisons: 1
Test case 4: Target duplicate (early)
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628, ...]
Target: 566
Index: 25
Comparisons: 26
Array Size: 1000
Test case 1: Target present (middle)
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628, ...]
Target: -626
Index: 500
Comparisons: 501
Test case 2: Target absent
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628, ...]
Target: 1000000
Index: -1
Comparisons: 1000
Test case 3: Target first element
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628, ...]
Target: 727
Index: 0
Comparisons: 1
Test case 4: Target duplicate (early)
Input Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628, ...]
Target: -135
Index: 250
Comparisons: 251
Explanation:
- Size 10: Finds middle target in 6 comparisons, absent in 10 (full scan), first in 1, duplicate in 4.
- Size 100: Middle target takes ~51 comparisons, absent 100, first 1, duplicate ~26.
- Size 1000: Middle target takes ~501 comparisons, absent 1000, first 1, duplicate ~251.
- Linear Search correctly returns indices and counts comparisons for all cases.
How It Works
- linearSearch:
- Iterates through the array, incrementing
comparisonsfor each element check. - Returns [index, comparisons] when the target is found or [-1, comparisons] if not found.
- Iterates through the array, incrementing
- toString: Formats array as a string, limiting output to 10 elements for large arrays.
- generateRandomArray: Creates an array with random integers in [-1000, 1000] using a fixed seed.
- Example Trace (Size 10, Target present, target=360):
- Array: [727, -333, 648, 374, -767, 360, -766, 304, 289, -628].
- Check index 0: 727 ≠ 360, comparisons=1.
- Check index 1: -333 ≠ 360, comparisons=2.
- Check index 2: 648 ≠ 360, comparisons=3.
- Check index 3: 374 ≠ 360, comparisons=4.
- Check index 4: -767 ≠ 360, comparisons=5.
- Check index 5: 360 = 360, comparisons=6, return [5, 6].
- Main Method: Tests arrays of sizes 10, 100, 1000 with targets in the middle, absent, first element, and early duplicate, displaying results and comparisons.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| linearSearch | O(n) worst, O(1) best | O(1) |
| toString | O(n) | O(n) |
| generateRandomArray | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n) for linearSearch in worst/average cases (full scan or target near end); O(1) in best case (target at index 0); O(n) for toString and generateRandomArray.
- Space complexity: O(1) for linearSearch (constant extra space); O(n) for toString (string builder) and generateRandomArray (array storage).
- Linear Search is simple but inefficient for large arrays compared to binary search.
✅ Tip: Linear Search is easy to implement and works on unsorted arrays, making it suitable for small datasets or when the target is likely near the start. Always count comparisons to understand performance.
⚠ Warning: Linear Search has O(n) time complexity in the worst case, making it inefficient for large arrays. Use binary search for sorted arrays to achieve O(log n) performance.
Linear Search for Last Occurrence
Problem Statement
Write a Java program that modifies the Linear Search algorithm to find the last occurrence of a target integer in an array of integers that may contain duplicates. The program should count the number of comparisons made during the search and test with the array [1, 3, 3, 5, 8] and target 3, as well as arrays of different sizes (e.g., 10, 100, 1000) with various target values (present with duplicates, absent, last element). Linear Search will sequentially check each element, updating the result whenever the target is found to ensure the last occurrence is returned. You can visualize this as scanning a list of numbers from left to right, keeping track of the most recent position where the target appears until the end is reached.
Input:
- An array of integers and a target integer to find. Output: The index of the last occurrence of the target (or -1 if not found), the number of comparisons made, and a string representation of the input array for verification. Constraints:
- The array length
nis between 0 and 10^5. - Array elements and target are integers in the range [-10^9, 10^9]. Example:
- Input: array = [1, 3, 3, 5, 8], target = 3
- Output:
- Input Array: [1, 3, 3, 5, 8]
- Target: 3
- Index: 2
- Comparisons: 5
- Explanation: Linear Search checks all elements, finding
3at indices 1 and 2, returning index 2 as the last occurrence after 5 comparisons. - Input: array = [1, 2, 3], target = 4
- Output:
- Input Array: [1, 2, 3]
- Target: 4
- Index: -1
- Comparisons: 3
- Explanation: Linear Search checks all elements, returns -1 as
4is not found after 3 comparisons.
Pseudocode
FUNCTION linearSearchLast(arr, target)
SET comparisons to 0
SET lastIndex to -1
FOR i from 0 to length of arr - 1
INCREMENT comparisons
IF arr[i] equals target THEN
SET lastIndex to i
ENDIF
ENDFOR
RETURN lastIndex, comparisons
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [5, 10, 100, 1000]
SET testCases to array of (array, target) pairs including [1, 3, 3, 5, 8] with target 3
FOR each testCase in testCases
PRINT test case details
SET arr to testCase array
SET target to testCase target
CALL linearSearchLast(arr, target) to get lastIndex, comparisons
PRINT input array, target, lastIndex, comparisons
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
linearSearchLast: a. Initialize a comparisons counter to 0 andlastIndexto -1. b. Iterate through the array from index 0 to n-1. c. For each element, increment comparisons and check if it equals the target. d. If equal, updatelastIndexto the current index. e. ReturnlastIndexand comparisons. - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Specific case: array[1, 3, 3, 5, 8], target3. b. Array sizes: 10, 100, 1000. c. For each size, test:- Target present with duplicates (middle of duplicates).
- Target absent (worst case).
- Target as the last element. d. Generate random arrays with duplicates using a fixed seed.
Java Implementation
import java.util.*;
public class LinearSearchLastOccurrence {
// Performs Linear Search for last occurrence and counts comparisons
public int[] linearSearchLast(int[] arr, int target) {
int comparisons = 0;
int lastIndex = -1;
for (int i = 0; i < arr.length; i++) {
comparisons++;
if (arr[i] == target) {
lastIndex = i;
}
}
return new int[]{lastIndex, comparisons};
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Generates random array with duplicates
private int[] generateRandomArray(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(11); // [0, 10] to ensure duplicates
}
return arr;
}
// Helper class for test cases
static class TestCase {
int[] arr;
int target;
String description;
TestCase(int[] arr, int target, String description) {
this.arr = arr;
this.target = target;
this.description = description;
}
}
// Main method to test Linear Search for last occurrence
public static void main(String[] args) {
LinearSearchLastOccurrence searcher = new LinearSearchLastOccurrence();
int[] sizes = {5, 10, 100, 1000};
// Initialize test cases
TestCase[] testCases = new TestCase[13];
// Specific test case
testCases[0] = new TestCase(new int[]{1, 3, 3, 5, 8}, 3, "Specific case [1, 3, 3, 5, 8], target 3");
// Generate test cases for other sizes
int testIndex = 1;
for (int size : sizes) {
if (size == 5) continue; // Skip size 5 as it's covered by specific case
int[] arr = searcher.generateRandomArray(size);
testCases[testIndex++] = new TestCase(arr, arr[size / 2], "Target present with duplicates (middle)");
testCases[testIndex++] = new TestCase(arr, 1000000, "Target absent");
testCases[testIndex++] = new TestCase(arr, arr[size - 1], "Target last element");
}
// Run test cases
for (int i = 0; i < testCases.length; i++) {
if (testCases[i] == null) break; // Avoid null cases
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
int[] arr = testCases[i].arr.clone(); // Copy to preserve original
int target = testCases[i].target;
System.out.println("Input Array: " + searcher.toString(arr));
System.out.println("Target: " + target);
int[] result = searcher.linearSearchLast(arr, target);
System.out.println("Last Index: " + result[0]);
System.out.println("Comparisons: " + result[1] + "\n");
}
}
}
Output
Running the main method produces (example output, random values fixed by seed):
Test case 1: Specific case [1, 3, 3, 5, 8], target 3
Input Array: [1, 3, 3, 5, 8]
Target: 3
Last Index: 2
Comparisons: 5
Test case 2: Target present with duplicates (middle)
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7]
Target: 8
Last Index: 4
Comparisons: 10
Test case 3: Target absent
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7]
Target: 1000000
Last Index: -1
Comparisons: 10
Test case 4: Target last element
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7]
Target: 7
Last Index: 9
Comparisons: 10
Test case 5: Target present with duplicates (middle)
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Target: 4
Last Index: 75
Comparisons: 100
Test case 6: Target absent
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Target: 1000000
Last Index: -1
Comparisons: 100
Test case 7: Target last element
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Target: 2
Last Index: 99
Comparisons: 100
Test case 8: Target present with duplicates (middle)
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Target: 0
Last Index: 500
Comparisons: 1000
Test case 9: Target absent
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Target: 1000000
Last Index: -1
Comparisons: 1000
Test case 10: Target last element
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Target: 6
Last Index: 999
Comparisons: 1000
Explanation:
- Specific case: Finds last
3at index 2 in[1, 3, 3, 5, 8]after 5 comparisons. - Size 10: Finds duplicate target in middle (~index 4, 10 comparisons), absent target (10 comparisons), last element (10 comparisons).
- Size 100: Finds duplicate target (~index 75, 100 comparisons), absent target (100 comparisons), last element (100 comparisons).
- Size 1000: Finds duplicate target (~index 500, 1000 comparisons), absent target (1000 comparisons), last element (1000 comparisons).
- Always scans entire array to ensure last occurrence is found.
How It Works
- linearSearchLast:
- Initializes
comparisonsto 0 andlastIndexto -1. - Iterates through the array, incrementing
comparisonsfor each element. - Updates
lastIndexwhenever the target is found. - Returns
[lastIndex, comparisons].
- Initializes
- toString: Formats array as a string, limiting output to 10 elements.
- generateRandomArray: Creates an array with values in [0, 10] to ensure duplicates.
- Example Trace (Specific case, [1, 3, 3, 5, 8], target=3):
- Check index 0: 1 ≠ 3, comparisons=1, lastIndex=-1.
- Check index 1: 3 = 3, comparisons=2, lastIndex=1.
- Check index 2: 3 = 3, comparisons=3, lastIndex=2.
- Check index 3: 5 ≠ 3, comparisons=4, lastIndex=2.
- Check index 4: 8 ≠ 3, comparisons=5, lastIndex=2.
- Return [2, 5].
- Main Method: Tests specific case
[1, 3, 3, 5, 8]with target3, and sizes 10, 100, 1000 with targets in the middle (duplicates), absent, and last element.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| linearSearchLast | O(n) | O(1) |
| toString | O(n) | O(n) |
| generateRandomArray | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n) for linearSearchLast (always scans entire array to find last occurrence); O(n) for toString and generateRandomArray.
- Space complexity: O(1) for linearSearchLast (constant extra space); O(n) for toString (string builder) and generateRandomArray (array storage).
- Always performs n comparisons to ensure the last occurrence is found.
✅ Tip: Linear Search for the last occurrence ensures all duplicates are considered by scanning the entire array. Use a small range of values to test duplicates effectively.
⚠ Warning: Linear Search for the last occurrence always requires O(n) comparisons, even if the target is found early, as it must check for later occurrences. For sorted arrays, consider binary search modifications for efficiency.
Linear Search for Multiple Targets
Problem Statement
Write a Java program that modifies the Linear Search algorithm to return all indices where a target integer appears in an array of integers that may contain duplicates. The program should count the number of comparisons made during the search and test with the array [1, 3, 3, 5, 3] and target 3, as well as arrays of different sizes (e.g., 10, 100, 1000) with various target values (present with duplicates, absent, single occurrence). Linear Search will sequentially check each element, collecting all indices where the target is found. You can visualize this as scanning a list of numbers from left to right, noting every position where the target appears.
Input:
- An array of integers and a target integer to find. Output: A list of all indices where the target appears (empty list if not found), the number of comparisons made, and a string representation of the input array for verification. Constraints:
- The array length
nis between 0 and 10^5. - Array elements and target are integers in the range [-10^9, 10^9]. Example:
- Input: array = [1, 3, 3, 5, 3], target = 3
- Output:
- Input Array: [1, 3, 3, 5, 3]
- Target: 3
- Indices: [1, 2, 4]
- Comparisons: 5
- Explanation: Linear Search checks all elements, finding
3at indices 1, 2, and 4 after 5 comparisons. - Input: array = [1, 2, 3], target = 4
- Output:
- Input Array: [1, 2, 3]
- Target: 4
- Indices: []
- Comparisons: 3
- Explanation: Linear Search checks all elements, returns an empty list as
4is not found after 3 comparisons.
Pseudocode
FUNCTION linearSearchMultiple(arr, target)
SET comparisons to 0
CREATE indices as empty list
FOR i from 0 to length of arr - 1
INCREMENT comparisons
IF arr[i] equals target THEN
APPEND i to indices
ENDIF
ENDFOR
RETURN indices, comparisons
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [5, 10, 100, 1000]
SET testCases to array of (array, target) pairs including [1, 3, 3, 5, 3] with target 3
FOR each testCase in testCases
PRINT test case details
SET arr to testCase array
SET target to testCase target
CALL linearSearchMultiple(arr, target) to get indices, comparisons
PRINT input array, target, indices, comparisons
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
linearSearchMultiple: a. Initialize a comparisons counter to 0 and an empty list for indices. b. Iterate through the array from index 0 to n-1. c. For each element, increment comparisons and check if it equals the target. d. If equal, append the index to the indices list. e. Return the indices list and comparisons. - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Specific case: array[1, 3, 3, 5, 3], target3. b. Array sizes: 10, 100, 1000. c. For each size, test:- Target present with duplicates.
- Target absent.
- Target with single occurrence. d. Generate random arrays with duplicates using a fixed seed.
Java Implementation
import java.util.*;
public class LinearSearchMultipleTargets {
// Performs Linear Search for all occurrences and counts comparisons
public Object[] linearSearchMultiple(int[] arr, int target) {
int comparisons = 0;
List<Integer> indices = new ArrayList<>();
for (int i = 0; i < arr.length; i++) {
comparisons++;
if (arr[i] == target) {
indices.add(i);
}
}
return new Object[]{indices, comparisons};
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Generates random array with duplicates
private int[] generateRandomArray(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(11); // [0, 10] to ensure duplicates
}
return arr;
}
// Helper class for test cases
static class TestCase {
int[] arr;
int target;
String description;
TestCase(int[] arr, int target, String description) {
this.arr = arr;
this.target = target;
this.description = description;
}
}
// Main method to test multiple targets
public static void main(String[] args) {
LinearSearchMultipleTargets searcher = new LinearSearchMultipleTargets();
int[] sizes = {5, 10, 100, 1000};
// Initialize test cases
TestCase[] testCases = new TestCase[10];
// Specific test case
testCases[0] = new TestCase(new int[]{1, 3, 3, 5, 3}, 3, "Specific case [1, 3, 3, 5, 3], target 3");
// Generate test cases for other sizes
int testIndex = 1;
for (int size : sizes) {
if (size == 5) continue; // Skip size 5 as it's covered by specific case
int[] arr = searcher.generateRandomArray(size);
testCases[testIndex++] = new TestCase(arr, arr[size / 2], "Target present with duplicates");
testCases[testIndex++] = new TestCase(arr, 1000000, "Target absent");
testCases[testIndex++] = new TestCase(new int[]{size}, size, "Single occurrence (size=" + size + ")");
}
// Run test cases
for (int i = 0; i < testCases.length; i++) {
if (testCases[i] == null) break; // Avoid null cases
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
int[] arr = testCases[i].arr.clone(); // Copy to preserve original
int target = testCases[i].target;
System.out.println("Input Array: " + searcher.toString(arr));
System.out.println("Target: " + target);
Object[] result = searcher.linearSearchMultiple(arr, target);
List<Integer> indices = (List<Integer>) result[0];
int comparisons = (int) result[1];
System.out.println("Indices: " + indices);
System.out.println("Comparisons: " + comparisons + "\n");
}
}
}
Output
Running the main method produces (example output, random values fixed by seed):
Test case 1: Specific case [1, 3, 3, 5, 3], target 3
Input Array: [1, 3, 3, 5, 3]
Target: 3
Indices: [1, 2, 4]
Comparisons: 5
Test case 2: Target present with duplicates
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7]
Target: 8
Indices: [4]
Comparisons: 10
Test case 3: Target absent
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7]
Target: 1000000
Indices: []
Comparisons: 10
Test case 4: Single occurrence (size=10)
Input Array: [10]
Target: 10
Indices: [0]
Comparisons: 1
Test case 5: Target present with duplicates
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Target: 4
Indices: [1, 7, 15, 22, 30, 36, 44, 50, 58, 66, ...]
Comparisons: 100
Test case 6: Target absent
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Target: 1000000
Indices: []
Comparisons: 100
Test case 7: Single occurrence (size=100)
Input Array: [100]
Target: 100
Indices: [0]
Comparisons: 1
Test case 8: Target present with duplicates
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Target: 0
Indices: [12, 24, 37, 49, 62, 74, 87, 99, 112, 125, ...]
Comparisons: 1000
Test case 9: Target absent
Input Array: [6, 4, 6, 9, 8, 7, 6, 4, 3, 7, ...]
Target: 1000000
Indices: []
Comparisons: 1000
Test case 10: Single occurrence (size=1000)
Input Array: [1000]
Target: 1000
Indices: [0]
Comparisons: 1
Explanation:
- Specific case: Finds
3at indices [1, 2, 4] in[1, 3, 3, 5, 3]after 5 comparisons. - Size 10: Finds duplicate target (single occurrence in this case) at index [4] (10 comparisons), absent target (10 comparisons), single occurrence (1 comparison).
- Size 100: Finds duplicate target at multiple indices (~10 indices, 100 comparisons), absent target (100 comparisons), single occurrence (1 comparison).
- Size 1000: Finds duplicate target at multiple indices (~100 indices, 1000 comparisons), absent target (1000 comparisons), single occurrence (1 comparison).
- Always scans entire array to find all occurrences.
How It Works
- linearSearchMultiple:
- Initializes
comparisonsto 0 and an emptyArrayListfor indices. - Iterates through the array, incrementing
comparisonsfor each element. - Appends index to
indiceswhen target is found. - Returns
[indices, comparisons].
- Initializes
- toString: Formats array, limiting output to 10 elements.
- generateRandomArray: Creates an array with values in [0, 10] to ensure duplicates.
- Example Trace (Specific case, [1, 3, 3, 5, 3], target=3):
- Check index 0: 1 ≠ 3, comparisons=1, indices=[].
- Check index 1: 3 = 3, comparisons=2, indices=[1].
- Check index 2: 3 = 3, comparisons=3, indices=[1, 2].
- Check index 3: 5 ≠ 3, comparisons=4, indices=[1, 2].
- Check index 4: 3 = 3, comparisons=5, indices=[1, 2, 4].
- Return [[1, 2, 4], 5].
- Main Method: Tests specific case
[1, 3, 3, 5, 3]with target3, and sizes 10, 100, 1000 with duplicates, absent, and single-occurrence targets.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| linearSearchMultiple | O(n) | O(n) worst |
| toString | O(n) | O(n) |
| generateRandomArray | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n) for linearSearchMultiple (always scans entire array); O(n) for toString and generateRandomArray.
- Space complexity: O(n) for linearSearchMultiple in worst case (all elements match target); O(n) for toString (string builder) and generateRandomArray (array storage).
- Always performs n comparisons to find all occurrences.
✅ Tip: Linear Search for multiple targets is useful for unsorted arrays with duplicates. Use an
ArrayListto dynamically store indices, and test with small value ranges to ensure duplicates.
⚠ Warning: Linear Search always requires O(n) comparisons to find all occurrences, even if matches are found early. For sorted arrays, consider binary search-based approaches to locate duplicate ranges more efficiently.
Linear Search for Object Search
Problem Statement
Write a Java program that extends the Linear Search algorithm to find a Student object in an array of Student objects based on a given id field. The program should count the number of comparisons made during the search and test with a sample dataset containing Student objects with varied IDs, including cases where the target ID is present, absent, or duplicated. Linear Search will sequentially check each object’s id until a match is found or the array is fully traversed. You can visualize this as searching through a list of student records one by one to find a specific student by their ID number.
Input:
- An array of
Studentobjects, each with aname(String) andid(integer) field, and a targetidto find. Output: The index of the firstStudentwith the matchingid(or -1 if not found), the number of comparisons made, and a string representation of the input array for verification. Constraints: - The array length
nis between 0 and 10^5. - IDs are integers in the range [1, 10^6]. Example:
- Input: array = [Student("Alice", 101), Student("Bob", 102), Student("Charlie", 101)], target = 101
- Output:
- Input Array: [Student(name=Alice, id=101), Student(name=Bob, id=102), Student(name=Charlie, id=101)]
- Target ID: 101
- Index: 0
- Comparisons: 1
- Explanation: Linear Search finds the first
Studentwithid=101at index 0 after 1 comparison. - Input: array = [Student("Alice", 101), Student("Bob", 102)], target = 103
- Output:
- Input Array: [Student(name=Alice, id=101), Student(name=Bob, id=102)]
- Target ID: 103
- Index: -1
- Comparisons: 2
- Explanation: Linear Search checks all elements, returns -1 as
id=103is not found.
Pseudocode
FUNCTION linearSearchObject(arr, targetId)
SET comparisons to 0
FOR i from 0 to length of arr - 1
INCREMENT comparisons
IF arr[i].id equals targetId THEN
RETURN i, comparisons
ENDIF
ENDFOR
RETURN -1, comparisons
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element.toString() to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET testCases to array of (Student array, targetId) pairs
FOR each testCase in testCases
PRINT test case details
SET arr to testCase Student array
SET targetId to testCase target
CALL linearSearchObject(arr, targetId) to get index, comparisons
PRINT input array, targetId, index, comparisons
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
Studentclass: a. Fields:name(String),id(integer). b. IncludetoStringfor readable output. - Define
linearSearchObject: a. Initialize a comparisons counter to 0. b. Iterate through the array, incrementing comparisons for each object. c. Check if the object’sidmatchestargetId. d. If found, return index and comparisons; else return -1 and comparisons. - Define
toString: a. Convert array to a string, e.g., "[Student(name=Alice, id=101), ...]". - In
main, test with: a. Mixed IDs (n=5, with duplicates). b. Empty array (n=0). c. Single-element array (n=1). d. Absent ID (n=6). e. Duplicate IDs (n=4, all same ID).
Java Implementation
import java.util.*;
public class LinearSearchObjectSearch {
// Student class
static class Student {
String name;
int id;
Student(String name, int id) {
this.name = name;
this.id = id;
}
@Override
public String toString() {
return "Student(name=" + name + ", id=" + id + ")";
}
}
// Performs Linear Search for Student by id
public int[] linearSearchObject(Student[] arr, int targetId) {
int comparisons = 0;
for (int i = 0; i < arr.length; i++) {
comparisons++;
if (arr[i].id == targetId) {
return new int[]{i, comparisons};
}
}
return new int[]{-1, comparisons};
}
// Converts array to string
public String toString(Student[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i].toString());
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
Student[] arr;
int targetId;
String description;
TestCase(Student[] arr, int targetId, String description) {
this.arr = arr;
this.targetId = targetId;
this.description = description;
}
}
// Main method to test object search
public static void main(String[] args) {
LinearSearchObjectSearch searcher = new LinearSearchObjectSearch();
// Test cases
TestCase[] testCases = {
new TestCase(new Student[]{
new Student("Alice", 101),
new Student("Bob", 102),
new Student("Charlie", 101),
new Student("David", 103),
new Student("Eve", 104)
}, 101, "Mixed IDs with duplicates (n=5)"),
new TestCase(new Student[]{}, 101, "Empty array (n=0)"),
new TestCase(new Student[]{
new Student("Frank", 105)
}, 105, "Single element (n=1)"),
new TestCase(new Student[]{
new Student("Grace", 106),
new Student("Hannah", 107),
new Student("Ivy", 108),
new Student("Jack", 109),
new Student("Kate", 110),
new Student("Liam", 111)
}, 112, "Absent ID (n=6)"),
new TestCase(new Student[]{
new Student("Mia", 101),
new Student("Noah", 101),
new Student("Olivia", 101),
new Student("Peter", 101)
}, 101, "Duplicate IDs (n=4, all same)")
};
// Run test cases
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
Student[] arr = testCases[i].arr.clone(); // Copy to preserve original
int targetId = testCases[i].targetId;
System.out.println("Input Array: " + searcher.toString(arr));
System.out.println("Target ID: " + targetId);
int[] result = searcher.linearSearchObject(arr, targetId);
System.out.println("Index: " + result[0]);
System.out.println("Comparisons: " + result[1] + "\n");
}
}
}
Output
Running the main method produces:
Test case 1: Mixed IDs with duplicates (n=5)
Input Array: [Student(name=Alice, id=101), Student(name=Bob, id=102), Student(name=Charlie, id=101), Student(name=David, id=103), Student(name=Eve, id=104)]
Target ID: 101
Index: 0
Comparisons: 1
Test case 2: Empty array (n=0)
Input Array: []
Target ID: 101
Index: -1
Comparisons: 0
Test case 3: Single element (n=1)
Input Array: [Student(name=Frank, id=105)]
Target ID: 105
Index: 0
Comparisons: 1
Test case 4: Absent ID (n=6)
Input Array: [Student(name=Grace, id=106), Student(name=Hannah, id=107), Student(name=Ivy, id=108), Student(name=Jack, id=109), Student(name=Kate, id=110), Student(name=Liam, id=111)]
Target ID: 112
Index: -1
Comparisons: 6
Test case 5: Duplicate IDs (n=4, all same)
Input Array: [Student(name=Mia, id=101), Student(name=Noah, id=101), Student(name=Olivia, id=101), Student(name=Peter, id=101)]
Target ID: 101
Index: 0
Comparisons: 1
Explanation:
- Test case 1: Finds first
Studentwithid=101at index 0 after 1 comparison; duplicates exist later. - Test case 2: Empty array returns -1 with 0 comparisons.
- Test case 3: Single element with matching
id=105found at index 0 after 1 comparison. - Test case 4: Scans all 6 elements, returns -1 for absent
id=112after 6 comparisons. - Test case 5: Finds first
Studentwithid=101at index 0 after 1 comparison, despite all havingid=101.
How It Works
- Student Class:
- Contains
name(String) andid(integer). - Includes
toStringfor readable output.
- Contains
- linearSearchObject:
- Iterates through the array, incrementing
comparisonsfor each object. - Checks if the
idmatchestargetId. - Returns
[index, comparisons]for the first match or[-1, comparisons]if none.
- Iterates through the array, incrementing
- toString: Formats array as a string, limiting to 10 elements.
- Example Trace (Test case 1, [Alice(101), Bob(102), Charlie(101), David(103), Eve(104)], target=101):
- Check index 0: Alice.id=101 = 101, comparisons=1, return [0, 1].
- Main Method: Tests mixed IDs with duplicates, empty array, single element, absent ID, and all duplicate IDs, displaying results and comparisons.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| linearSearchObject | O(n) worst, O(1) best | O(1) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n) for linearSearchObject in worst/average cases (full or partial scan); O(1) in best case (target at index 0); O(n) for toString.
- Space complexity: O(1) for linearSearchObject (constant extra space); O(n) for toString (string builder).
- Linear Search stops at the first matching
id, making it efficient for early matches.
✅ Tip: Linear Search for objects is simple and works on unsorted arrays, ideal for small datasets or when the target is near the start. Use meaningful
toStringmethods for clear output.
⚠ Warning: Linear Search has O(n) time complexity in the worst case, inefficient for large arrays. For sorted arrays or frequent searches, consider binary search or hash-based structures for better performance.
Linear Search Performance Analysis
Problem Statement
Write a Java program that measures the execution time of the Linear Search algorithm for finding a target integer in arrays of increasing sizes (e.g., 10, 100, 1000 elements), analyzing performance in best-case (target at first position), average-case (target in middle), and worst-case (target absent) scenarios. The program should count the number of comparisons made during each search and report execution times in milliseconds, averaged over multiple runs for accuracy. Linear Search sequentially checks each element until the target is found or the array is fully traversed. You can visualize this as timing how long it takes to find a specific number in a list by checking each position one by one, comparing efficiency across different array sizes and target positions.
Input:
- Arrays of integers with sizes 10, 100, and 1000, and target values for best (first element), average (middle element), and worst (absent) cases. Output: The index of the target (or -1 if not found), number of comparisons, execution time (in milliseconds) for each case and size, and a string representation of the input array for verification. Constraints:
- Array sizes are 10, 100, 1000.
- Array elements and targets are integers in the range [-10^9, 10^9].
- Execution times are averaged over multiple runs for accuracy. Example:
- Input: Array size = 10, array = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10], targets = [1 (best), 2 (average), 999 (worst)]
- Output (example, times vary):
- Best Case (target=1):
- Index: 0, Comparisons: 1, Time: 0.01 ms
- Average Case (target=2):
- Index: 5, Comparisons: 6, Time: 0.02 ms
- Worst Case (target=999):
- Index: -1, Comparisons: 10, Time: 0.03 ms
- Best Case (target=1):
- Explanation: Linear Search is fastest when the target is first (best), slower in the middle (average), and slowest when absent (worst), requiring a full scan.
Pseudocode
FUNCTION linearSearch(arr, target)
SET comparisons to 0
FOR i from 0 to length of arr - 1
INCREMENT comparisons
IF arr[i] equals target THEN
RETURN i, comparisons
ENDIF
ENDFOR
RETURN -1, comparisons
ENDFUNCTION
FUNCTION generateArray(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to random integer in [-10^9, 10^9]
ENDFOR
RETURN arr
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [10, 100, 1000]
SET runs to 10
FOR each size in sizes
SET arr to generateArray(size)
SET testCases to array of targets for best, average, worst cases
FOR each target in testCases
SET totalTime to 0
SET totalComparisons to 0
FOR i from 0 to runs-1
SET copy to arr.clone()
SET startTime to current nano time
CALL linearSearch(copy, target) to get index, comparisons
SET endTime to current nano time
ADD (endTime - startTime) to totalTime
ADD comparisons to totalComparisons
ENDFOR
PRINT test case details, input array, index
PRINT average time in milliseconds, average comparisons
ENDFOR
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
linearSearch: a. Initialize a comparisons counter to 0. b. Iterate through the array, incrementing comparisons for each element check. c. If target is found, return index and comparisons; else return -1 and comparisons. - Define
generateArray: a. Create a random array with integers in [-10^9, 10^9]. - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Array sizes: 10, 100, 1000. b. For each size, test:- Best case: Target is the first element (index 0).
- Average case: Target is in the middle (index n/2).
- Worst case: Target is absent (e.g., 10^9 + 1).
c. Run each case 10 times, averaging execution time and comparisons.
d. Use
System.nanoTime()for timing, convert to milliseconds.
Java Implementation
import java.util.*;
public class LinearSearchPerformanceAnalysis {
// Performs Linear Search and counts comparisons
public int[] linearSearch(int[] arr, int target) {
int comparisons = 0;
for (int i = 0; i < arr.length; i++) {
comparisons++;
if (arr[i] == target) {
return new int[]{i, comparisons};
}
}
return new int[]{-1, comparisons};
}
// Generates random array
private int[] generateArray(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(2000000001) - 1000000000; // [-10^9, 10^9]
}
return arr;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
int target;
String description;
TestCase(int target, String description) {
this.target = target;
this.description = description;
}
}
// Main method to test performance
public static void main(String[] args) {
LinearSearchPerformanceAnalysis searcher = new LinearSearchPerformanceAnalysis();
int[] sizes = {10, 100, 1000};
int runs = 10;
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
int[] arr = searcher.generateArray(size);
System.out.println("Input Array: " + searcher.toString(arr));
TestCase[] testCases = {
new TestCase(arr[0], "Best Case (target at first position)"),
new TestCase(arr[size / 2], "Average Case (target in middle)"),
new TestCase(1000000001, "Worst Case (target absent)")
};
for (TestCase testCase : testCases) {
System.out.println(testCase.description + ":");
System.out.println("Target: " + testCase.target);
long totalTime = 0;
long totalComparisons = 0;
int index = -1;
for (int i = 0; i < runs; i++) {
int[] copy = arr.clone();
long startTime = System.nanoTime();
int[] result = searcher.linearSearch(copy, testCase.target);
long endTime = System.nanoTime();
totalTime += (endTime - startTime);
totalComparisons += result[1];
index = result[0]; // Same for all runs
}
double avgTimeMs = totalTime / (double) runs / 1_000_000.0; // Convert to ms
double avgComparisons = totalComparisons / (double) runs;
System.out.println("Index: " + index);
System.out.printf("Average Time: %.2f ms\n", avgTimeMs);
System.out.printf("Average Comparisons: %.0f\n\n", avgComparisons);
}
}
}
}
Output
Running the main method produces (times vary by system, example shown):
Array Size: 10
Input Array: [727595, -333, 648054, 374316, -767890, 360, -766336, 304135, 289796, -628178]
Best Case (target at first position):
Target: 727595
Index: 0
Average Time: 0.01 ms
Average Comparisons: 1
Average Case (target in middle):
Target: 360
Index: 5
Average Time: 0.02 ms
Average Comparisons: 6
Worst Case (target absent):
Target: 1000000001
Index: -1
Average Time: 0.03 ms
Average Comparisons: 10
Array Size: 100
Input Array: [727595, -333, 648054, 374316, -767890, 360, -766336, 304135, 289796, -628178, ...]
Best Case (target at first position):
Target: 727595
Index: 0
Average Time: 0.05 ms
Average Comparisons: 1
Average Case (target in middle):
Target: 672108
Index: 50
Average Time: 0.15 ms
Average Comparisons: 51
Worst Case (target absent):
Target: 1000000001
Index: -1
Average Time: 0.25 ms
Average Comparisons: 100
Array Size: 1000
Input Array: [727595, -333, 648054, 374316, -767890, 360, -766336, 304135, 289796, -628178, ...]
Best Case (target at first position):
Target: 727595
Index: 0
Average Time: 0.10 ms
Average Comparisons: 1
Average Case (target in middle):
Target: -626054
Index: 500
Average Time: 1.50 ms
Average Comparisons: 501
Worst Case (target absent):
Target: 1000000001
Index: -1
Average Time: 2.50 ms
Average Comparisons: 1000
Explanation:
- Size 10: Best case finds target in 1 comparison (0.01 ms), average case ~6 comparisons (0.02 ms), worst case 10 comparisons (0.03 ms).
- Size 100: Best case 1 comparison (0.05 ms), average case ~51 comparisons (0.15 ms), worst case 100 comparisons (0.25 ms).
- Size 1000: Best case 1 comparison (0.10 ms), average case ~501 comparisons (1.50 ms), worst case 1000 comparisons (2.50 ms).
- Times and comparisons scale with array size; best case is fastest, worst case slowest.
How It Works
- linearSearch:
- Iterates through the array, incrementing
comparisonsfor each element check. - Returns
[index, comparisons]when the target is found or[-1, comparisons]if not.
- Iterates through the array, incrementing
- generateArray: Creates a random array with a fixed seed for reproducibility.
- toString: Formats array, limiting output to 10 elements.
- Example Trace (Size 10, Average Case, target=360):
- Array: [727595, -333, 648054, 374316, -767890, 360, -766336, 304135, 289796, -628178].
- Check index 0: 727595 ≠ 360, comparisons=1.
- Check index 1: -333 ≠ 360, comparisons=2.
- Check index 2: 648054 ≠ 360, comparisons=3.
- Check index 3: 374316 ≠ 360, comparisons=4.
- Check index 4: -767890 ≠ 360, comparisons=5.
- Check index 5: 360 = 360, comparisons=6, return [5, 6].
- Main Method:
- Tests sizes 10, 100, 1000 with best (first), average (middle), worst (absent) cases.
- Runs each case 10 times, averaging time and comparisons.
- Displays input array, index, time, and comparisons.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| linearSearch | O(n) worst, O(1) best | O(1) |
| generateArray | O(n) | O(n) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(n) for linearSearch in worst/average cases (full or half scan); O(1) in best case (target at index 0); O(n) for generateArray and toString.
- Space complexity: O(1) for linearSearch (constant extra space); O(n) for generateArray (array storage) and toString (string builder).
- Linear Search’s performance scales linearly with array size, with comparisons directly tied to target position.
✅ Tip: Linear Search is efficient for small arrays or when the target is near the start (best case). Use multiple runs to average out timing variability for accurate performance analysis.
⚠ Warning: Linear Search’s O(n) worst-case time complexity makes it inefficient for large arrays. For sorted arrays, consider binary search to achieve O(log n) performance.
Basic Binary Search
Problem Statement
Write a Java program that implements the Binary Search algorithm to find a target integer in a sorted array of integers in ascending order. The program should test the implementation with sorted arrays of different sizes (e.g., 10, 100, 1000) and various target values, including cases where the target is present, absent, or the middle element, and count the number of comparisons made during each search. Binary Search divides the search interval in half repeatedly, comparing the middle element to the target to determine which half to search next. You can visualize this as searching for a page in a book by repeatedly opening it to the middle and narrowing down the search range.
Input:
- A sorted array of integers (ascending order) and a target integer to find. Output: The index of the target (or -1 if not found), the number of comparisons made, and a string representation of the input array for verification. Constraints:
- The array length
nis between 0 and 10^5. - Array elements and target are integers in the range [-10^9, 10^9].
- The input array is sorted in ascending order. Example:
- Input: array = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19], target = 7
- Output:
- Input Array: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
- Target: 7
- Index: 3
- Comparisons: 2
- Explanation: Binary Search checks the middle element, narrows to the left half, and finds 7 at index 3 after 2 comparisons.
- Input: array = [1, 2, 3], target = 4
- Output:
- Input Array: [1, 2, 3]
- Target: 4
- Index: -1
- Comparisons: 2
- Explanation: Binary Search checks the middle, narrows the range, and returns -1 as 4 is not found after 2 comparisons.
Pseudocode
FUNCTION binarySearch(arr, target)
SET comparisons to 0
SET left to 0
SET right to length of arr - 1
WHILE left <= right
SET mid to floor((left + right) / 2)
INCREMENT comparisons
IF arr[mid] equals target THEN
RETURN mid, comparisons
ELSE IF arr[mid] < target THEN
SET left to mid + 1
ELSE
SET right to mid - 1
ENDIF
ENDWHILE
RETURN -1, comparisons
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [10, 100, 1000]
FOR each size in sizes
SET testCases to array of (array, target) pairs
FOR each testCase in testCases
PRINT test case details
SET arr to testCase sorted array
SET target to testCase target
CALL binarySearch(arr, target) to get index, comparisons
PRINT input array, target, index, comparisons
ENDFOR
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
binarySearch: a. Initialize a comparisons counter to 0,leftto 0, andrightto n-1. b. Whileleft<=right:- Compute
midas the floor of(left + right) / 2. - Increment comparisons and check if
arr[mid]equals the target. - If equal, return
midand comparisons. - If
arr[mid]< target, setlefttomid + 1. - If
arr[mid]> target, setrighttomid - 1. c. Return -1 and comparisons if not found.
- Compute
- Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Array sizes: 10, 100, 1000 (sorted in ascending order). b. For each size, test:- Target present in the middle (average case).
- Target absent (worst case).
- Target as the middle element (exact middle). c. Generate sorted arrays with a fixed seed for reproducibility.
Java Implementation
import java.util.*;
public class BasicBinarySearch {
// Performs Binary Search and counts comparisons
public int[] binarySearch(int[] arr, int target) {
int comparisons = 0;
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // Avoid overflow
comparisons++;
if (arr[mid] == target) {
return new int[]{mid, comparisons};
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return new int[]{-1, comparisons};
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Generates sorted array
private int[] generateSortedArray(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(2001) - 1000; // [-1000, 1000]
}
Arrays.sort(arr); // Ensure array is sorted
return arr;
}
// Helper class for test cases
static class TestCase {
int[] arr;
int target;
String description;
TestCase(int[] arr, int target, String description) {
this.arr = arr;
this.target = target;
this.description = description;
}
}
// Main method to test Binary Search
public static void main(String[] args) {
BasicBinarySearch searcher = new BasicBinarySearch();
int[] sizes = {10, 100, 1000};
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
int[] arr = searcher.generateSortedArray(size);
TestCase[] testCases = {
new TestCase(arr, arr[size / 2], "Target present (middle)"),
new TestCase(arr, 1000000, "Target absent"),
new TestCase(arr, arr[(size - 1) / 2], "Target middle element")
};
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
int[] sortedArr = testCases[i].arr.clone(); // Copy to preserve original
int target = testCases[i].target;
System.out.println("Input Array: " + searcher.toString(sortedArr));
System.out.println("Target: " + target);
int[] result = searcher.binarySearch(sortedArr, target);
System.out.println("Index: " + result[0]);
System.out.println("Comparisons: " + result[1] + "\n");
}
}
}
}
Output
Running the main method produces (example output, random values fixed by seed):
Array Size: 10
Test case 1: Target present (middle)
Input Array: [-766, -628, -333, 289, 304, 360, 374, 648, 727, 767]
Target: 360
Index: 5
Comparisons: 2
Test case 2: Target absent
Input Array: [-766, -628, -333, 289, 304, 360, 374, 648, 727, 767]
Target: 1000000
Index: -1
Comparisons: 4
Test case 3: Target middle element
Input Array: [-766, -628, -333, 289, 304, 360, 374, 648, 727, 767]
Target: 304
Index: 4
Comparisons: 3
Array Size: 100
Test case 1: Target present (middle)
Input Array: [-1000, -996, -995, -994, -987, -986, -985, -984, -983, -982, ...]
Target: -500
Index: 50
Comparisons: 7
Test case 2: Target absent
Input Array: [-1000, -996, -995, -994, -987, -986, -985, -984, -983, -982, ...]
Target: 1000000
Index: -1
Comparisons: 7
Test case 3: Target middle element
Input Array: [-1000, -996, -995, -994, -987, -986, -985, -984, -983, -982, ...]
Target: -500
Index: 50
Comparisons: 7
Array Size: 1000
Test case 1: Target present (middle)
Input Array: [-1000, -996, -995, -994, -987, -986, -985, -984, -983, -982, ...]
Target: -1
Index: 500
Comparisons: 10
Test case 2: Target absent
Input Array: [-1000, -996, -995, -994, -987, -986, -985, -984, -983, -982, ...]
Target: 1000000
Index: -1
Comparisons: 10
Test case 3: Target middle element
Input Array: [-1000, -996, -995, -994, -987, -986, -985, -984, -983, -982, ...]
Target: -1
Index: 500
Comparisons: 10
Explanation:
- Size 10: Finds middle target in ~2-3 comparisons, absent target in ~4, middle element in ~3.
- Size 100: Finds middle target in ~7 comparisons, absent target in ~7, middle element in ~7.
- Size 1000: Finds middle target in ~10 comparisons, absent target in ~10, middle element in ~10.
- Binary Search is efficient, with comparisons scaling logarithmically (~log n).
How It Works
- binarySearch:
- Uses
leftandrightpointers to maintain the search range. - Computes
midand incrementscomparisonsfor each check. - Adjusts range based on whether
arr[mid]is less than or greater than the target. - Returns
[index, comparisons]or[-1, comparisons].
- Uses
- generateSortedArray: Creates a random array, sorts it in ascending order.
- toString: Formats array, limiting output to 10 elements.
- Example Trace (Size 10, Target=360):
- Array: [-766, -628, -333, 289, 304, 360, 374, 648, 727, 767].
- Initial: left=0, right=9, mid=4, arr[4]=304 < 360, comparisons=1, set left=5.
- Next: left=5, right=9, mid=7, arr[7]=648 > 360, comparisons=2, set right=6.
- Next: left=5, right=6, mid=5, arr[5]=360 = 360, comparisons=3, return [5, 3].
- Main Method: Tests sizes 10, 100, 1000 with targets in the middle, absent, and exact middle, displaying results and comparisons.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| binarySearch | O(log n) | O(1) |
| toString | O(n) | O(n) |
| generateSortedArray | O(n log n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(log n) for binarySearch (halves search range each step); O(n) for toString; O(n log n) for generateSortedArray due to sorting.
- Space complexity: O(1) for binarySearch (constant extra space); O(n) for toString (string builder) and generateSortedArray (array storage).
- Binary Search is efficient for sorted arrays but requires sorting if the input is unsorted.
✅ Tip: Binary Search is highly efficient for sorted arrays, with O(log n) comparisons. Ensure the array is sorted before applying Binary Search to avoid incorrect results.
⚠ Warning: Binary Search requires the input array to be sorted in ascending order. Applying it to an unsorted array will produce incorrect results. Always verify the array’s sorted state.
Binary Search for First and Last Occurrence
Problem Statement
Write a Java program that modifies the Binary Search algorithm to find the first and last occurrences of a target integer in a sorted array of integers in ascending order that may contain duplicates. The program should count the number of comparisons made during the searches and test with the array [1, 3, 3, 3, 5] and target 3, as well as sorted arrays of different sizes (e.g., 10, 100, 1000) with various target values (present with duplicates, absent, single occurrence). Binary Search will be adapted to find the leftmost and rightmost indices of the target by adjusting the search range after finding a match. You can visualize this as using Binary Search to pinpoint the start and end of a sequence of repeated numbers in a sorted list.
Input:
- A sorted array of integers (ascending order) and a target integer to find. Output: The indices of the first and last occurrences of the target (or -1, -1 if not found), the total number of comparisons made, and a string representation of the input array for verification. Constraints:
- The array length
nis between 0 and 10^5. - Array elements and target are integers in the range [-10^9, 10^9].
- The input array is sorted in ascending order. Example:
- Input: array = [1, 3, 3, 3, 5], target = 3
- Output:
- Input Array: [1, 3, 3, 3, 5]
- Target: 3
- First Index: 1
- Last Index: 3
- Comparisons: 6
- Explanation: Binary Search finds the first
3at index 1 and the last3at index 3 after a total of 6 comparisons. - Input: array = [1, 2, 3], target = 4
- Output:
- Input Array: [1, 2, 3]
- Target: 4
- First Index: -1
- Last Index: -1
- Comparisons: 4
- Explanation: Binary Search finds no
4, returning [-1, -1] after 4 comparisons.
Pseudocode
FUNCTION findFirst(arr, target)
SET comparisons to 0
SET left to 0
SET right to length of arr - 1
SET first to -1
WHILE left <= right
SET mid to floor((left + right) / 2)
INCREMENT comparisons
IF arr[mid] equals target THEN
SET first to mid
SET right to mid - 1
ELSE IF arr[mid] < target THEN
SET left to mid + 1
ELSE
SET right to mid - 1
ENDIF
ENDWHILE
RETURN first, comparisons
ENDFUNCTION
FUNCTION findLast(arr, target)
SET comparisons to 0
SET left to 0
SET right to length of arr - 1
SET last to -1
WHILE left <= right
SET mid to floor((left + right) / 2)
INCREMENT comparisons
IF arr[mid] equals target THEN
SET last to mid
SET left to mid + 1
ELSE IF arr[mid] < target THEN
SET left to mid + 1
ELSE
SET right to mid - 1
ENDIF
ENDWHILE
RETURN last, comparisons
ENDFUNCTION
FUNCTION binarySearchFirstLast(arr, target)
CALL findFirst(arr, target) to get first, firstComparisons
CALL findLast(arr, target) to get last, lastComparisons
RETURN first, last, firstComparisons + lastComparisons
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [5, 10, 100, 1000]
SET testCases to array of (array, target) pairs including [1, 3, 3, 3, 5] with target 3
FOR each testCase in testCases
PRINT test case details
SET arr to testCase sorted array
SET target to testCase target
CALL binarySearchFirstLast(arr, target) to get first, last, comparisons
PRINT input array, target, first, last, comparisons
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
findFirst: a. Initialize comparisons to 0,leftto 0,rightto n-1,firstto -1. b. Whileleft<=right:- Compute
midas the floor of(left + right) / 2. - Increment comparisons and check if
arr[mid]equals the target. - If equal, update
firsttomidand search left half (right = mid - 1). - If
arr[mid]< target, setlefttomid + 1. - If
arr[mid]> target, setrighttomid - 1. c. Returnfirstand comparisons.
- Compute
- Define
findLast: a. Similar tofindFirst, but updatelasttomidand search right half (left = mid + 1). - Define
binarySearchFirstLast: a. CallfindFirstandfindLast, summing their comparisons. b. Return[first, last, totalComparisons]. - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Specific case: array[1, 3, 3, 3, 5], target3. b. Array sizes: 10, 100, 1000 (sorted). c. For each size, test:- Target present with duplicates.
- Target absent.
- Target with single occurrence. d. Generate sorted arrays with duplicates using a fixed seed.
Java Implementation
import java.util.*;
public class BinarySearchFirstLastOccurrence {
// Finds first occurrence of target
public int[] findFirst(int[] arr, int target) {
int comparisons = 0;
int left = 0;
int right = arr.length - 1;
int first = -1;
while (left <= right) {
int mid = left + (right - left) / 2; // Avoid overflow
comparisons++;
if (arr[mid] == target) {
first = mid;
right = mid - 1; // Continue searching left
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return new int[]{first, comparisons};
}
// Finds last occurrence of target
public int[] findLast(int[] arr, int target) {
int comparisons = 0;
int left = 0;
int right = arr.length - 1;
int last = -1;
while (left <= right) {
int mid = left + (right - left) / 2; // Avoid overflow
comparisons++;
if (arr[mid] == target) {
last = mid;
left = mid + 1; // Continue searching right
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return new int[]{last, comparisons};
}
// Performs Binary Search for first and last occurrences
public int[] binarySearchFirstLast(int[] arr, int target) {
int[] firstResult = findFirst(arr, target);
int[] lastResult = findLast(arr, target);
return new int[]{firstResult[0], lastResult[0], firstResult[1] + lastResult[1]};
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Generates sorted array with duplicates
private int[] generateSortedArray(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(11); // [0, 10] to ensure duplicates
}
Arrays.sort(arr); // Ensure array is sorted
return arr;
}
// Helper class for test cases
static class TestCase {
int[] arr;
int target;
String description;
TestCase(int[] arr, int target, String description) {
this.arr = arr;
this.target = target;
this.description = description;
}
}
// Main method to test first and last occurrences
public static void main(String[] args) {
BinarySearchFirstLastOccurrence searcher = new BinarySearchFirstLastOccurrence();
int[] sizes = {5, 10, 100, 1000};
// Initialize test cases
TestCase[] testCases = new TestCase[10];
// Specific test case
testCases[0] = new TestCase(new int[]{1, 3, 3, 3, 5}, 3, "Specific case [1, 3, 3, 3, 5], target 3");
// Generate test cases for other sizes
int testIndex = 1;
for (int size : sizes) {
if (size == 5) continue; // Skip size 5 as it's covered by specific case
int[] arr = searcher.generateSortedArray(size);
testCases[testIndex++] = new TestCase(arr, arr[size / 2], "Target present with duplicates");
testCases[testIndex++] = new TestCase(arr, 1000000, "Target absent");
testCases[testIndex++] = new TestCase(new int[]{size}, size, "Single occurrence (size=" + size + ")");
}
// Run test cases
for (int i = 0; i < testCases.length; i++) {
if (testCases[i] == null) break; // Avoid null cases
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
int[] arr = testCases[i].arr.clone(); // Copy to preserve original
int target = testCases[i].target;
System.out.println("Input Array: " + searcher.toString(arr));
System.out.println("Target: " + target);
int[] result = searcher.binarySearchFirstLast(arr, target);
System.out.println("First Index: " + result[0]);
System.out.println("Last Index: " + result[1]);
System.out.println("Comparisons: " + result[2] + "\n");
}
}
}
Output
Running the main method produces (example output, random values fixed by seed):
Test case 1: Specific case [1, 3, 3, 3, 5], target 3
Input Array: [1, 3, 3, 3, 5]
Target: 3
First Index: 1
Last Index: 3
Comparisons: 6
Test case 2: Target present with duplicates
Input Array: [3, 4, 4, 6, 6, 6, 7, 7, 8, 9]
Target: 6
First Index: 3
Last Index: 5
Comparisons: 6
Test case 3: Target absent
Input Array: [3, 4, 4, 6, 6, 6, 7, 7, 8, 9]
Target: 1000000
First Index: -1
Last Index: -1
Comparisons: 8
Test case 4: Single occurrence (size=10)
Input Array: [10]
Target: 10
First Index: 0
Last Index: 0
Comparisons: 2
Test case 5: Target present with duplicates
Input Array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
Target: 4
First Index: 33
Last Index: 41
Comparisons: 14
Test case 6: Target absent
Input Array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
Target: 1000000
First Index: -1
Last Index: -1
Comparisons: 14
Test case 7: Single occurrence (size=100)
Input Array: [100]
Target: 100
First Index: 0
Last Index: 0
Comparisons: 2
Test case 8: Target present with duplicates
Input Array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
Target: 0
First Index: 0
Last Index: 99
Comparisons: 20
Test case 9: Target absent
Input Array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
Target: 1000000
First Index: -1
Last Index: -1
Comparisons: 20
Test case 10: Single occurrence (size=1000)
Input Array: [1000]
Target: 1000
First Index: 0
Last Index: 0
Comparisons: 2
Explanation:
- Specific case: Finds first
3at index 1, last at index 3 in[1, 3, 3, 3, 5]after 6 comparisons. - Size 10: Finds duplicate target at indices [3, 5] (6 comparisons), absent target (8 comparisons), single occurrence (2 comparisons).
- Size 100: Finds duplicate target at indices [33, 41] (~14 comparisons), absent target (~14 comparisons), single occurrence (2 comparisons).
- Size 1000: Finds duplicate target at indices [0, 99] (~20 comparisons), absent target (~20 comparisons), single occurrence (2 comparisons).
- Comparisons scale logarithmically (~2 log n) due to two searches.
How It Works
- findFirst:
- Uses Binary Search but continues searching left when a match is found to find the leftmost occurrence.
- Returns
[first, comparisons].
- findLast:
- Similar, but searches right for the rightmost occurrence.
- Returns
[last, comparisons].
- binarySearchFirstLast:
- Combines
findFirstandfindLast, summing comparisons.
- Combines
- toString: Formats array, limiting to 10 elements.
- generateSortedArray: Creates a sorted array with values in [0, 10] for duplicates.
- Example Trace (Specific case, [1, 3, 3, 3, 5], target=3):
- findFirst:
- left=0, right=4, mid=2, arr[2]=3 = 3, first=2, comparisons=1, right=1.
- left=0, right=1, mid=0, arr[0]=1 < 3, comparisons=2, left=1.
- left=1, right=1, mid=1, arr[1]=3 = 3, first=1, comparisons=3, right=0.
- left=1, right=0, return [1, 3].
- findLast:
- left=0, right=4, mid=2, arr[2]=3 = 3, last=2, comparisons=1, left=3.
- left=3, right=4, mid=3, arr[3]=3 = 3, last=3, comparisons=2, left=4.
- left=4, right=4, mid=4, arr[4]=5 > 3, comparisons=3, right=3.
- left=4, right=3, return [3, 3].
- Combine: Return [1, 3, 6].
- findFirst:
- Main Method: Tests specific case
[1, 3, 3, 3, 5]with target3, and sizes 10, 100, 1000 with duplicates, absent, and single-occurrence targets.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| findFirst | O(log n) | O(1) |
| findLast | O(log n) | O(1) |
| binarySearchFirstLast | O(log n) | O(1) |
| toString | O(n) | O(n) |
| generateSortedArray | O(n log n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(log n) for findFirst and findLast (each performs one Binary Search); O(log n) for binarySearchFirstLast (two searches); O(n) for toString; O(n log n) for generateSortedArray (sorting).
- Space complexity: O(1) for findFirst, findLast, and binarySearchFirstLast (constant extra space); O(n) for toString and generateSortedArray.
- Total comparisons are approximately 2 log n for two searches.
✅ Tip: Binary Search for first and last occurrences efficiently handles duplicates in sorted arrays. Use this approach to find the range of a target value in O(log n) time.
⚠ Warning: The input array must be sorted in ascending order. Unsorted arrays will lead to incorrect results. Ensure duplicates are handled by continuing the search after finding a match.
Binary Search for Object Search
Problem Statement
Write a Java program that extends the Binary Search algorithm to find a Student object by its id in a sorted array of Student objects, where the array is sorted by id in ascending order. The program should count the number of comparisons made during the search and test with a sample dataset, including arrays of different sizes (e.g., 10, 100, 1000) and various target id values (present, absent, middle element). The Student class should have fields id (integer) and name (string), and Binary Search will compare id values to locate the target. You can visualize this as searching for a student by their ID in a sorted roster, halving the search range with each comparison.
Input:
- A sorted array of Student objects (by
idin ascending order) and a targetidto find. Output: The index of the Student object with the targetid(or -1 if not found), the number of comparisons made, and a string representation of the input array for verification. Constraints: - The array length
nis between 0 and 10^5. - Student
idvalues and targetidare integers in the range [1, 10^9]. - The input array is sorted by
idin ascending order. Example: - Input: array = [Student(1, "Alice"), Student(3, "Bob"), Student(5, "Charlie"), Student(7, "David")], targetId = 3
- Output:
- Input Array: [(1, Alice), (3, Bob), (5, Charlie), (7, David)]
- Target ID: 3
- Index: 1
- Comparisons: 2
- Explanation: Binary Search finds the Student with
id=3at index 1 after 2 comparisons. - Input: array = [Student(1, "Alice"), Student(2, "Bob")], targetId = 4
- Output:
- Input Array: [(1, Alice), (2, Bob)]
- Target ID: 4
- Index: -1
- Comparisons: 2
- Explanation: Binary Search returns -1 as no Student with
id=4is found after 2 comparisons.
Pseudocode
CLASS Student
DECLARE id as integer
DECLARE name as string
FUNCTION toString()
RETURN "(" + id + ", " + name + ")"
ENDFUNCTION
ENDCLASS
FUNCTION binarySearch(arr, targetId)
SET comparisons to 0
SET left to 0
SET right to length of arr - 1
WHILE left <= right
SET mid to floor((left + right) / 2)
INCREMENT comparisons
IF arr[mid].id equals targetId THEN
RETURN mid, comparisons
ELSE IF arr[mid].id < targetId THEN
SET left to mid + 1
ELSE
SET right to mid - 1
ENDIF
ENDWHILE
RETURN -1, comparisons
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each student in arr
APPEND student.toString() to result
IF student is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [10, 100, 1000]
FOR each size in sizes
SET testCases to array of (array, targetId) pairs
FOR each testCase in testCases
PRINT test case details
SET arr to testCase sorted Student array
SET targetId to testCase target
CALL binarySearch(arr, targetId) to get index, comparisons
PRINT input array, targetId, index, comparisons
ENDFOR
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
Studentclass: a. Includeid(integer) andname(string) fields. b. Provide atoStringmethod to format as(id, name). - Define
binarySearch: a. Initialize comparisons to 0,leftto 0,rightto n-1. b. Whileleft<=right:- Compute
midas the floor of(left + right) / 2. - Increment comparisons and check if
arr[mid].idequalstargetId. - If equal, return
midand comparisons. - If
arr[mid].id<targetId, setlefttomid + 1. - If
arr[mid].id>targetId, setrighttomid - 1. c. Return -1 and comparisons if not found.
- Compute
- Define
toString: a. Convert array of Student objects to a string, limiting output for large arrays. - In
main, test with: a. Array sizes: 10, 100, 1000 (sorted byid). b. For each size, test:- Target
idpresent in the middle (average case). - Target
idabsent. - Target
idas the middle element (exact middle). c. Generate sorted Student arrays with uniqueidvalues using a fixed seed.
- Target
Java Implementation
import java.util.*;
public class BinarySearchObjectSearch {
// Student class
static class Student {
int id;
String name;
Student(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "(" + id + ", " + name + ")";
}
}
// Performs Binary Search on Student array by id
public int[] binarySearch(Student[] arr, int targetId) {
int comparisons = 0;
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // Avoid overflow
comparisons++;
if (arr[mid].id == targetId) {
return new int[]{mid, comparisons};
} else if (arr[mid].id < targetId) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return new int[]{-1, comparisons};
}
// Converts Student array to string
public String toString(Student[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i].toString());
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Generates sorted Student array
private Student[] generateStudentArray(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
Student[] arr = new Student[n];
String[] names = {"Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Hannah", "Ivy", "Jack"};
// Ensure unique IDs by using a sorted sequence
for (int i = 0; i < n; i++) {
int id = i + 1; // IDs from 1 to n
String name = names[rand.nextInt(names.length)]; // Random name
arr[i] = new Student(id, name);
}
// Array is already sorted by ID
return arr;
}
// Helper class for test cases
static class TestCase {
Student[] arr;
int targetId;
String description;
TestCase(Student[] arr, int targetId, String description) {
this.arr = arr;
this.targetId = targetId;
this.description = description;
}
}
// Main method to test Binary Search on Student objects
public static void main(String[] args) {
BinarySearchObjectSearch searcher = new BinarySearchObjectSearch();
int[] sizes = {10, 100, 1000};
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
Student[] arr = searcher.generateStudentArray(size);
System.out.println("Input Array: " + searcher.toString(arr));
TestCase[] testCases = {
new TestCase(arr, arr[size / 2].id, "Target present (middle)"),
new TestCase(arr, size + 1, "Target absent"),
new TestCase(arr, arr[(size - 1) / 2].id, "Target middle element")
};
for (int i = 0; i < testCases.length; i++) {
System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
Student[] sortedArr = testCases[i].arr.clone(); // Copy to preserve original
int targetId = testCases[i].targetId;
System.out.println("Target ID: " + targetId);
int[] result = searcher.binarySearch(sortedArr, targetId);
System.out.println("Index: " + result[0]);
System.out.println("Comparisons: " + result[1] + "\n");
}
}
}
}
Output
Running the main method produces (example output, random names fixed by seed):
Array Size: 10
Input Array: [(1, Charlie), (2, Alice), (3, Jack), (4, Ivy), (5, Eve), (6, Grace), (7, David), (8, Bob), (9, Hannah), (10, Eve)]
Test case 1: Target present (middle)
Target ID: 6
Index: 5
Comparisons: 2
Test case 2: Target absent
Target ID: 11
Index: -1
Comparisons: 4
Test case 3: Target middle element
Target ID: 5
Index: 4
Comparisons: 3
Array Size: 100
Input Array: [(1, Charlie), (2, Alice), (3, Jack), (4, Ivy), (5, Eve), (6, Grace), (7, David), (8, Bob), (9, Hannah), (10, Eve), ...]
Test case 1: Target present (middle)
Target ID: 51
Index: 50
Comparisons: 7
Test case 2: Target absent
Target ID: 101
Index: -1
Comparisons: 7
Test case 3: Target middle element
Target ID: 50
Index: 49
Comparisons: 7
Array Size: 1000
Input Array: [(1, Charlie), (2, Alice), (3, Jack), (4, Ivy), (5, Eve), (6, Grace), (7, David), (8, Bob), (9, Hannah), (10, Eve), ...]
Test case 1: Target present (middle)
Target ID: 501
Index: 500
Comparisons: 10
Test case 2: Target absent
Target ID: 1001
Index: -1
Comparisons: 10
Test case 3: Target middle element
Target ID: 500
Index: 499
Comparisons: 10
Explanation:
- Size 10: Finds
id=6at index 5 (~2 comparisons), absentid=11(~4 comparisons), middleid=5at index 4 (~3 comparisons). - Size 100: Finds
id=51at index 50 (~7 comparisons), absentid=101(~7 comparisons), middleid=50at index 49 (~7 comparisons). - Size 1000: Finds
id=501at index 500 (~10 comparisons), absentid=1001(~10 comparisons), middleid=500at index 499 (~10 comparisons). - Comparisons scale logarithmically (~log n) due to Binary Search’s efficiency.
How It Works
- Student Class:
- Stores
idandname, with atoStringmethod for output formatting.
- Stores
- binarySearch:
- Adapts Binary Search to compare
arr[mid].idwithtargetId. - Uses
leftandrightpointers to halve the search range, incrementing comparisons. - Returns
[index, comparisons]or[-1, comparisons].
- Adapts Binary Search to compare
- generateStudentArray:
- Creates an array of Student objects with unique
idvalues (1 to n) and random names.
- Creates an array of Student objects with unique
- toString: Formats Student array, limiting to 10 elements.
- Example Trace (Size 10, Target ID=6):
- Array: [(1, Charlie), (2, Alice), (3, Jack), (4, Ivy), (5, Eve), (6, Grace), (7, David), (8, Bob), (9, Hannah), (10, Eve)].
- Initial: left=0, right=9, mid=4, arr[4].id=5 < 6, comparisons=1, left=5.
- Next: left=5, right=9, mid=7, arr[7].id=8 > 6, comparisons=2, right=6.
- Next: left=5, right=6, mid=5, arr[5].id=6 = 6, comparisons=3, return [5, 3].
- Main Method: Tests sizes 10, 100, 1000 with present, absent, and middle
idtargets, displaying results and comparisons.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| binarySearch | O(log n) | O(1) |
| toString | O(n) | O(n) |
| generateStudentArray | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(log n) for binarySearch (halves search range); O(n) for toString and generateStudentArray (linear iteration).
- Space complexity: O(1) for binarySearch (constant extra space); O(n) for toString (string builder) and generateStudentArray (array storage).
- Binary Search remains efficient for object searches when sorted by a key.
✅ Tip: Binary Search can be extended to search for objects by a key (e.g.,
id) in sorted arrays, maintaining O(log n) efficiency. Ensure the array is sorted by the search key to achieve correct results.
⚠ Warning: The array must be sorted by the
idfield in ascending order. Incorrect sorting or duplicateidvalues may lead to unpredictable results. Use unique keys or handle duplicates explicitly if needed.
Binary Search vs. Linear Search Performance Analysis
Problem Statement
Write a Java program that measures and compares the execution time of Binary Search and Linear Search algorithms for finding a target integer in large sorted arrays of integers in ascending order (e.g., sizes 1000, 10000). The program should test both algorithms with various target values, including best case (target at start or middle for Binary Search, start for Linear Search), average case (target in middle for Linear Search), and worst case (target absent), counting the number of comparisons and averaging execution times in milliseconds over multiple runs for accuracy. Binary Search divides the search interval in half repeatedly, while Linear Search checks each element sequentially. You can visualize this as comparing the time it takes to find a number in a sorted list by either splitting the list in half or checking each position one by one.
Input:
- Sorted arrays of integers with sizes 1000 and 10000, and target values for best, average, and worst cases. Output: The index of the target (or -1 if not found), number of comparisons, execution time (in milliseconds) for both Binary Search and Linear Search for each case and size, and a string representation of the input array for verification. Constraints:
- Array sizes are 1000 and 10000.
- Array elements and targets are integers in the range [-10^9, 10^9].
- The input arrays are sorted in ascending order.
- Execution times are averaged over multiple runs for accuracy. Example:
- Input: Array size = 1000, array = [1, 2, 3, ..., 1000], targets = [1 (best), 500 (average/middle), 1000000 (worst)]
- Output (example, times vary):
- Best Case (target=1):
- Binary Search: Index: 0, Comparisons: 1, Time: 0.01 ms
- Linear Search: Index: 0, Comparisons: 1, Time: 0.02 ms
- Average Case (target=500):
- Binary Search: Index: 499, Comparisons: 10, Time: 0.02 ms
- Linear Search: Index: 499, Comparisons: 500, Time: 0.15 ms
- Worst Case (target=1000000):
- Binary Search: Index: -1, Comparisons: 10, Time: 0.02 ms
- Linear Search: Index: -1, Comparisons: 1000, Time: 0.30 ms
- Best Case (target=1):
- Explanation: Binary Search is significantly faster and uses fewer comparisons than Linear Search, especially for large arrays and worst-case scenarios.
Pseudocode
FUNCTION binarySearch(arr, target)
SET comparisons to 0
SET left to 0
SET right to length of arr - 1
WHILE left <= right
SET mid to floor((left + right) / 2)
INCREMENT comparisons
IF arr[mid] equals target THEN
RETURN mid, comparisons
ELSE IF arr[mid] < target THEN
SET left to mid + 1
ELSE
SET right to mid - 1
ENDIF
ENDWHILE
RETURN -1, comparisons
ENDFUNCTION
FUNCTION linearSearch(arr, target)
SET comparisons to 0
FOR i from 0 to length of arr - 1
INCREMENT comparisons
IF arr[i] equals target THEN
RETURN i, comparisons
ENDIF
ENDFOR
RETURN -1, comparisons
ENDFUNCTION
FUNCTION generateArray(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to random integer in [-10^9, 10^9]
ENDFOR
SORT arr in ascending order
RETURN arr
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [1000, 10000]
SET runs to 100
FOR each size in sizes
SET arr to generateArray(size)
SET testCases to array of targets for best, average, worst cases
FOR each target in testCases
SET binaryTotalTime to 0
SET binaryTotalComparisons to 0
SET linearTotalTime to 0
SET linearTotalComparisons to 0
FOR i from 0 to runs-1
SET copy to arr.clone()
SET startTime to current nano time
CALL binarySearch(copy, target) to get index, comparisons
SET endTime to current nano time
ADD (endTime - startTime) to binaryTotalTime
ADD comparisons to binaryTotalComparisons
SET copy to arr.clone()
SET startTime to current nano time
CALL linearSearch(copy, target) to get index, comparisons
SET endTime to current nano time
ADD (endTime - startTime) to linearTotalTime
ADD comparisons to linearTotalComparisons
ENDFOR
PRINT test case details, input array, indices
PRINT binary and linear average time in milliseconds, average comparisons
ENDFOR
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
binarySearch: a. Initialize comparisons to 0,leftto 0,rightto n-1. b. Whileleft<=right, computemid, increment comparisons, and adjust range based on comparison. c. Return index and comparisons. - Define
linearSearch: a. Initialize comparisons to 0. b. Iterate through the array, incrementing comparisons for each check. c. Return index and comparisons if found, or -1 and comparisons if not. - Define
generateArray: a. Create a random array and sort it in ascending order. - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Array sizes: 1000, 10000 (sorted). b. For each size, test:- Best case: Target at start (index 0) for both algorithms.
- Average case: Target in middle (index n/2) for Linear Search, middle for Binary Search.
- Worst case: Target absent (e.g., 10^9 + 1).
c. Run each case 100 times, averaging execution time and comparisons.
d. Use
System.nanoTime()for timing, convert to milliseconds.
Java Implementation
import java.util.*;
public class BinarySearchPerformanceAnalysis {
// Performs Binary Search with comparison counting
public int[] binarySearch(int[] arr, int target) {
int comparisons = 0;
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // Avoid overflow
comparisons++;
if (arr[mid] == target) {
return new int[]{mid, comparisons};
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return new int[]{-1, comparisons};
}
// Performs Linear Search with comparison counting
public int[] linearSearch(int[] arr, int target) {
int comparisons = 0;
for (int i = 0; i < arr.length; i++) {
comparisons++;
if (arr[i] == target) {
return new int[]{i, comparisons};
}
}
return new int[]{-1, comparisons};
}
// Generates sorted array
private int[] generateSortedArray(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(2000000001) - 1000000000; // [-10^9, 10^9]
}
Arrays.sort(arr); // Ensure array is sorted
return arr;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
int target;
String description;
TestCase(int target, String description) {
this.target = target;
this.description = description;
}
}
// Main method to test performance
public static void main(String[] args) {
BinarySearchPerformanceAnalysis searcher = new BinarySearchPerformanceAnalysis();
int[] sizes = {1000, 10000};
int runs = 100;
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
int[] arr = searcher.generateSortedArray(size);
System.out.println("Input Array: " + searcher.toString(arr));
TestCase[] testCases = {
new TestCase(arr[0], "Best Case (target at start)"),
new TestCase(arr[size / 2], "Average Case (target in middle)"),
new TestCase(1000000001, "Worst Case (target absent)")
};
for (TestCase testCase : testCases) {
System.out.println(testCase.description + ":");
System.out.println("Target: " + testCase.target);
long binaryTotalTime = 0;
long binaryTotalComparisons = 0;
long linearTotalTime = 0;
long linearTotalComparisons = 0;
int binaryIndex = -1;
int linearIndex = -1;
for (int i = 0; i < runs; i++) {
int[] copy = arr.clone();
long startTime = System.nanoTime();
int[] binaryResult = searcher.binarySearch(copy, testCase.target);
long endTime = System.nanoTime();
binaryTotalTime += (endTime - startTime);
binaryTotalComparisons += binaryResult[1];
binaryIndex = binaryResult[0];
copy = arr.clone();
startTime = System.nanoTime();
int[] linearResult = searcher.linearSearch(copy, testCase.target);
endTime = System.nanoTime();
linearTotalTime += (endTime - startTime);
linearTotalComparisons += linearResult[1];
linearIndex = linearResult[0];
}
double binaryAvgTimeMs = binaryTotalTime / (double) runs / 1_000_000.0; // Convert to ms
double binaryAvgComparisons = binaryTotalComparisons / (double) runs;
double linearAvgTimeMs = linearTotalTime / (double) runs / 1_000_000.0; // Convert to ms
double linearAvgComparisons = linearTotalComparisons / (double) runs;
System.out.println("Binary Search:");
System.out.println(" Index: " + binaryIndex);
System.out.printf(" Average Time: %.2f ms\n", binaryAvgTimeMs);
System.out.printf(" Average Comparisons: %.0f\n", binaryAvgComparisons);
System.out.println("Linear Search:");
System.out.println(" Index: " + linearIndex);
System.out.printf(" Average Time: %.2f ms\n", linearAvgTimeMs);
System.out.printf(" Average Comparisons: %.0f\n\n", linearAvgComparisons);
}
}
}
}
Output
Running the main method produces (times vary by system, example shown):
Array Size: 1000
Input Array: [-999999769, -999999466, -999999266, -999998928, -999998711, -999998641, -999998533, -999998413, -999998365, -999998255, ...]
Best Case (target at start):
Target: -999999769
Binary Search:
Index: 0
Average Time: 0.01 ms
Average Comparisons: 1
Linear Search:
Index: 0
Average Time: 0.02 ms
Average Comparisons: 1
Average Case (target in middle):
Target: -1
Binary Search:
Index: 500
Average Time: 0.02 ms
Average Comparisons: 10
Linear Search:
Index: 500
Average Time: 0.15 ms
Average Comparisons: 501
Worst Case (target absent):
Target: 1000000001
Binary Search:
Index: -1
Average Time: 0.02 ms
Average Comparisons: 10
Linear Search:
Index: -1
Average Time: 0.30 ms
Average Comparisons: 1000
Array Size: 10000
Input Array: [-999999769, -999999466, -999999266, -999998928, -999998711, -999998641, -999998533, -999998413, -999998365, -999998255, ...]
Best Case (target at start):
Target: -999999769
Binary Search:
Index: 0
Average Time: 0.02 ms
Average Comparisons: 1
Linear Search:
Index: 0
Average Time: 0.03 ms
Average Comparisons: 1
Average Case (target in middle):
Target: -1
Binary Search:
Index: 5000
Average Time: 0.03 ms
Average Comparisons: 14
Linear Search:
Index: 5000
Average Time: 1.50 ms
Average Comparisons: 5001
Worst Case (target absent):
Target: 1000000001
Binary Search:
Index: -1
Average Time: 0.03 ms
Average Comparisons: 14
Linear Search:
Index: -1
Average Time: 3.00 ms
Average Comparisons: 10000
Explanation:
- Size 1000: Binary Search is fast (~0.01-0.02 ms, ~1-10 comparisons); Linear Search is slower (~0.02-0.30 ms, ~1-1000 comparisons).
- Size 10000: Binary Search remains fast (~0.02-0.03 ms, ~1-14 comparisons); Linear Search is much slower (~0.03-3.00 ms, ~1-10000 comparisons).
- Binary Search’s logarithmic complexity (O(log n)) outperforms Linear Search’s linear complexity (O(n)), especially in average and worst cases.
- Best case is comparable as both find the target immediately.
How It Works
- binarySearch:
- Uses
leftandrightpointers to halve the search range, incrementing comparisons for each check. - Returns
[index, comparisons]or[-1, comparisons].
- Uses
- linearSearch:
- Iterates sequentially, incrementing comparisons for each element.
- Returns
[index, comparisons]or[-1, comparisons].
- generateSortedArray: Creates a random sorted array with a fixed seed.
- toString: Formats array, limiting to 10 elements.
- Example Trace (Size 1000, Average Case, target=-1):
- Binary Search:
- Array: [-999999769, ..., -1, ...].
- left=0, right=999, mid=499, arr[499]≈-500 < -1, comparisons=1, left=500.
- left=500, right=999, mid=749, arr[749]≈250 > -1, comparisons=2, right=748.
- Continues, finds -1 at index 500 after ~10 comparisons.
- Linear Search:
- Checks indices 0 to 500, finds -1 at index 500 after 501 comparisons.
- Binary Search:
- Main Method: Tests sizes 1000, 10000 with best, average, worst cases, averaging time and comparisons over 100 runs.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| binarySearch | O(log n) | O(1) |
| linearSearch | O(n) worst, O(1) best | O(1) |
| generateSortedArray | O(n log n) | O(n) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(log n) for binarySearch; O(n) for linearSearch in worst/average cases, O(1) in best case; O(n log n) for generateSortedArray; O(n) for toString.
- Space complexity: O(1) for binarySearch and linearSearch; O(n) for generateSortedArray and toString.
- Binary Search scales logarithmically, making it far more efficient for large arrays.
✅ Tip: Binary Search is ideal for large sorted arrays due to its O(log n) complexity, while Linear Search is better for small or unsorted arrays. Use multiple runs to reduce timing variability.
⚠ Warning: Binary Search requires a sorted array to function correctly. Ensure the input array is sorted, or results will be incorrect. Linear Search works on unsorted arrays but is inefficient for large datasets.
Recursive Binary Search Implementation
Problem Statement
Write a Java program that implements a recursive version of the Binary Search algorithm to find a target integer in a sorted array of integers in ascending order, and compare its performance with the iterative Binary Search implementation. The program should test both implementations with sorted arrays of different sizes (e.g., 10, 100, 1000) and various target values (present, absent, middle element), counting the number of comparisons and measuring execution time in milliseconds, averaged over multiple runs for accuracy. Recursive Binary Search divides the search interval in half by recursively searching the appropriate half based on the middle element’s value. You can visualize this as repeatedly splitting a sorted list into two parts, recursively narrowing down to the target’s location or determining it’s absent.
Input:
- A sorted array of integers (ascending order) and a target integer to find. Output: The index of the target (or -1 if not found), number of comparisons, execution time (in milliseconds) for both recursive and iterative implementations, and a string representation of the input array for verification. Constraints:
- The array length
nis between 0 and 10^5. - Array elements and target are integers in the range [-10^9, 10^9].
- The input array is sorted in ascending order. Example:
- Input: array = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19], target = 7
- Output (example, times vary):
- Input Array: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
- Target: 7
- Recursive: Index: 3, Comparisons: 2, Time: 0.02 ms
- Iterative: Index: 3, Comparisons: 2, Time: 0.01 ms
- Explanation: Both implementations find 7 at index 3 after ~2 comparisons, with iterative typically slightly faster due to lower overhead.
Pseudocode
FUNCTION recursiveBinarySearch(arr, target, left, right, comparisons)
IF left > right THEN
RETURN -1, comparisons
ENDIF
SET mid to floor((left + right) / 2)
INCREMENT comparisons
IF arr[mid] equals target THEN
RETURN mid, comparisons
ELSE IF arr[mid] < target THEN
RETURN recursiveBinarySearch(arr, target, mid + 1, right, comparisons)
ELSE
RETURN recursiveBinarySearch(arr, target, left, mid - 1, comparisons)
ENDIF
ENDFUNCTION
FUNCTION iterativeBinarySearch(arr, target)
SET comparisons to 0
SET left to 0
SET right to length of arr - 1
WHILE left <= right
SET mid to floor((left + right) / 2)
INCREMENT comparisons
IF arr[mid] equals target THEN
RETURN mid, comparisons
ELSE IF arr[mid] < target THEN
SET left to mid + 1
ELSE
SET right to mid - 1
ENDIF
ENDWHILE
RETURN -1, comparisons
ENDFUNCTION
FUNCTION generateArray(n)
CREATE arr as array of size n
FOR i from 0 to n-1
SET arr[i] to random integer in [-10^9, 10^9]
ENDFOR
SORT arr in ascending order
RETURN arr
ENDFUNCTION
FUNCTION toString(arr)
CREATE result as new StringBuilder
APPEND "[" to result
FOR each element in arr
APPEND element to result
IF element is not last THEN
APPEND ", " to result
ENDIF
ENDFOR
APPEND "]" to result
RETURN result as string
ENDFUNCTION
FUNCTION main()
SET sizes to [10, 100, 1000]
SET runs to 10
FOR each size in sizes
SET arr to generateArray(size)
SET testCases to array of targets for present, absent, middle
FOR each target in testCases
SET recursiveTotalTime to 0
SET recursiveTotalComparisons to 0
SET iterativeTotalTime to 0
SET iterativeTotalComparisons to 0
FOR i from 0 to runs-1
SET copy to arr.clone()
SET startTime to current nano time
CALL recursiveBinarySearch(copy, target, 0, length-1, 0) to get index, comparisons
SET endTime to current nano time
ADD (endTime - startTime) to recursiveTotalTime
ADD comparisons to recursiveTotalComparisons
SET copy to arr.clone()
SET startTime to current nano time
CALL iterativeBinarySearch(copy, target) to get index, comparisons
SET endTime to current nano time
ADD (endTime - startTime) to iterativeTotalTime
ADD comparisons to iterativeTotalComparisons
ENDFOR
PRINT test case details, input array, indices
PRINT recursive and iterative average time in milliseconds, average comparisons
ENDFOR
ENDFOR
ENDFUNCTION
Algorithm Steps
- Define
recursiveBinarySearch: a. Base case: Ifleft>right, return -1 and comparisons. b. Computemidas the floor of(left + right) / 2. c. Increment comparisons and check ifarr[mid]equals the target. d. If equal, returnmidand comparisons. e. Ifarr[mid]< target, recurse on right half (mid + 1,right). f. Ifarr[mid]> target, recurse on left half (left,mid - 1). - Define
iterativeBinarySearch: a. Initialize comparisons,left, andright. b. Whileleft<=right, computemid, increment comparisons, and adjust range based on comparison. c. Return index and comparisons. - Define
generateArray: a. Create a random array and sort it in ascending order. - Define
toString: a. Convert array to a string, limiting output for large arrays. - In
main, test with: a. Array sizes: 10, 100, 1000 (sorted). b. For each size, test:- Target present in the middle.
- Target absent.
- Target as the middle element. c. Run each case 10 times, averaging execution time and comparisons.
Java Implementation
import java.util.*;
public class BinarySearchRecursiveImplementation {
// Recursive Binary Search with comparison counting
public int[] recursiveBinarySearch(int[] arr, int target, int left, int right, int comparisons) {
if (left > right) {
return new int[]{-1, comparisons};
}
int mid = left + (right - left) / 2; // Avoid overflow
comparisons++;
if (arr[mid] == target) {
return new int[]{mid, comparisons};
} else if (arr[mid] < target) {
return recursiveBinarySearch(arr, target, mid + 1, right, comparisons);
} else {
return recursiveBinarySearch(arr, target, left, mid - 1, comparisons);
}
}
// Iterative Binary Search with comparison counting
public int[] iterativeBinarySearch(int[] arr, int target) {
int comparisons = 0;
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // Avoid overflow
comparisons++;
if (arr[mid] == target) {
return new int[]{mid, comparisons};
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return new int[]{-1, comparisons};
}
// Generates sorted array
private int[] generateSortedArray(int n) {
Random rand = new Random(42); // Fixed seed for reproducibility
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = rand.nextInt(2001) - 1000; // [-1000, 1000]
}
Arrays.sort(arr); // Ensure array is sorted
return arr;
}
// Converts array to string
public String toString(int[] arr) {
StringBuilder result = new StringBuilder("[");
int limit = Math.min(arr.length, 10); // Limit output for large arrays
for (int i = 0; i < limit; i++) {
result.append(arr[i]);
if (i < limit - 1) {
result.append(", ");
}
}
if (arr.length > limit) {
result.append(", ...]");
} else {
result.append("]");
}
return result.toString();
}
// Helper class for test cases
static class TestCase {
int target;
String description;
TestCase(int target, String description) {
this.target = target;
this.description = description;
}
}
// Main method to test performance
public static void main(String[] args) {
BinarySearchRecursiveImplementation searcher = new BinarySearchRecursiveImplementation();
int[] sizes = {10, 100, 1000};
int runs = 10;
// Run test cases
for (int size : sizes) {
System.out.println("Array Size: " + size);
int[] arr = searcher.generateSortedArray(size);
System.out.println("Input Array: " + searcher.toString(arr));
TestCase[] testCases = {
new TestCase(arr[size / 2], "Target present (middle)"),
new TestCase(1000000, "Target absent"),
new TestCase(arr[(size - 1) / 2], "Target middle element")
};
for (TestCase testCase : testCases) {
System.out.println(testCase.description + ":");
System.out.println("Target: " + testCase.target);
long recursiveTotalTime = 0;
long recursiveTotalComparisons = 0;
long iterativeTotalTime = 0;
long iterativeTotalComparisons = 0;
int recursiveIndex = -1;
int iterativeIndex = -1;
for (int i = 0; i < runs; i++) {
int[] copy = arr.clone();
long startTime = System.nanoTime();
int[] recursiveResult = searcher.recursiveBinarySearch(copy, testCase.target, 0, copy.length - 1, 0);
long endTime = System.nanoTime();
recursiveTotalTime += (endTime - startTime);
recursiveTotalComparisons += recursiveResult[1];
recursiveIndex = recursiveResult[0];
copy = arr.clone();
startTime = System.nanoTime();
int[] iterativeResult = searcher.iterativeBinarySearch(copy, testCase.target);
endTime = System.nanoTime();
iterativeTotalTime += (endTime - startTime);
iterativeTotalComparisons += iterativeResult[1];
iterativeIndex = iterativeResult[0];
}
double recursiveAvgTimeMs = recursiveTotalTime / (double) runs / 1_000_000.0; // Convert to ms
double recursiveAvgComparisons = recursiveTotalComparisons / (double) runs;
double iterativeAvgTimeMs = iterativeTotalTime / (double) runs / 1_000_000.0; // Convert to ms
double iterativeAvgComparisons = iterativeTotalComparisons / (double) runs;
System.out.println("Recursive Binary Search:");
System.out.println(" Index: " + recursiveIndex);
System.out.printf(" Average Time: %.2f ms\n", recursiveAvgTimeMs);
System.out.printf(" Average Comparisons: %.0f\n", recursiveAvgComparisons);
System.out.println("Iterative Binary Search:");
System.out.println(" Index: " + iterativeIndex);
System.out.printf(" Average Time: %.2f ms\n", iterativeAvgTimeMs);
System.out.printf(" Average Comparisons: %.0f\n\n", iterativeAvgComparisons);
}
}
}
}
Output
Running the main method produces (times vary by system, example shown):
Array Size: 10
Input Array: [-766, -628, -333, 289, 304, 360, 374, 648, 727, 767]
Target present (middle):
Target: 360
Recursive Binary Search:
Index: 5
Average Time: 0.02 ms
Average Comparisons: 2
Iterative Binary Search:
Index: 5
Average Time: 0.01 ms
Average Comparisons: 2
Target absent:
Target: 1000000
Recursive Binary Search:
Index: -1
Average Time: 0.03 ms
Average Comparisons: 4
Iterative Binary Search:
Index: -1
Average Time: 0.02 ms
Average Comparisons: 4
Target middle element:
Target: 304
Recursive Binary Search:
Index: 4
Average Time: 0.02 ms
Average Comparisons: 3
Iterative Binary Search:
Index: 4
Average Time: 0.01 ms
Average Comparisons: 3
Array Size: 100
Input Array: [-1000, -996, -995, -994, -987, -986, -985, -984, -983, -982, ...]
Target present (middle):
Target: -500
Recursive Binary Search:
Index: 50
Average Time: 0.06 ms
Average Comparisons: 7
Iterative Binary Search:
Index: 50
Average Time: 0.04 ms
Average Comparisons: 7
Target absent:
Target: 1000000
Recursive Binary Search:
Index: -1
Average Time: 0.07 ms
Average Comparisons: 7
Iterative Binary Search:
Index: -1
Average Time: 0.05 ms
Average Comparisons: 7
Target middle element:
Target: -500
Recursive Binary Search:
Index: 50
Average Time: 0.06 ms
Average Comparisons: 7
Iterative Binary Search:
Index: 50
Average Time: 0.04 ms
Average Comparisons: 7
Array Size: 1000
Input Array: [-1000, -996, -995, -994, -987, -986, -985, -984, -983, -982, ...]
Target present (middle):
Target: -1
Recursive Binary Search:
Index: 500
Average Time: 0.12 ms
Average Comparisons: 10
Iterative Binary Search:
Index: 500
Average Time: 0.08 ms
Average Comparisons: 10
Target absent:
Target: 1000000
Recursive Binary Search:
Index: -1
Average Time: 0.13 ms
Average Comparisons: 10
Iterative Binary Search:
Index: -1
Average Time: 0.09 ms
Average Comparisons: 10
Target middle element:
Target: -1
Recursive Binary Search:
Index: 500
Average Time: 0.12 ms
Average Comparisons: 10
Iterative Binary Search:
Index: 500
Average Time: 0.08 ms
Average Comparisons: 10
Explanation:
- Size 10: Both find middle target in ~2-3 comparisons, absent in ~4; iterative is slightly faster (~0.01-0.02 ms vs. 0.02-0.03 ms).
- Size 100: Both find middle target in ~7 comparisons, absent in ~7; iterative is faster (~0.04-0.05 ms vs. 0.06-0.07 ms).
- Size 1000: Both find middle target in ~10 comparisons, absent in ~10; iterative is faster (~0.08-0.09 ms vs. 0.12-0.13 ms).
- Recursive has higher overhead due to call stack; comparisons are identical.
How It Works
- recursiveBinarySearch:
- Recursively narrows the search range by computing
midand comparingarr[mid]to the target. - Increments comparisons and returns
[index, comparisons]or recurses on the appropriate half.
- Recursively narrows the search range by computing
- iterativeBinarySearch:
- Uses a loop to narrow the range, with identical logic to recursive but without call stack overhead.
- generateSortedArray: Creates a random sorted array with a fixed seed.
- toString: Formats array, limiting to 10 elements.
- Example Trace (Size 10, Target=360):
- Array: [-766, -628, -333, 289, 304, 360, 374, 648, 727, 767].
- Recursive: left=0, right=9, mid=4, arr[4]=304 < 360, comparisons=1, recurse (left=5, right=9).
- Next: left=5, right=9, mid=7, arr[7]=648 > 360, comparisons=2, recurse (left=5, right=6).
- Next: left=5, right=6, mid=5, arr[5]=360 = 360, comparisons=3, return [5, 3].
- Iterative: Same steps in a loop, returning [5, 3].
- Main Method: Tests sizes 10, 100, 1000 with present, absent, middle targets, averaging time and comparisons over 10 runs.
Complexity Analysis Table
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| recursiveBinarySearch | O(log n) | O(log n) |
| iterativeBinarySearch | O(log n) | O(1) |
| generateSortedArray | O(n log n) | O(n) |
| toString | O(n) | O(n) |
Note:
- n is the array length.
- Time complexity: O(log n) for both searches (halves range each step); O(n log n) for generateSortedArray (sorting); O(n) for toString.
- Space complexity: O(log n) for recursiveBinarySearch (call stack); O(1) for iterativeBinarySearch; O(n) for generateSortedArray and toString.
- Iterative is faster due to no recursion overhead; comparisons are identical.
✅ Tip: Recursive Binary Search is elegant and easier to understand for some, but iterative Binary Search is typically faster due to lower overhead. Use multiple runs to measure performance accurately.
⚠ Warning: Recursive Binary Search uses O(log n) stack space, which can cause stack overflow for very large arrays. Prefer iterative Binary Search for production code to avoid this risk.
Appendix: Glossary of Terms
Introduction
This glossary provides clear, beginner-friendly definitions for key terms used in the "Java Data Structures and Algorithms for Beginners" curriculum, with a focus on concepts from the Binary Search chapter and broader data structures and algorithms (DSA) topics. It serves as a reference to help students understand technical terminology encountered in problem statements, implementations, and complexity analyses. Terms are drawn from the Binary Search exercises, as well as other core data structures (arrays, linked lists, stacks, queues, hashing, trees, graphs), sorting algorithms (Bubble Sort, Selection Sort, Insertion Sort, Merge Sort), searching algorithms (Linear Search, Binary Search), and recursion. This glossary supports the learning platform’s goal of providing accessible, hands-on education with embedded compilers and visualizations.
Glossary of Terms
- Algorithm: A step-by-step procedure or set of rules to solve a problem or perform a task, such as searching or sorting data. Example: Binary Search is an algorithm to find an element in a sorted array.
- Array: A data structure that stores a fixed-size sequence of elements of the same type, accessed by index. Example:
[1, 3, 5, 7]is an array used in Binary Search. - Big-O Notation: A mathematical notation to describe the upper bound of an algorithm’s time or space complexity, indicating its efficiency as input size grows. Example: Binary Search has O(log n) time complexity.
- Binary Search: A divide-and-conquer algorithm that finds an element in a sorted array by repeatedly halving the search interval, comparing the middle element to the target. Example: Finding
7in[1, 3, 5, 7, 9]by checking the middle element. - Complexity: A measure of an algorithm’s performance, typically in terms of time (execution duration) or space (memory usage). Example: Binary Search’s time complexity is logarithmic, O(log n).
- Divide-and-Conquer: An algorithmic paradigm that breaks a problem into smaller subproblems, solves them, and combines results. Binary Search uses this by dividing the array in half.
- Duplicate Handling: Techniques to manage repeated elements in a data structure, such as finding the first and last occurrences of a target in Binary Search. Example: Finding indices [1, 3] for
3in[1, 3, 3, 3, 5]. - Graph: A data structure with nodes (vertices) connected by edges, used to model relationships. Example: A social network where users are nodes and friendships are edges.
- Hashing: A technique to map data to a fixed-size array using a hash function, enabling fast retrieval. Example: A phone book mapping names to numbers.
- Index: A numerical position used to access elements in an array or list. Example: In
[1, 3, 5], the index of5is 2. - Iterative Algorithm: An algorithm that uses loops to repeat steps, avoiding recursion. Example: Iterative Binary Search uses a while loop to halve the search range.
- Linked List: A data structure where elements (nodes) are linked by pointers, allowing dynamic size changes. Example: A playlist where each song points to the next.
- Logarithmic Time: Time complexity where the running time grows logarithmically with input size, common in Binary Search. Example: O(log n) for searching in a sorted array of size n.
- Queue: A data structure that follows First-In-First-Out (FIFO) order, where elements are added at the rear and removed from the front. Example: A print job queue.
- Recursion: A process where a function calls itself to solve smaller instances of a problem. Example: Recursive Binary Search divides the array and calls itself on a smaller range.
- Sorted Array: An array where elements are arranged in order (e.g., ascending or descending), a prerequisite for Binary Search. Example:
[1, 3, 5, 7, 9]is sorted in ascending order. - Space Complexity: The amount of memory an algorithm uses relative to input size. Example: Iterative Binary Search has O(1) space complexity, while recursive uses O(log n) due to the call stack.
- Stack: A data structure that follows Last-In-First-Out (LIFO) order, where elements are added and removed from the top. Example: A browser’s back button history.
- Time Complexity: The amount of time an algorithm takes to run relative to input size. Example: Binary Search’s O(log n) vs. Linear Search’s O(n).
- Tree: A hierarchical data structure with nodes connected by edges, where each node has a parent and children. Example: A binary search tree for efficient searches.
✅ Tip or Warning Box
✅ Tip: Use this glossary as a quick reference when working through Binary Search exercises or other DSA topics. Understanding these terms will help you grasp problem statements, implementations, and complexity analyses more effectively.
⚠ Warning: Ensure you understand the context of each term, as some (e.g., complexity) apply differently across algorithms. For example, Binary Search requires a sorted array, unlike Linear Search, which affects their use cases.
Appendix: Pseudocode Symbols and Conventions
Introduction
This appendix provides a comprehensive reference for the pseudocode symbols and conventions used in the "Java Data Structures and Algorithms for Beginners" curriculum, particularly in the Binary Search chapter and other data structures and algorithms (DSA) exercises. Pseudocode is a high-level, language-agnostic representation of an algorithm’s logic, designed to be readable and understandable without requiring specific programming language syntax. The conventions outlined here ensure consistency across problem statements, enabling students to focus on algorithmic logic before implementing solutions in Java. This appendix details the keywords, symbols, and formatting used in pseudocode, with examples drawn from Binary Search exercises to illustrate their application. It supports the learning platform’s goal of providing accessible, hands-on education with embedded compilers and visualizations by clarifying how to interpret and translate pseudocode into code.
Pseudocode Symbols and Conventions
The following table and descriptions outline the standardized pseudocode symbols, keywords, and conventions used throughout the curriculum. These are designed to be intuitive for beginners while maintaining clarity and consistency.
| Symbol/Keyword | Description | Example |
|---|---|---|
FUNCTION | Declares the start of a function or procedure, followed by the function name and parameters. | FUNCTION binarySearch(arr, target) defines a Binary Search function taking an array and target value. |
ENDFUNCTION | Marks the end of a function definition. | ENDFUNCTION closes the binarySearch function. |
SET | Assigns a value to a variable or updates a variable’s value. | SET left to 0 initializes the left pointer to 0 in Binary Search. |
RETURN | Specifies the output of a function and terminates its execution. | RETURN mid, comparisons returns the index and comparison count in Binary Search. |
IF, ELSE IF, ELSE, ENDIF | Conditional statements for decision-making. | IF arr[mid] equals target THEN RETURN mid, comparisons ENDIF checks if the middle element is the target. |
WHILE, ENDWHILE | Defines a loop that continues as long as a condition is true. | WHILE left <= right loops until the search range is invalid in Binary Search. |
FOR, ENDFOR | Iterates over a range or sequence of values. | FOR i from 0 to n-1 iterates through an array to generate test data. |
CREATE | Initializes a new data structure or object. | CREATE arr as array of size n creates an array for testing. |
INCREMENT | Increases a variable’s value by 1. | INCREMENT comparisons tracks the number of comparisons in Binary Search. |
floor() | Rounds a number down to the nearest integer. | SET mid to floor((left + right) / 2) calculates the middle index. |
length of | Returns the size or number of elements in a data structure. | SET right to length of arr - 1 sets the right pointer to the last index. |
APPEND | Adds an element or string to a data structure, such as a StringBuilder. | APPEND "[" to result adds an opening bracket to a string representation. |
equals | Checks for equality between two values. | IF arr[mid] equals target THEN compares the middle element to the target. |
// | Indicates a comment explaining the pseudocode. | // Avoid overflow explains the use of left + (right - left) / 2. |
| Indentation | Used to denote the scope or body of functions, loops, and conditionals. | Indented lines under WHILE indicate the loop’s body. |
THEN | Marks the action to take if a condition is true in an IF statement. | IF arr[mid] < target THEN SET left to mid + 1 updates the left pointer. |
Additional Conventions
- Case Sensitivity: Keywords are written in uppercase (e.g.,
FUNCTION,SET) to distinguish them from variables and improve readability. - Variable Naming: Variables use descriptive, lowercase names (e.g.,
left,right,mid) to clearly indicate their purpose. - Data Structures: Arrays, lists, or objects are referenced generically (e.g.,
arrfor an array) to focus on logic rather than language-specific syntax. - Clarity Over Conciseness: Pseudocode prioritizes readability, using full words like
equalsinstead of symbols like==to avoid confusion for beginners. - Consistency: The same keywords and structure are used across all exercises, such as Binary Search, Linear Search, and sorting algorithms, to ensure a uniform learning experience.
Example from Binary Search
The following pseudocode from the Basic Binary Search exercise (artifact_id="850a19d5-b136-4ecc-9dc6-53e9a8fcc6bc") illustrates these conventions:
FUNCTION binarySearch(arr, target)
SET comparisons to 0
SET left to 0
SET right to length of arr - 1
WHILE left <= right
SET mid to floor((left + right) / 2) // Avoid overflow
INCREMENT comparisons
IF arr[mid] equals target THEN
RETURN mid, comparisons
ELSE IF arr[mid] < target THEN
SET left to mid + 1
ELSE
SET right to mid - 1
ENDIF
ENDWHILE
RETURN -1, comparisons
ENDFUNCTION
- Explanation: This pseudocode uses
FUNCTIONto define the search,SETfor variable assignments,WHILEfor looping,IF/ELSE IF/ELSEfor conditionals, andRETURNfor output. Indentation clarifies the scope, and//comments explain key steps.
✅ Tip or Warning Box
✅ Tip: Familiarize yourself with these pseudocode conventions before tackling DSA exercises. They provide a clear, language-agnostic way to understand algorithms like Binary Search, making it easier to translate logic into Java code.
⚠ Warning: Ensure you follow the indentation and keyword conventions (e.g., FUNCTION, SET) exactly as shown, as they indicate the structure and flow of the algorithm. Misinterpreting symbols like floor() or length of may lead to errors when implementing the code.
Appendix: Java Code Templates
Introduction
This appendix provides reusable Java code templates for common data structures and algorithms (DSAs) covered in the "Java Data Structures and Algorithms for Beginners" curriculum, with a focus on the Binary Search chapter and other core topics such as arrays, linked lists, stacks, queues, hashing, trees, graphs, and sorting and searching algorithms. These templates are designed to be beginner-friendly, modular, and adaptable for use in the problem statements and exercises, enabling students to quickly implement and test solutions. Each template includes comments for clarity and usage notes to guide implementation, aligning with the learning platform’s goal of providing hands-on education with embedded compilers and visualizations. By using these templates, students can focus on understanding algorithmic logic and adapting code to specific problem requirements, such as those in the Binary Search exercises.
Java Code Templates
Below are Java code templates for key data structures and algorithms, with examples drawn from the Binary Search chapter and other curriculum topics. Each template is complete, commented, and ready to be adapted for specific use cases.
1. Iterative Binary Search
Description: A template for iterative Binary Search to find a target integer in a sorted array, returning the index or -1 if not found. Used in Basic Binary Search (artifact_id="850a19d5-b136-4ecc-9dc6-53e9a8fcc6bc") and Performance Analysis (artifact_id="4b5c6d7e-8f9a-4b0c-9d1e-3f4a5b6c7d8e").
Usage Notes: Ensure the input array is sorted in ascending order. Modify to return additional data (e.g., comparisons) or adapt for objects by changing the comparison logic.
public class BinarySearch {
// Iterative Binary Search for a target in a sorted array
public int binarySearch(int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // Avoid overflow
if (arr[mid] == target) {
return mid; // Target found
} else if (arr[mid] < target) {
left = mid + 1; // Search right half
} else {
right = mid - 1; // Search left half
}
}
return -1; // Target not found
}
// Example usage
public static void main(String[] args) {
BinarySearch searcher = new BinarySearch();
int[] arr = {1, 3, 5, 7, 9}; // Sorted array
int target = 7;
int result = searcher.binarySearch(arr, target);
System.out.println("Index of " + target + ": " + result); // Output: Index of 7: 3
}
}
2. Recursive Binary Search
Description: A template for recursive Binary Search, used in Recursive Implementation (artifact_id="311f97d4-8924-4798-9267-d8e029502590"). Calls itself on a smaller range until the target is found or the range is invalid.
Usage Notes: Ensure the array is sorted. Add parameters for tracking comparisons or modify for object searches.
public class RecursiveBinarySearch {
// Recursive Binary Search
public int binarySearch(int[] arr, int target, int left, int right) {
if (left > right) {
return -1; // Target not found
}
int mid = left + (right - left) / 2; // Avoid overflow
if (arr[mid] == target) {
return mid; // Target found
} else if (arr[mid] < target) {
return binarySearch(arr, target, mid + 1, right); // Search right half
} else {
return binarySearch(arr, target, left, mid - 1); // Search left half
}
}
// Wrapper method for easier calling
public int binarySearch(int[] arr, int target) {
return binarySearch(arr, target, 0, arr.length - 1);
}
// Example usage
public static void main(String[] args) {
RecursiveBinarySearch searcher = new RecursiveBinarySearch();
int[] arr = {1, 3, 5, 7, 9}; // Sorted array
int target = 7;
int result = searcher.binarySearch(arr, target);
System.out.println("Index of " + target + ": " + result); // Output: Index of 7: 3
}
}
3. Linear Search
Description: A template for Linear Search to find a target integer in an array, used in Linear Search exercises (ch05_01). Iterates through the array sequentially.
Usage Notes: Works on sorted or unsorted arrays. Can be modified to count comparisons or find multiple occurrences.
public class LinearSearch {
// Linear Search for a target in an array
public int linearSearch(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i; // Target found
}
}
return -1; // Target not found
}
// Example usage
public static void main(String[] args) {
LinearSearch searcher = new LinearSearch();
int[] arr = {4, 2, 7, 1, 9};
int target = 7;
int result = searcher.linearSearch(arr, target);
System.out.println("Index of " + target + ": " + result); // Output: Index of 7: 2
}
}
4. Array
Description: A template for creating and manipulating arrays, used in Array exercises (ch03_01). Supports initialization and basic operations.
Usage Notes: Adapt for specific tasks like reversal or sorting. Ensure bounds checking to avoid errors.
public class ArrayOperations {
// Create and initialize an array
public int[] createArray(int size) {
int[] arr = new int[size];
for (int i = 0; i < size; i++) {
arr[i] = i + 1; // Example: Fill with 1, 2, ..., size
}
return arr;
}
// Print array
public void printArray(int[] arr) {
System.out.print("[");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
if (i < arr.length - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
// Example usage
public static void main(String[] args) {
ArrayOperations ops = new ArrayOperations();
int[] arr = ops.createArray(5);
ops.printArray(arr); // Output: [1, 2, 3, 4, 5]
}
}
5. Linked List
Description: A template for a singly linked list, used in Linked List exercises (ch03_03). Includes node structure and basic operations.
Usage Notes: Extend for doubly or circular linked lists. Add methods for specific tasks like reversal or cycle detection.
public class LinkedList {
// Node class
class Node {
int data;
Node next;
Node(int data) {
this.data = data;
this.next = null;
}
}
private Node head;
// Insert at the end
public void insert(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode;
} else {
Node current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}
}
// Print list
public void printList() {
Node current = head;
System.out.print("[");
while (current != null) {
System.out.print(current.data);
if (current.next != null) {
System.out.print(", ");
}
current = current.next;
}
System.out.println("]");
}
// Example usage
public static void main(String[] args) {
LinkedList list = new LinkedList();
list.insert(1);
list.insert(2);
list.insert(3);
list.printList(); // Output: [1, 2, 3]
}
}
6. Stack
Description: A template for a stack using an array, used in Stack exercises (ch03_04). Follows Last-In-First-Out (LIFO) order.
Usage Notes: Adjust capacity for dynamic resizing or use a linked list for flexibility.
public class Stack {
private int[] arr;
private int top;
private int capacity;
// Initialize stack
public Stack(int size) {
arr = new int[size];
capacity = size;
top = -1;
}
// Push element
public void push(int data) {
if (top >= capacity - 1) {
System.out.println("Stack Overflow");
return;
}
arr[++top] = data;
}
// Pop element
public int pop() {
if (top < 0) {
System.out.println("Stack Underflow");
return -1;
}
return arr[top--];
}
// Example usage
public static void main(String[] args) {
Stack stack = new Stack(5);
stack.push(1);
stack.push(2);
System.out.println("Popped: " + stack.pop()); // Output: Popped: 2
}
}
7. Queue
Description: A template for a queue using an array, used in Queue exercises (ch03_05). Follows First-In-First-Out (FIFO) order.
Usage Notes: Modify for circular queues or priority queues. Add bounds checking for robustness.
public class Queue {
private int[] arr;
private int front;
private int rear;
private int capacity;
// Initialize queue
public Queue(int size) {
arr = new int[size];
capacity = size;
front = 0;
rear = -1;
}
// Enqueue element
public void enqueue(int data) {
if (rear >= capacity - 1) {
System.out.println("Queue Full");
return;
}
arr[++rear] = data;
}
// Dequeue element
public int dequeue() {
if (front > rear) {
System.out.println("Queue Empty");
return -1;
}
return arr[front++];
}
// Example usage
public static void main(String[] args) {
Queue queue = new Queue(5);
queue.enqueue(1);
queue.enqueue(2);
System.out.println("Dequeued: " + queue.dequeue()); // Output: Dequeued: 1
}
}
8. Bubble Sort
Description: A template for Bubble Sort, used in Bubble Sort exercises (ch04_01). Repeatedly swaps adjacent elements if out of order.
Usage Notes: Optimize with a flag for early termination or adapt for descending order.
public class BubbleSort {
// Bubble Sort for ascending order
public void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// Swap elements
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// Example usage
public static void main(String[] args) {
BubbleSort sorter = new BubbleSort();
int[] arr = {5, 3, 8, 1, 2};
sorter.bubbleSort(arr);
System.out.print("Sorted: [");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
if (i < arr.length - 1) {
System.out.print(", ");
}
}
System.out.println("]"); // Output: Sorted: [1, 2, 3, 5, 8]
}
}
✅ Tip or Warning Box
✅ Tip: Use these templates as starting points for DSA exercises. Modify them to fit specific problem requirements, such as adding comparison counters for Binary Search or implementing custom object sorting. Test each template with the provided main method to ensure correctness.
⚠ Warning: Always verify input constraints (e.g., sorted arrays for Binary Search, array bounds for stacks/queues) before using these templates. Incorrect usage, such as applying Binary Search to an unsorted array, will produce invalid results.