Data Structures & Algorithms

Comprehensive Guide for IT Students and Freshers

PrepCampus • Your Learning Partner

1. Arrays

1.1 Introduction

An Array is a fundamental data structure that stores a collection of elements in a contiguous block of memory. Think of it as a linear collection where each element has a unique position called an index.

Key Characteristics:

  • Homogeneous: All elements in an array are of the same data type (all integers, all floats, all characters, etc.)
  • Fixed Size: Once declared, the size of an array remains constant (in most programming languages)
  • Indexed Access: Each element can be accessed directly using its position number (index)
  • Sequential Storage: Elements are stored in consecutive memory locations for efficient access
  • Random Access: You can access any element directly without traversing through others

Memory Layout: Arrays use contiguous memory allocation, meaning all elements are stored next to each other in memory. This allows the computer to calculate the exact memory address of any element using a simple formula.

Memory Trick: Think of an array like a row of houses with house numbers (indexes). All houses are next to each other, and each house stores one thing.

In programming, each element of an array can be accessed using an index (also called a subscript).

  • Index starts from 0 in most languages (C, C++, Java, Python).
  • First element = index 0, last element = index size - 1.

1.2 Why use Arrays?

Arrays are one of the most fundamental and widely used data structures in programming. Here's why they are essential:

  • Efficient Storage: Store multiple values of the same type under a single variable name, reducing memory overhead
  • Fast Access: Direct access to any element using its index (O(1) time complexity)
  • Easy Iteration: Simple loops to process all elements sequentially
  • Memory Efficiency: Contiguous memory allocation reduces fragmentation and improves cache performance
  • Mathematical Operations: Easy to perform operations on collections of numbers (sum, average, sorting, etc.)
  • Data Organization: Keep related data together for better code organization and readability
  • Algorithm Foundation: Many advanced algorithms and data structures are built upon arrays
  • Performance: Excellent cache locality due to contiguous memory storage

Real-World Applications: Arrays are used in databases, image processing, scientific computing, game development, and virtually every programming domain.

1.3 How Arrays Work in Memory

Understanding how arrays work in memory is crucial for optimizing performance and understanding their behavior. Arrays use a contiguous memory allocation strategy, which means all elements are stored in consecutive memory locations.

Memory Allocation Process:

  1. Base Address: The computer allocates a starting memory address for the array
  2. Sequential Storage: Each subsequent element is stored immediately after the previous one
  3. Size Calculation: Total memory = Number of elements × Size of each element
  4. Address Calculation: Any element's address can be calculated using: Base Address + (Index × Element Size)

Example: If the base address of the array is 1000 and each element takes 4 bytes (integer), then the memory layout would be:

Index Value Address
0101000
1201004
2301008
3401012

Why This Matters: This memory layout is why accessing arr[2] is extremely fast — the computer directly calculates its memory address using the formula:

Address = Base Address + (Index × Size of each element)

Performance Benefits:

  • O(1) Access Time: Direct calculation without searching or traversing
  • Cache Efficiency: Contiguous memory improves CPU cache performance
  • Predictable Performance: Access time is consistent regardless of array size
  • Memory Locality: Related data is stored together, reducing cache misses

1.4 Array Declaration and Initialization

Arrays can be declared and initialized in various ways depending on the programming language and your specific needs. Understanding these different methods helps you choose the most appropriate approach for your use case.


    // 1. Declaration with size
    int marks[5]; // Creates an array of 5 integers
    
    // 2. Declaration + Initialization
    int marks[5] = {10, 20, 30, 40, 50};
    
    // 3. Initialization without size
    int marks[] = {10, 20, 30, 40, 50}; 
    // Compiler counts size automatically
    
    // 4. Accessing elements
    printf("%d", marks[0]); // prints 10
    printf("%d", marks[4]); // prints 50
    
    // 5. Updating an element
    marks[2] = 99; 
    // Now marks = {10, 20, 99, 40, 50}
    

Explanation: This example demonstrates the fundamental ways to work with arrays in C/C++. It shows declaration, initialization, element access, and modification. Arrays provide O(1) random access using indices.

Key Points:

  • Declaration: Specifies the data type and size
  • Initialization: Can be done at declaration or separately
  • Zero-based Indexing: Array indices start from 0
  • Bounds Checking: C/C++ doesn't perform automatic bounds checking

1.5 Traversing an Array

Array Traversal is the process of visiting each element of the array one by one, typically in a systematic order. This is one of the most fundamental operations performed on arrays and is essential for many algorithms and data processing tasks.

Common Traversal Patterns:

  • Forward Traversal: Visit elements from index 0 to n-1
  • Backward Traversal: Visit elements from index n-1 to 0
  • Partial Traversal: Visit only a subset of elements
  • Conditional Traversal: Visit elements based on certain conditions

    for (int i = 0; i < 5; i++) {
        printf("%d ", marks[i]);
    }
    

Explanation: This code demonstrates forward array traversal using a for loop. It visits each element sequentially from index 0 to the end of the array.

Time Complexity: O(n) where n is the array size | Space Complexity: O(1) as no extra space is used

Key Points:

  • Loop Control: Uses counter variable 'i' to track current position
  • Condition: Continues until 'i' reaches the array size
  • Increment: 'i++' moves to the next element
  • Access Pattern: Sequential access is cache-friendly
Output: 10 20 99 40 50

1.6 Common Operations & Time Complexity Analysis

Understanding the time complexity of array operations is crucial for algorithm design and performance optimization. Different operations have different efficiency characteristics, and knowing these helps you choose the right approach for your specific problem.

Operation Time Complexity Explanation
Access (by index) O(1) Direct jump using formula.
Search (Linear) O(n) Check each element until found.
Insertion (end) O(1) Place at next empty position.
Insertion (middle) O(n) Shift elements to make space.
Deletion (middle) O(n) Shift elements to fill the gap.

1.7 Advantages of Arrays

Arrays offer several significant advantages that make them the foundation of many data structures and algorithms:

  • Fast Random Access: O(1) time complexity for accessing any element using its index
  • Memory Efficiency: Contiguous memory allocation reduces overhead and improves cache performance
  • Simple Implementation: Easy to understand, implement, and debug
  • Predictable Performance: Consistent access times regardless of data size
  • Cache Locality: Excellent CPU cache performance due to contiguous storage
  • Foundation for Other Structures: Many advanced data structures are built upon arrays
  • Efficient Iteration: Fast sequential access for processing all elements
  • Mathematical Operations: Easy to perform vector and matrix operations
  • Memory Predictability: Fixed size allows for precise memory allocation
  • Language Support: Native support in all programming languages

1.8 Disadvantages of Arrays

While arrays are powerful and efficient, they also have several limitations that make them unsuitable for certain applications:

  • Fixed Size: Cannot grow or shrink at runtime in most programming languages (except dynamic arrays)
  • Expensive Insertions/Deletions: Inserting or deleting elements in the middle requires shifting all subsequent elements (O(n) time complexity)
  • Memory Wastage: May allocate more memory than needed if size is overestimated
  • Homogeneous Data: All elements must be of the same data type, limiting flexibility
  • Memory Fragmentation: Large arrays may cause memory fragmentation issues
  • No Built-in Bounds Checking: In languages like C/C++, accessing invalid indices can cause buffer overflows
  • Inefficient for Dynamic Data: Poor performance when data size changes frequently
  • Memory Allocation Issues: May fail to allocate memory for very large arrays
  • Limited Flexibility: Cannot easily represent hierarchical or complex relationships
  • Search Complexity: Linear search is O(n) unless the array is sorted

1.9 Real-Life Applications and Examples

Arrays are used extensively in real-world applications across various domains. Understanding these applications helps you see the practical importance of arrays in solving real problems.

Everyday Examples:

  • Temperature Records: Daily temperatures for a week, monthly averages, yearly climate data
  • Academic Records: Student marks in exams, attendance records, grade point averages
  • Seating Arrangements: Theater seats, classroom seating, stadium seating
  • Contact Lists: Phone numbers, email addresses, contact information
  • Inventory Management: Product quantities, stock levels, item codes

Technical Applications:

  • Image Processing: Pixel arrays, color channels, image filters
  • Audio Processing: Sound samples, frequency data, audio buffers
  • Game Development: Game board states, player positions, score tracking
  • Database Systems: Record storage, index structures, query results
  • Scientific Computing: Mathematical vectors, matrices, statistical data
  • Web Development: Form data, API responses, user sessions
  • Financial Applications: Stock prices, transaction history, portfolio data
  • Machine Learning: Feature vectors, training data, model parameters
Exam Tip: Remember — Array is best when:
  • You know the size in advance.
  • You need fast access by position.
  • You don't insert/delete much in the middle.

2. Strings

2.1 Introduction to Strings

A String is a fundamental data structure that represents a sequence of characters. It is one of the most commonly used data types in programming, essential for text processing, data manipulation, and user interface development.

Key Characteristics:

  • Character Sequence: Ordered collection of characters (letters, numbers, symbols, spaces)
  • Immutable Nature: In most programming languages, strings cannot be modified once created (new strings are created instead)
  • Indexed Access: Each character can be accessed using its position (index) in the string
  • Length Property: Strings have a defined length (number of characters)
  • Unicode Support: Modern strings support international characters and symbols
  • Text Processing: Built-in methods for searching, replacing, and manipulating text

Importance in Programming: Strings are fundamental for file I/O, user input processing, data parsing, web development, database operations, and virtually every application that deals with human-readable text.

Key Concept: Strings are essentially arrays of characters, but with special properties and built-in methods for text manipulation.

2.2 String Representation in Memory

Understanding how strings are represented in memory is crucial for optimizing string operations and understanding their performance characteristics. Different programming languages and systems use various approaches to store string data.

Common String Representation Methods:

  • Character Array: Each character stored in consecutive memory locations, similar to arrays
  • Null-terminated Strings: C-style strings that end with a special null character ('\0') to mark the end
  • Length-prefixed Strings: Store the string length at the beginning, followed by the character data
  • Object-based Strings: Modern languages treat strings as objects with metadata and methods
  • Rope Data Structure: Used for very large strings, breaking them into smaller pieces
  • String Interning: Shared storage for identical strings to save memory

Memory Considerations: String representation affects memory usage, access speed, and the efficiency of operations like concatenation, substring extraction, and comparison.


     // String representation in different languages
     // C/C++ - Character array
     char str[] = "Hello World";
     char* str2 = "Hello World";
     
     // Java - String object
     String str = "Hello World";
     
     // Python - String object
     str = "Hello World"
     
     // JavaScript
     let str = "Hello World";
     

Explanation: This example shows how strings are represented differently across programming languages, each with their own memory management and immutability characteristics.

Key Differences:

  • C/C++: Character arrays with null termination, mutable, manual memory management
  • Java: Immutable objects with automatic memory management via garbage collection
  • Python: Immutable objects with automatic memory management
  • JavaScript: Primitive type with automatic memory management

Memory Implications: C/C++ strings are memory-efficient but require careful management, while higher-level languages provide safety at the cost of some overhead.

2.3 Common String Operations

2.3.1 Basic Operations

Operation Time Complexity Description
Length O(1) Get string length
Access O(1) Access character at index
Concatenation O(n+m) Join two strings
Substring O(k) Extract substring of length k

2.3.2 String Manipulation


     // Common string operations
     String str = "Hello World";
     
     // Length
     int len = str.length();  // 11
     
     // Access character
     char ch = str.charAt(0);  // 'H'
     
     // Substring
     String sub = str.substring(0, 5);  // "Hello"
     
     // Concatenation
     String result = str + "!";  // "Hello World!"
     
     // Case conversion
     String upper = str.toUpperCase();  // "HELLO WORLD"
     // "hello world"
     
     // Replace
     String replaced = str.replace("World", "Java");  // "Hello Java"
     

Explanation: This example demonstrates fundamental string operations available in most programming languages. These operations form the basis for string processing and manipulation.

Key Operations:

  • Length: O(1) operation to get string size
  • Character Access: O(1) random access to any character
  • Substring: O(k) where k is substring length
  • Concatenation: O(n+m) where n and m are string lengths
  • Case Conversion: O(n) linear time operation
  • Replace: O(n) operation that scans the entire string

Memory Note: String operations often create new string objects, so be mindful of memory usage in performance-critical applications.

2.4 String Algorithms

2.4.1 String Reversal

Reverse a string using different approaches:


     // Method 1: Using two pointers
     public String reverse(String str) {
         char[] chars = str.toCharArray();
         int left = 0, right = chars.length - 1;
         
         while (left < right) {
             char temp = chars[left];
             chars[left] = chars[right];
             chars[right] = temp;
             left++;
             right--;
         }
         
         return new String(chars);
     }
     
     // Method 2: Using StringBuilder
     public String reverse(String str) {
         return new StringBuilder(str).reverse().toString();
     }
     
     // Method 3: Recursive approach
     public String reverse(String str) {
         if (str.length() <= 1) return str;
         return reverse(str.substring(1)) + str.charAt(0);
     }
     

Explanation: This example shows three different approaches to reverse a string, each with different trade-offs in terms of performance, memory usage, and readability.

Method Comparison:

  • Two Pointers: O(n) time, O(n) space, in-place modification, most efficient
  • StringBuilder: O(n) time, O(n) space, clean and readable, Java-specific
  • Recursive: O(n) time, O(n) space, elegant but uses call stack

When to Use:

  • Two Pointers: Performance-critical applications, memory-constrained environments
  • StringBuilder: Java applications where readability is priority
  • Recursive: Educational purposes, functional programming style

2.4.2 Palindrome Check

Check if a string is palindrome (reads same forward and backward):


     public boolean isPalindrome(String str) {
         // Remove non-alphanumeric characters and convert to lowercase
         str = str.replaceAll("[^a-zA-Z0-9]", "").toLowerCase();
         
         int left = 0, right = str.length() - 1;
         
         while (left < right) {
             if (str.charAt(left) != str.charAt(right)) {
                 return false;
             }
             left++;
             right--;
         }
         
         return true;
     }
     
     // Example usage
     System.out.println(isPalindrome("A man, a plan, a canal: Panama")); // true
     System.out.println(isPalindrome("race a car")); // false
     

Explanation: This algorithm checks if a string reads the same forward and backward, ignoring case and non-alphanumeric characters. It uses the two-pointer technique for efficient comparison.

Algorithm Steps:

  • Preprocessing: Remove special characters and convert to lowercase
  • Two Pointers: Start from both ends and move inward
  • Comparison: Compare characters at left and right pointers
  • Early Exit: Return false immediately if characters don't match

Time Complexity: O(n) where n is string length | Space Complexity: O(n) due to string preprocessing

Edge Cases: Handles empty strings, single characters, and strings with special characters correctly.

2.4.3 String Anagrams

Check if two strings are anagrams (contain same characters in different order):


     // Method 1: Using character count array
     public boolean isAnagram(String s, String t) {
         if (s.length() != t.length()) return false;
         
         int[] count = new int[26]; // Assuming lowercase letters only
         
         for (char c : s.toCharArray()) {
             count[c - 'a']++;
         }
         
         for (char c : t.toCharArray()) {
             count[c - 'a']--;
             if (count[c - 'a'] < 0) return false;
         }
         
         return true;
     }
     
     // Method 2: Using sorting
     public boolean isAnagram(String s, String t) {
         if (s.length() != t.length()) return false;
         
         char[] sChars = s.toCharArray();
         char[] tChars = t.toCharArray();
         
         Arrays.sort(sChars);
         Arrays.sort(tChars);
         
         return Arrays.equals(sChars, tChars);
     }
     

Explanation: This example shows two different approaches to check if two strings are anagrams (contain the same characters in different order).

Method 1 - Character Count Array:

  • Approach: Count frequency of each character in both strings
  • Time Complexity: O(n) where n is string length
  • Space Complexity: O(1) since we use fixed-size array
  • Advantage: Most efficient for large strings

Method 2 - Sorting:

  • Approach: Sort both strings and compare
  • Time Complexity: O(n log n) due to sorting
  • Space Complexity: O(n) for temporary arrays
  • Advantage: Simple and easy to understand

When to Use: Use Method 1 for performance-critical applications, Method 2 for simplicity and readability.

2.4.4 Longest Common Subsequence (LCS)

Find the longest subsequence present in both strings (characters need not be consecutive):


     public int longestCommonSubsequence(String text1, String text2) {
         int m = text1.length(), n = text2.length();
         int[][] dp = new int[m + 1][n + 1];
         
         for (int i = 1; i <= m; i++) {
             for (int j = 1; j <= n; j++) {
                 if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                     dp[i][j] = dp[i - 1][j - 1] + 1;
                 } else {
                     dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                 }
             }
         }
         
         return dp[m][n];
     }
     
     // Example
     // text1 = "abcde", text2 = "ace"
     // LCS = "ace" (length 3)
     

Explanation: This algorithm finds the longest subsequence that appears in both strings in the same order (characters need not be consecutive). It uses dynamic programming with a 2D table.

Algorithm Steps:

  • DP Table: Create (m+1) × (n+1) table where dp[i][j] represents LCS of text1[0...i-1] and text2[0...j-1]
  • Base Case: dp[0][j] = 0 and dp[i][0] = 0 for all i, j
  • Recurrence: If characters match, dp[i][j] = dp[i-1][j-1] + 1; else take maximum of dp[i-1][j] and dp[i][j-1]
  • Result: dp[m][n] contains the length of LCS

Time Complexity: O(m×n) where m and n are string lengths | Space Complexity: O(m×n) for the DP table

Applications: DNA sequence analysis, plagiarism detection, version control systems, and text similarity.

2.4.5 String Matching Algorithms

a) Naive String Matching

     public int naiveSearch(String text, String pattern) {
         int n = text.length();
         int m = pattern.length();
         
         for (int i = 0; i <= n - m; i++) {
             int j;
             for (j = 0; j < m; j++) {
                 if (text.charAt(i + j) != pattern.charAt(j)) {
                     break;
                 }
             }
             if (j == m) {
                 return i; // Pattern found at index i
             }
         }
         return -1; // Pattern not found
     }
     
     // Time Complexity: O((n-m+1) * m) = O(n*m) in worst case
     

Explanation: The naive string matching algorithm checks every possible position in the text for the pattern. It's simple but inefficient for large texts.

Algorithm Steps:

  • Outer Loop: Try each starting position in the text
  • Inner Loop: Compare pattern characters with text characters
  • Early Exit: Break if characters don't match
  • Success: Return position if entire pattern matches

Time Complexity: O((n-m+1) × m) = O(n×m) in worst case | Space Complexity: O(1)

When to Use: Only for small texts or when simplicity is more important than performance. For production use, prefer KMP or Boyer-Moore algorithms.

b) KMP (Knuth-Morris-Pratt) Algorithm

Efficient string matching using prefix function:


     public int kmpSearch(String text, String pattern) {
         int n = text.length();
         int m = pattern.length();
         
         // Compute prefix function
         int[] lps = computeLPS(pattern);
         
         int i = 0, j = 0;
         while (i < n) {
             if (pattern.charAt(j) == text.charAt(i)) {
                 i++;
                 j++;
             }
             if (j == m) {
                 return i - j; // Pattern found
             } else if (i < n && pattern.charAt(j) != text.charAt(i)) {
                 if (j != 0) {
                     j = lps[j - 1];
                 } else {
                     i++;
                 }
             }
         }
         return -1;
     }
     
     private int[] computeLPS(String pattern) {
         int m = pattern.length();
         int[] lps = new int[m];
         int len = 0, i = 1;
         
         while (i < m) {
             if (pattern.charAt(i) == pattern.charAt(len)) {
                 len++;
                 lps[i] = len;
                 i++;
             } else {
                 if (len != 0) {
                     len = lps[len - 1];
                 } else {
                     lps[i] = 0;
                     i++;
                 }
             }
         }
         return lps;
     }
     
     // Time Complexity: O(n + m)
     

Explanation: The KMP algorithm is an efficient string matching algorithm that uses the concept of longest proper prefix which is also a suffix (LPS) to avoid unnecessary comparisons.

Key Concepts:

  • LPS Array: Precomputed array that stores the length of the longest proper prefix that is also a suffix
  • Smart Skipping: When a mismatch occurs, KMP can skip characters based on the LPS array
  • Linear Time: Achieves O(n+m) time complexity where n is text length and m is pattern length

Algorithm Steps:

  • Preprocessing: Compute LPS array for the pattern
  • Matching: Use LPS to skip unnecessary comparisons
  • Backtracking: When mismatch occurs, use LPS to determine next position

Time Complexity: O(n + m) | Space Complexity: O(m) for the LPS array

Advantage: Never moves backwards in the text, making it suitable for streaming data.

2.5 Advanced String Problems

2.5.1 Longest Palindromic Substring


     public String longestPalindrome(String s) {
         if (s == null || s.length() < 2) return s;
         
         int start = 0, maxLen = 1;
         int n = s.length();
         
         for (int i = 0; i < n; i++) {
             // Check odd length palindromes
             int len1 = expandAroundCenter(s, i, i);
             // Check even length palindromes
             int len2 = expandAroundCenter(s, i, i + 1);
             
             int len = Math.max(len1, len2);
             if (len > maxLen) {
                 start = i - (len - 1) / 2;
                 maxLen = len;
             }
         }
         
         return s.substring(start, start + maxLen);
     }
     
     private int expandAroundCenter(String s, int left, int right) {
         while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
             left--;
             right++;
         }
         return right - left - 1;
     }
     

2.5.2 Valid Parentheses


     public boolean isValid(String s) {
         Stack<Character> stack = new Stack<>();
         
         for (char c : s.toCharArray()) {
             if (c == '(' || c == '{' || c == '[') {
                 stack.push(c);
             } else {
                 if (stack.isEmpty()) return false;
                 
                 char top = stack.pop();
                 if ((c == ')' && top != '(') || 
                     (c == '}' && top != '{') || 
                     (c == ']' && top != '[')) {
                     return false;
                 }
             }
         }
         
         return stack.isEmpty();
     }
     

2.5.3 String Compression


     public String compress(String chars) {
         StringBuilder sb = new StringBuilder();
         int count = 1;
         
         for (int i = 1; i < chars.length(); i++) {
             if (chars.charAt(i) == chars.charAt(i - 1)) {
                 count++;
             } else {
                 sb.append(chars.charAt(i - 1));
                 if (count > 1) {
                     sb.append(count);
                 }
                 count = 1;
             }
         }
         
         // Handle last group
         sb.append(chars.charAt(chars.length() - 1));
         if (count > 1) {
             sb.append(count);
         }
         
         return sb.toString();
     }
     
     // Example: "aaabbc" → "a3b2c"
     

2.6 String Interview Questions

Q1: Find the first non-repeating character in a string

public char firstUniqChar(String s) {
    int[] count = new int[26];
    
    // Count frequency of each character
    for (char c : s.toCharArray()) {
        count[c - 'a']++;
    }
    
    // Find first character with count 1
    for (char c : s.toCharArray()) {
        if (count[c - 'a'] == 1) {
            return c;
        }
    }
    
    return '#'; // No unique character found
}

Explanation: This solution uses a character count array to track the frequency of each character. It first counts all characters, then finds the first one with count 1.

Time Complexity: O(n) | Space Complexity: O(1) since we use a fixed-size array.

Q2: Check if strings are rotations of each other

public boolean isRotation(String s1, String s2) {
    if (s1.length() != s2.length()) return false;
    
    // Concatenate s1 with itself and check if s2 is substring
    String concatenated = s1 + s1;
    return concatenated.contains(s2);
}

Explanation: If s2 is a rotation of s1, then s2 must be a substring of s1+s1. This is because rotating a string is equivalent to taking a substring from the concatenated string.

Example: "waterbottle" and "erbottlewat" are rotations.

Time Complexity: O(n²) | Space Complexity: O(n)

Q3: Find all anagrams in a string

public List<Integer> findAnagrams(String s, String p) {
    List<Integer> result = new ArrayList<>();
    if (s.length() < p.length()) return result;
    
    int[] pCount = new int[26];
    int[] sCount = new int[26];
    
    // Count characters in pattern
    for (char c : p.toCharArray()) {
        pCount[c - 'a']++;
    }
    
    // Sliding window approach
    for (int i = 0; i < s.length(); i++) {
        sCount[s.charAt(i) - 'a']++;
        
        if (i >= p.length()) {
            sCount[s.charAt(i - p.length()) - 'a']--;
        }
        
        if (Arrays.equals(pCount, sCount)) {
            result.add(i - p.length() + 1);
        }
    }
    
    return result;
}

Explanation: This solution uses a sliding window approach with character counting. It maintains a window of size p.length() and compares character frequencies to find anagrams.

Time Complexity: O(n) | Space Complexity: O(1) since we use fixed-size arrays.

2.7 Key Takeaways

  • Immutable Nature: Strings cannot be modified once created
  • Memory Efficiency: String interning can save memory
  • Pattern Matching: KMP algorithm is efficient for string searching
  • Palindrome Problems: Use two-pointer technique
  • Anagram Problems: Use character counting or sorting
  • Dynamic Programming: Useful for LCS and other complex problems
Interview Tip: Always clarify if the string contains only ASCII characters, Unicode characters, or special symbols. This affects the approach and space complexity.

3. Linked Lists

3.1 Introduction to Linked Lists

A Linked List is a linear data structure consisting of a sequence of elements called nodes, where each node contains data and a reference (link) to the next node in the sequence. Unlike arrays, linked lists do not require contiguous memory allocation, making them more flexible for dynamic data.

Key Characteristics:

  • Dynamic Size: Can grow and shrink at runtime without memory reallocation
  • Non-contiguous Memory: Nodes can be scattered throughout memory
  • Sequential Access: Must traverse from the beginning to access elements
  • Flexible Structure: Easy to insert and delete elements at any position
  • Memory Efficiency: Only allocates memory for actual data and links

3.2 Types of Linked Lists

Linked lists come in several variations, each offering different trade-offs between functionality and complexity:

  • Singly Linked List: Each node contains data and a reference to the next node only
  • Doubly Linked List: Each node contains data and references to both next and previous nodes
  • Circular Linked List: The last node points back to the first node, creating a circular structure
  • Circular Doubly Linked List: Combines the benefits of both circular and doubly linked lists
  • Skip List: A probabilistic data structure that allows fast search within an ordered sequence

     // Node structure for singly linked list
     struct Node {
         int data;
         struct Node* next;
     };
     
     // Creating a new node
     struct Node* newNode(int data) {
         struct Node* node = (struct Node*)malloc(sizeof(struct Node));
         node->data = data;
         node->next = NULL;
         return node;
     }
     

Explanation: This example shows the fundamental structure of a linked list node in C/C++. Each node contains data and a pointer to the next node.

Key Components:

  • Data Field: Stores the actual value (can be any data type)
  • Next Pointer: References the next node in the sequence
  • Memory Allocation: Uses malloc() to allocate memory dynamically
  • Initialization: Sets next pointer to NULL to indicate end of list

Memory Management: Remember to free allocated memory when nodes are no longer needed to prevent memory leaks.

Variations: This structure can be extended for doubly linked lists (add prev pointer) or circular lists (last node points to first).

3.3 Time Complexity

Operation Time Complexity Space Complexity
Access O(n) O(1)
Search O(n) O(1)
Insertion (at beginning) O(1) O(1)
Insertion (at end) O(n) O(1)
Deletion (at beginning) O(1) O(1)
Deletion (at end) O(n) O(1)

4. Stacks

4.1 Introduction to Stacks

A Stack is a linear data structure that follows the Last In First Out (LIFO) principle. Think of it like a stack of plates - you can only add or remove plates from the top. The last element added to the stack is the first one to be removed.

Key Characteristics:

  • LIFO Principle: Last element added is the first one removed
  • Single Access Point: Only the top element is accessible
  • Dynamic Size: Can grow and shrink as needed
  • Simple Operations: Limited set of operations (push, pop, peek)
  • Efficient Operations: All operations are O(1) time complexity
  • Memory Management: Used by programming languages for function calls

4.2 LIFO Principle Explained

The Last In First Out (LIFO) principle means that the most recently added element is the first one to be removed. This is similar to how a stack of books works - you can only remove the book that was placed on top most recently.

Basic Operations:

  1. Push: Add an element to the top
  2. Pop: Remove the top element
  3. Peek/Top: View the top element without removing
  4. isEmpty: Check if stack is empty

     // Stack implementation using array
     class Stack {
         private int maxSize;
         private int[] stackArray;
         private int top;
         
         public Stack(int size) {
             maxSize = size;
             stackArray = new int[maxSize];
             top = -1;
         }
         
         public void push(int value) {
             if (top < maxSize - 1) {
                 stackArray[top++] = value;
             }
         }
         
         public int pop() {
             if (top >= 0) {
                 return stackArray[top--];
             }
             return -1; // Stack underflow
         }
     }
     

Explanation: This example demonstrates a stack implementation using an array in Java. The stack follows the LIFO (Last In First Out) principle.

Key Components:

  • maxSize: Maximum capacity of the stack
  • stackArray: Array to store stack elements
  • top: Index pointing to the top element

Operations:

  • Push: Adds element to top (O(1) time complexity)
  • Pop: Removes and returns top element (O(1) time complexity)
  • Peek: Returns top element without removing (not shown in code)

Limitations: Fixed size, potential stack overflow/underflow. For production use, consider using Java's built-in Stack class or LinkedList.

4.3 Applications

  • Function call management (call stack)
  • Expression evaluation
  • Undo operations
  • Backtracking algorithms
  • Browser history
  • Parentheses matching
  • Postfix expression evaluation

5. Queues

5.1 Introduction to Queues

A Queue is a linear data structure that follows the First In First Out (FIFO) principle. Think of it like a line of people waiting for a service - the first person to join the line is the first one to be served.

Key Characteristics:

  • FIFO Principle: First element added is the first one removed
  • Two Access Points: Elements are added at the rear and removed from the front
  • Dynamic Size: Can grow and shrink as needed
  • Ordered Processing: Maintains the order of elements
  • Efficient Operations: Enqueue and dequeue operations are O(1) time complexity
  • Real-world Modeling: Naturally models many real-world scenarios

5.2 FIFO Principle Explained

The First In First Out (FIFO) principle means that the first element added to the queue is the first one to be removed. This is similar to how a queue of people works - the person who arrived first gets served first.


     // Queue implementation using array
     class Queue {
         private int maxSize;
         private int[] queueArray;
         private int front;
         private int rear;
         private int currentSize;
         
         public Queue(int size) {
             maxSize = size;
             queueArray = new int[maxSize];
             front = 0;
             rear = -1;
             currentSize = 0;
         }
         
         public void enqueue(int value) {
             if (currentSize < maxSize) {
                 rear = (rear + 1) % maxSize;
                 queueArray[rear] = value;
                 currentSize++;
             }
         }
         
         public int dequeue() {
             if (currentSize > 0) {
                 int value = queueArray[front];
                 front = (front + 1) % maxSize;
                 currentSize--;
                 return value;
             }
             return -1; // Queue underflow
         }
     }
     

Explanation: This example demonstrates a circular queue implementation using an array in Java. The queue follows the FIFO (First In First Out) principle with efficient space utilization.

Key Components:

  • maxSize: Maximum capacity of the queue
  • queueArray: Array to store queue elements
  • front: Index of the element to be removed next
  • rear: Index where the next element will be added
  • currentSize: Current number of elements in the queue

Circular Implementation:

  • Modulo Operation: Uses % maxSize to wrap around the array
  • Space Efficiency: Reuses space when elements are dequeued
  • O(1) Operations: Both enqueue and dequeue are constant time

Applications: Task scheduling, breadth-first search, printer queues, and any scenario requiring ordered processing.

5.3 Types of Queues

  • Simple Queue: Basic FIFO queue
  • Circular Queue: Efficient use of array space
  • Priority Queue: Elements with priority
  • Double-ended Queue (Deque): Insert/delete from both ends
  • Bounded Queue: Queue with maximum capacity
  • Unbounded Queue: Queue that can grow indefinitely

6. Trees

6.1 Introduction to Trees

A Tree is a hierarchical data structure consisting of nodes connected by edges. It represents a hierarchical relationship between elements, similar to a family tree or organizational chart.

Key Characteristics:

  • Hierarchical Structure: Elements are organized in levels with parent-child relationships
  • Single Root: One node serves as the starting point (root) of the tree
  • No Cycles: No loops or cycles in the structure
  • Connected Graph: Every node is reachable from the root
  • Flexible Structure: Can represent complex hierarchical relationships
  • Efficient Search: Many tree types offer fast search operations

6.2 Tree Terminology

  • Node: Each element in the tree containing data and references to children
  • Root: The topmost node with no parent
  • Parent: A node that has one or more children
  • Child: A node that has a parent
  • Leaf: A node with no children
  • Sibling: Nodes that share the same parent
  • Ancestor: Any node on the path from root to a given node
  • Descendant: Any node reachable from a given node
  • Height: The length of the longest path from root to a leaf
  • Depth: The length of the path from root to a specific node

6.3 Binary Tree

A Binary Tree is a tree data structure where each node has at most two children, referred to as the left child and right child. This is one of the most fundamental tree structures and serves as the basis for many advanced data structures.


     // Binary Tree Node
     class TreeNode {
         int data;
         TreeNode left;
         TreeNode right;
         
         TreeNode(int data) {
             this.data = data;
             this.left = null;
             this.right = null;
         }
     }
     
     // Tree Traversals
     void inorder(TreeNode root) {
         if (root != null) {
             inorder(root.left);
             System.out.print(root.data + " ");
             inorder(root.right);
         }
     }
     
     void preorder(TreeNode root) {
         if (root != null) {
             System.out.print(root.data + " ");
             preorder(root.left);
             preorder(root.right);
         }
     }
     
     void postorder(TreeNode root) {
         if (root != null) {
             postorder(root.left);
             postorder(root.right);
             System.out.print(root.data + " ");
         }
     }
     

Explanation: This example demonstrates the fundamental structure of a binary tree node and the three main tree traversal algorithms: inorder, preorder, and postorder.

Node Structure:

  • Data Field: Stores the node's value
  • Left Pointer: References the left child node
  • Right Pointer: References the right child node

Traversal Algorithms:

  • Inorder (LNR): Left → Node → Right. For BST, this gives elements in sorted order
  • Preorder (NLR): Node → Left → Right. Useful for copying trees
  • Postorder (LRN): Left → Right → Node. Useful for deleting trees

Time Complexity: O(n) for all traversals where n is the number of nodes | Space Complexity: O(h) where h is the height of the tree (due to recursion stack)

Applications: Expression trees, syntax trees, file system organization, and hierarchical data representation.

6.4 Binary Search Tree (BST)

Properties:
  • Left subtree contains nodes with values less than the parent
  • Right subtree contains nodes with values greater than the parent
  • Both left and right subtrees must also be BSTs
  • Inorder traversal gives sorted order
  • No duplicate values (in most implementations)

6.5 Time Complexity

Operation Average Case Worst Case
Search O(log n) O(n)
Insertion O(log n) O(n)
Deletion O(log n) O(n)

7. Graphs

7.1 Introduction to Graphs

A Graph is a non-linear data structure consisting of a finite set of vertices (nodes) and edges that connect these vertices. Graphs are used to represent relationships between objects and are fundamental in modeling real-world networks, social connections, and complex systems.

Key Characteristics:

  • Vertices (Nodes): Represent entities or objects in the system
  • Edges: Represent relationships or connections between vertices
  • Flexible Structure: Can represent complex relationships and networks
  • Directional/Undirectional: Edges can be directed or undirected
  • Weighted/Unweighted: Edges can have associated weights or costs
  • Cyclic/Acyclic: Can contain cycles or be acyclic

7.2 Types of Graphs

  • Undirected Graph: Edges have no direction, bidirectional relationships
  • Directed Graph (Digraph): Edges have direction, one-way relationships
  • Weighted Graph: Edges have associated weights or costs
  • Unweighted Graph: All edges have equal weight
  • Connected Graph: Every vertex is reachable from every other vertex
  • Disconnected Graph: Contains isolated components
  • Complete Graph: Every vertex is connected to every other vertex
  • Bipartite Graph: Vertices can be divided into two disjoint sets

7.3 Graph Representation

Graphs can be represented in memory using various data structures, each with different trade-offs in terms of space complexity and operation efficiency.


     // Adjacency Matrix representation
     class Graph {
         private int V;
         private int[][] adjMatrix;
         
         Graph(int vertices) {
             V = vertices;
             adjMatrix = new int[V][V];
         }
         
         void addEdge(int src, int dest) {
             adjMatrix[src][dest] = 1;
             adjMatrix[dest][src] = 1; // For undirected graph
         }
     }
     
     // Adjacency List representation
     class Graph {
         private int V;
         private LinkedList<Integer>[] adjList;
         
         Graph(int vertices) {
             V = vertices;
             adjList = new LinkedList[V];
             for (int i = 0; i < V; i++) {
                 adjList[i] = new LinkedList<>();
             }
         }
         
         void addEdge(int src, int dest) {
             adjList[src].add(dest);
             adjList[dest].add(src); // For undirected graph
         }
     }
     

Explanation: This example shows two different ways to represent a graph in memory: adjacency matrix and adjacency list. Each has different trade-offs in terms of space and time complexity.

Adjacency Matrix:

  • Structure: 2D array where adjMatrix[i][j] = 1 if there's an edge between vertices i and j
  • Space Complexity: O(V²) where V is the number of vertices
  • Edge Check: O(1) constant time to check if an edge exists
  • Best For: Dense graphs with many edges

Adjacency List:

  • Structure: Array of linked lists where each list contains adjacent vertices
  • Space Complexity: O(V + E) where E is the number of edges
  • Edge Check: O(degree) time to check if an edge exists
  • Best For: Sparse graphs with few edges

When to Use: Use adjacency matrix for dense graphs and frequent edge queries, adjacency list for sparse graphs and traversal operations.

7.4 Graph Traversal

Breadth First Search (BFS):

  1. Start from a given vertex
  2. Visit all adjacent vertices
  3. Use a queue to keep track of vertices to visit
  4. Mark visited vertices to avoid cycles
  5. Process vertices level by level

Depth First Search (DFS):

  1. Start from a given vertex
  2. Go as deep as possible along each branch
  3. Use recursion or stack
  4. Backtrack when reaching a dead end
  5. Explore one branch completely before moving to another

8. Sorting Algorithms

8.1 Introduction to Sorting

Sorting is the process of arranging elements in a specific order (ascending or descending). It is one of the most fundamental operations in computer science and is used in virtually every application that deals with data.

Importance of Sorting:

  • Data Organization: Makes data easier to read, search, and analyze
  • Search Optimization: Sorted data enables efficient search algorithms like binary search
  • Data Analysis: Essential for statistical analysis and data mining
  • User Experience: Users expect data to be presented in organized formats
  • Algorithm Efficiency: Many algorithms work better with sorted data
  • Database Operations: Improves query performance and indexing

Sorting Criteria: Algorithms can be compared based on:

  • Time Complexity: How fast the algorithm runs
  • Space Complexity: How much extra memory is required
  • Stability: Whether equal elements maintain their relative order
  • In-place: Whether the algorithm modifies the original array
  • Adaptive: Whether the algorithm performs better on partially sorted data

8.2 Bubble Sort

Bubble Sort is a simple sorting algorithm that repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order. The pass through the list is repeated until no swaps are needed.


     void bubbleSort(int arr[], int n) {
         for (int i = 0; i < n-1; i++) {
             for (int j = 0; j < n-i-1; j++) {
                 if (arr[j] > arr[j+1]) {
                     // Swap arr[j] and arr[j+1]
                     int temp = arr[j];
                     arr[j] = arr[j+1];
                     arr[j+1] = temp;
                 }
             }
         }
     }
     
How it works:
  • Compare adjacent elements and swap if they are in wrong order
  • After each pass, the largest element "bubbles up" to its correct position
  • Continue until no more swaps are needed

Time Complexity: O(n²) | Space Complexity: O(1) | Stability: Stable

8.3 Quick Sort

Quick Sort is a highly efficient, comparison-based sorting algorithm that uses a divide-and-conquer strategy. It picks a 'pivot' element and partitions the array around it.


     int partition(int arr[], int low, int high) {
         int pivot = arr[high];
         int i = (low - 1);
         
         for (int j = low; j < high; j++) {
             if (arr[j] <= pivot) {
                 i++;
                 int temp = arr[i];
                 arr[i] = arr[j];
                 arr[j] = temp;
             }
         }
         
         int temp = arr[i + 1];
         arr[i + 1] = arr[high];
         arr[high] = temp;
         
         return i + 1;
     }
     
     void quickSort(int arr[], int low, int high) {
         if (low < high) {
             int pi = partition(arr, low, high);
             quickSort(arr, low, pi - 1);
             quickSort(arr, pi + 1, high);
         }
     }
     
How it works:
  • Choose a pivot element (usually the last element)
  • Partition the array so that elements smaller than pivot are on the left
  • Recursively sort the left and right subarrays
  • Combine the results

Time Complexity: O(n log n) average, O(n²) worst | Space Complexity: O(log n) | Stability: Not stable

8.4 Merge Sort

Merge Sort is a stable, comparison-based sorting algorithm that uses a divide-and-conquer strategy. It divides the array into two halves, recursively sorts them, and then merges the sorted halves.


     void merge(int arr[], int l, int m, int r) {
         int n1 = m - l + 1;
         int n2 = r - m;
         
         int L[] = new int[n1];
         int R[] = new int[n2];
         
         for (int i = 0; i < n1; ++i)
             L[i] = arr[l + i];
         for (int j = 0; j < n2; ++j)
             R[j] = arr[m + 1 + j];
         
         int i = 0, j = 0, k = l;
         while (i < n1 && j < n2) {
             if (L[i] <= R[j]) {
                 arr[k] = L[i];
                 i++;
             } else {
                 arr[k] = R[j];
                 j++;
             }
             k++;
         }
         
         while (i < n1) {
             arr[k] = L[i];
             i++;
             k++;
         }
         
         while (j < n2) {
             arr[k] = R[j];
             j++;
             k++;
         }
     }
     
     void mergeSort(int arr[], int l, int r) {
         if (l < r) {
             int m = l + (r - l) / 2;
             mergeSort(arr, l, m);
             mergeSort(arr, m + 1, r);
             merge(arr, l, m, r);
         }
     }
     
How it works:
  • Divide the array into two equal halves
  • Recursively sort the two halves
  • Merge the sorted halves to produce final sorted array
  • Use two pointers to merge efficiently

Time Complexity: O(n log n) | Space Complexity: O(n) | Stability: Stable

9. Searching Algorithms

9.1 Introduction to Searching

Searching is the process of finding a specific element or value within a collection of data. It is one of the most fundamental operations in computer science and is used in virtually every application.

Types of Search:

  • Linear Search: Check each element sequentially until found
  • Binary Search: Efficient search in sorted data using divide-and-conquer
  • Hash-based Search: Use hash functions for constant-time search
  • Tree-based Search: Use tree structures for efficient search
  • Pattern Search: Find patterns or substrings within text

Search Criteria: Algorithms can be compared based on:

  • Time Complexity: How fast the search completes
  • Space Complexity: How much extra memory is required
  • Data Requirements: Whether data needs to be sorted or structured
  • Success Rate: How often the search finds the target
  • Adaptability: How well it handles different data distributions

9.2 Linear Search


     int linearSearch(int arr[], int n, int x) {
         for (int i = 0; i < n; i++) {
             if (arr[i] == x)
                 return i;
         }
         return -1; // Element not found
     }
     

Time Complexity: O(n) | Space Complexity: O(1)

9.3 Binary Search


     int binarySearch(int arr[], int l, int r, int x) {
         if (r >= l) {
             int mid = l + (r - l) / 2;
             
             if (arr[mid] == x)
                 return mid;
             
             if (arr[mid] > x)
                 return binarySearch(arr, l, mid - 1, x);
             
             return binarySearch(arr, mid + 1, r, x);
         }
         return -1; // Element not found
     }
     

Time Complexity: O(log n) | Space Complexity: O(1) iterative, O(log n) recursive

Important: Binary search requires the array to be sorted!

9.4 Search Algorithm Comparison

Algorithm Time Complexity Space Complexity Best Use Case
Linear Search O(n) O(1) Small arrays, unsorted data
Binary Search O(log n) O(1) Large sorted arrays
Hash Search O(1) O(n) Frequent lookups

13. Conclusion

This comprehensive guide covers the fundamental data structures and algorithms that every programmer should know. Understanding these concepts is crucial for:

  • Technical Interviews: Most companies test these concepts extensively
  • Problem Solving: Helps in designing efficient solutions
  • Code Quality: Better understanding leads to cleaner, more efficient code
  • System Design: Foundation for designing scalable systems
  • Career Growth: Essential knowledge for senior positions
Remember: Practice is key! Implement these algorithms, solve problems on platforms like LeetCode and HackerRank, and understand the trade-offs between different approaches.

Next Steps: Continue learning advanced topics like advanced graph algorithms, advanced tree structures (AVL, Red-Black trees), advanced sorting algorithms, and specialized data structures for specific use cases.

14. Dynamic Programming

14.1 Introduction to Dynamic Programming

Dynamic Programming (DP) is a method for solving complex problems by breaking them down into simpler subproblems. It is both a mathematical optimization method and a computer programming method.

Key Principles:

  • Optimal Substructure: The optimal solution to a problem contains optimal solutions to its subproblems
  • Overlapping Subproblems: The same subproblems are solved multiple times
  • Memoization: Store results of subproblems to avoid recomputation
  • Bottom-up or Top-down: Can be implemented iteratively or recursively

When to Use Dynamic Programming:

  • Optimization Problems: Finding the best solution among many possibilities
  • Counting Problems: Finding the number of ways to do something
  • Decision Problems: Making optimal decisions at each step
  • Sequential Problems: Problems that can be broken into stages

Common Applications: Fibonacci sequence, shortest path problems, knapsack problem, longest common subsequence, matrix chain multiplication, and many more.

14.2 Fibonacci Sequence


     // Recursive approach (inefficient)
     int fib(int n) {
         if (n <= 1) return n;
         return fib(n-1) + fib(n-2);
     }
     
     // Dynamic Programming approach
     int fibDP(int n) {
         int dp[] = new int[n + 1];
         dp[0] = 0;
         dp[1] = 1;
         
         for (int i = 2; i <= n; i++) {
             dp[i] = dp[i-1] + dp[i-2];
         }
         return dp[n];
     }
     

14.3 Longest Common Subsequence (LCS)


     int lcs(char[] X, char[] Y, int m, int n) {
         int L[][] = new int[m+1][n+1];
         
         for (int i = 0; i <= m; i++) {
             for (int j = 0; j <= n; j++) {
                 if (i == 0 || j == 0)
                     L[i][j] = 0;
                 else if (X[i-1] == Y[j-1])
                     L[i][j] = L[i-1][j-1] + 1;
                 else
                     L[i][j] = Math.max(L[i-1][j], L[i][j-1]);
             }
         }
         return L[m][n];
     }
     

Time Complexity: O(mn) | Space Complexity: O(mn)

15. Summary and Best Practices

15.1 When to Use Each Data Structure

Arrays: Use when you need fast random access, know the size in advance, and don't need frequent insertions/deletions in the middle.
Linked Lists: Use when you need frequent insertions/deletions, don't know the size in advance, and don't need random access.
Stacks: Use for LIFO operations, function call management, undo operations, and backtracking.
Queues: Use for FIFO operations, task scheduling, breadth-first search, and any scenario requiring ordered processing.
Trees: Use for hierarchical data, fast search operations, and when you need to maintain sorted order.
Graphs: Use for representing relationships, networks, social connections, and complex interconnected data.

15.2 Algorithm Selection Guidelines

  • Sorting: Use Quick Sort for general purpose, Merge Sort for stable sorting, and built-in sorts for most applications
  • Searching: Use Binary Search for sorted data, Linear Search for small unsorted data, and Hash-based search for frequent lookups
  • Dynamic Programming: Use for optimization problems with overlapping subproblems
  • Two Pointers: Use for array problems requiring O(n) time complexity
  • Sliding Window: Use for substring/subarray problems with fixed or variable size

15.3 Interview Preparation Tips

  1. Understand the Problem: Ask clarifying questions and identify edge cases
  2. Plan Your Approach: Think before coding, consider multiple solutions
  3. Analyze Complexity: Always discuss time and space complexity
  4. Test Your Solution: Use examples and edge cases to verify correctness
  5. Optimize: Look for ways to improve your initial solution

16. Common Interview Problems

16.1 Introduction to Interview Problems

Interview problems are carefully designed questions that test your understanding of data structures, algorithms, and problem-solving skills. These problems are commonly used in technical interviews to assess a candidate's programming abilities and analytical thinking.

Problem-Solving Approach:

  1. Understand the Problem: Read carefully and clarify any ambiguities
  2. Identify Input/Output: Determine what the function should accept and return
  3. Consider Edge Cases: Think about boundary conditions and special cases
  4. Choose Data Structures: Select appropriate structures for the problem
  5. Design Algorithm: Plan your approach before coding
  6. Analyze Complexity: Consider time and space complexity
  7. Test Your Solution: Verify with examples and edge cases

Common Problem Categories:

  • Array/String Manipulation: Working with sequences of data
  • Two Pointers: Using multiple pointers to solve problems efficiently
  • Sliding Window: Maintaining a window of elements
  • Hash Table Problems: Using hash maps for fast lookups
  • Tree/Graph Traversal: Navigating hierarchical structures
  • Dynamic Programming: Solving optimization problems

16.2 Two Sum


     // Find two numbers that add up to target
     public int[] twoSum(int[] nums, int target) {
         Map<Integer, Integer> map = new HashMap<>();
         
         for (int i = 0; i < nums.length; i++) {
             int complement = target - nums[i];
             if (map.containsKey(complement)) {
                 return new int[] {map.get(complement), i};
             }
             map.put(nums[i], i);
         }
         return new int[] {};
     }
     

Explanation: This is a classic two-pointer problem that finds two numbers in an array that add up to a target value. It uses a hash map for efficient lookup.

Algorithm Steps:

  • Hash Map: Store each number and its index as we iterate
  • Complement Check: For each number, check if (target - current_number) exists in the map
  • Early Return: If complement found, return both indices
  • Update Map: Add current number and index to map for future lookups

Time Complexity: O(n) where n is array length | Space Complexity: O(n) for the hash map

Key Insight: Instead of checking all pairs (O(n²)), we use a hash map to achieve O(n) time complexity.

Example: For nums = [2, 7, 11, 15] and target = 9, we find 2 + 7 = 9 at indices [0, 1].

16.3 Reverse a String


     public String reverseString(String s) {
         char[] chars = s.toCharArray();
         int left = 0, right = chars.length - 1;
         
         while (left < right) {
             char temp = chars[left];
             chars[left] = chars[right];
             chars[right] = temp;
             left++;
             right--;
         }
         return new String(chars);
     }
     

Explanation: This algorithm reverses a string using the two-pointer technique. It swaps characters from both ends until the pointers meet in the middle.

Algorithm Steps:

  • Convert to Array: Convert string to character array for in-place modification
  • Two Pointers: Initialize left pointer at start and right pointer at end
  • Swap Characters: Swap characters at left and right positions
  • Move Pointers: Move left pointer right and right pointer left
  • Termination: Continue until left >= right

Time Complexity: O(n) where n is string length | Space Complexity: O(n) for the character array

Key Insight: Only need to swap n/2 pairs of characters, not all characters individually.

Example: "hello" becomes "olleh" by swapping 'h'↔'o', 'e'↔'l', and leaving 'l' in the middle.

16.4 Check if String is Palindrome


     public boolean isPalindrome(String s) {
         s = s.toLowerCase().replaceAll("[^a-zA-Z0-9]", "");
         int left = 0, right = s.length() - 1;
         
         while (left < right) {
             if (s.charAt(left) != s.charAt(right)) {
                 return false;
             }
             left++;
             right--;
         }
         return true;
     }
     

Explanation: This algorithm checks if a string is a palindrome (reads the same forward and backward) after preprocessing to handle case and special characters.

Algorithm Steps:

  • Preprocessing: Convert to lowercase and remove non-alphanumeric characters
  • Two Pointers: Use left and right pointers starting from both ends
  • Character Comparison: Compare characters at left and right positions
  • Early Exit: Return false immediately if characters don't match
  • Pointer Movement: Move pointers inward until they meet

Time Complexity: O(n) where n is string length | Space Complexity: O(n) due to string preprocessing

Key Features: Handles case-insensitive comparison and ignores punctuation/spaces, making it suitable for real-world text analysis.

Example: "A man, a plan, a canal: Panama" becomes "amanaplanacanalpanama" and is identified as a palindrome.