Snooping the basics - Arrays, strings and Hashing

Snooping the basics - Arrays, strings and Hashing

Before we dive into the core topics of searching, sorting arrays, and various algorithms, let’s first understand the basics of a few key concepts.

Here, we will cover the very basics of these topics in a simple way. Detailed explanations will follow later. The reason for separating them like this is straightforward: all these topics are interconnected.

If you want to perform searching or sorting, you need to understand arrays, ArrayLists, strings, and how they work. At the same time, if we take just one topic, like arrays, and dive deep into it, we need to know what strings are and when to use ArrayLists.

So, let’s first cover the basics of these concepts, and then we will dive deep into all of them.

Arrays

Arrays are one of the most fundamental data structures in programming.

Imagine you have hundreds of students whose marks you need to store. You can't create 100 variables—well, you could, but it's not the right way. This is where arrays come in. Arrays allow you to store multiple values in a single variable, instead of declaring separate variables for each value. They are especially useful when you need to work with a collection of related data.

1. What is an Array?

In Java, an array is a data structure that can hold multiple values of the same type. Arrays are indexed, with the first element being at index 0, the second at index 1, and so on. Arrays provide a convenient way to manage large amounts of data, such as lists of numbers, strings, or objects, efficiently.

Key Features of Arrays:

  • Fixed size: Once you declare an array, its size cannot be changed.

  • Index-based: You can access any element in an array using its index.

  • Homogeneous: All elements in an array must be of the same data type (e.g., all integers, all strings, etc.).

2. How to Declare and Initialize Arrays in Java

Declaring an Array:

To declare an array in Java, you need to specify the type of elements it will store and use square brackets [].

// Syntax: type[] arrayName;
int[] numbers;  // Declares an integer array
String[] names; // Declares a String array

Initializing an Array:

You can initialize an array in two ways: at the time of declaration or later.

1. Using the new keyword:

When you use the new keyword, you create an array with a fixed size, but without initializing the elements.

int[] numbers = new int[5]; // Array of 5 integers, default values are 0
String[] names = new String[3]; // Array of 3 Strings, default values are null
2. Using an initializer list:

You can initialize an array at the time of declaration by providing the elements in curly braces {}.

int[] numbers = {1, 2, 3, 4, 5}; // Array with 5 integers
String[] names = {"Alice", "Bob", "Charlie"}; // Array with 3 Strings

// To initialize an array with values dynamically

int[] numbers = new int[5];
Scanner scanner = new Scanner(System.in);
for (int i = 0; i < numbers.length; i++) {
    System.out.print("Enter a number: ");
    numbers[i] = scanner.nextInt();
}

3. Accessing Array Elements

You access elements in an array using their index. Remember, Java arrays are zero-indexed, meaning the first element is at index 0.

int[] numbers = {1, 2, 3, 4, 5};
System.out.println(numbers[0]); // Outputs: 1
System.out.println(numbers[2]); // Outputs: 3

//You can also update the value of an element by specifying its index.

numbers[0] = 10; // Updates the first element to 10
System.out.println(numbers[0]); // Outputs: 10

4. Array Length

In Java, the length of an array is stored in a special field called length. This is not a method but a field, so it is accessed without parentheses.

int[] numbers = {1, 2, 3, 4, 5};
System.out.println(numbers.length); // Outputs: 5

5. Multi-Dimensional Arrays

Java supports multi-dimensional arrays, meaning arrays of arrays. A common example is a two-dimensional array, often used to represent matrices or tables.

// Declaring a 2D Array:

int[][] matrix = new int[3][3]; // A 3x3 matrix

// You can initialize a 2D array similarly to a one-dimensional array:

int[][] matrix = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

// Accessing Elements in a 2D Array:

System.out.println(matrix[0][0]); // Outputs: 1
System.out.println(matrix[2][1]); // Outputs: 8

6. Common Array Operations

Java provides several built-in methods and techniques to manipulate arrays. Here are some of the most common operations:

1. Iterating Over Arrays

You can loop through arrays using for loops or enhanced for loops (also called "foreach" loops).

Standard for Loop:
int[] numbers = {1, 2, 3, 4, 5};
for (int i = 0; i < numbers.length; i++) {
    System.out.println(numbers[i]);
}

// Enhanced for Loop:

for (int number : numbers) {
    System.out.println(number);
}

2. Copying Arrays

You can copy arrays in Java using System.arraycopy(), Arrays.copyOf(), or by manually iterating over the elements.

int[] numbers = {1, 2, 3};
int[] copiedNumbers = Arrays.copyOf(numbers, numbers.length);

3. Sorting Arrays

Java provides the Arrays.sort() method to sort arrays in ascending order.

int[] numbers = {5, 3, 8, 1};

Arrays.sort(numbers); // Sorts the array in ascending order

System.out.println(Arrays.toString(numbers)); // Outputs: [1, 3, 5, 8]

4. Searching Arrays

The Arrays.binarySearch() method allows you to search for an element in a sorted array.

int[] numbers = {1, 3, 5, 7, 9};

int index = Arrays.binarySearch(numbers, 5); // Returns index 2

System.out.println(index); // Outputs: 2

Common Problems on Arrays

  1. Find the Maximum and Minimum Element in an Array

     // Initialize variables to hold the maximum and minimum values in the array
     int max = arr[0]; // Assume the first element is the maximum initially
     int min = arr[0]; // Assume the first element is the minimum initially
    
     // Iterate through the array starting from the second element
     for (int i = 1; i < arr.length; i++) {
         // Check if the current element is greater than the current maximum
         if (arr[i] > max) {
             max = arr[i]; // Update the maximum value
         }
         // Check if the current element is smaller than the current minimum
         if (arr[i] < min) {
             min = arr[i]; // Update the minimum value
         }
     }
    
     // Time Complexity: O(n)
     // The loop runs once for each element in the array, making it linear in terms of the array size.
    
  2. Reverse an Array

     // Iterate through the array from both ends towards the center
     // 'i' starts from the beginning of the array
     // 'j' starts from the end of the array
     for (int i = 0, j = arr.length - 1; i < j; i++, j--) {
         // Swap the elements at index 'i' and 'j'
         int temp = arr[i]; // Store the value at index 'i' in a temporary variable
         arr[i] = arr[j];   // Assign the value at index 'j' to index 'i'
         arr[j] = temp;     // Assign the value in the temporary variable to index 'j'
     }
    
     // Time Complexity: O(n)
     // The loop runs for half the number of elements in the array since two elements are processed in each iteration.
     // This still results in a linear time complexity as the total number of operations is proportional to the array size.
    
  3. Move All Zeros to the End

     // Initialize an index variable to track the position for non-zero elements
     int index = 0;
    
     // Iterate through each element in the array using a for-each loop
     for (int num : arr) {
         // If the current element is not zero, place it at the 'index' position
         if (num != 0) {
             arr[index++] = num; // Assign the non-zero element to the current index and increment the index
         }
     }
    
     // After placing all non-zero elements, fill the remaining positions in the array with zeros
     while (index < arr.length) {
         arr[index++] = 0; // Assign zero to the current index and increment the index
     }
    
     // Time Complexity: O(n)
     // The array is traversed twice: once to process non-zero elements and once to fill the rest with zeros.
     // This results in linear time complexity, proportional to the size of the array.
    
  4. Find the Second Largest Element

     // Initialize variables to store the maximum and second maximum values
     int max = Integer.MIN_VALUE;      // Initially set the maximum to the smallest possible integer value
     int secondMax = Integer.MIN_VALUE; // Initially set the second maximum to the smallest possible integer value
    
     // Iterate through each element in the array using a for-each loop
     for (int num : arr) {
         // If the current number is greater than the current maximum
         if (num > max) {
             secondMax = max; // Update the second maximum to the current maximum
             max = num;       // Update the maximum to the current number
         } 
         // If the current number is greater than the second maximum but not equal to the maximum
         else if (num > secondMax && num != max) {
             secondMax = num; // Update the second maximum to the current number
         }
     }
    
     // Time Complexity: O(n)
     // The array is traversed once, making the time complexity linear, proportional to the size of the array.
    
  5. Find All Pairs with a Given Sum

     // Define the target sum to find pairs that add up to this value
     int target = 10;
    
     // Iterate through the array using a nested loop to check all pairs
     for (int i = 0; i < arr.length; i++) {
         // Inner loop starts from the next element after 'i'
         for (int j = i + 1; j < arr.length; j++) {
             // Check if the sum of the pair equals the target
             if (arr[i] + arr[j] == target) {
                 // Print the pair that adds up to the target
                 System.out.println(arr[i] + ", " + arr[j]);
             }
         }
     }
    
     // Time Complexity: O(n^2)
     // The outer loop runs 'n' times, and for each iteration, the inner loop runs approximately 'n-1' times.
     // This results in a quadratic time complexity, proportional to the square of the array size.
    
  6. Sum of array elements

     class Solution {
         // Method to calculate the sum of elements in an array
         public int sum(int arr[], int n) {
             // Initialize a variable to store the cumulative sum
             int sum = 0;
    
             // Iterate through the array using a for loop
             for (int i = 0; i < n; i++) {
                 // Add the current element to the cumulative sum
                 sum = sum + arr[i];
             }
    
             // Return the total sum after the loop completes
             return sum;
         }
     }
    
     // This method efficiently calculates the sum of all elements in the array with a time complexity of O(n), 
     // as it traverses the array only once.
    
  7. Count of odd numbers in array

     class Solution {
         // Method to count the number of odd numbers in an array
         public int countOdd(int[] arr, int n) {
             // Initialize a variable to store the count of odd numbers
             int odds = 0;
    
             // Iterate through the array using a for loop
             for (int i = 0; i < n; i++) {
                 // Check if the current element is odd
                 if (arr[i] % 2 != 0) {
                     odds++; // Increment the count of odd numbers
                 }
             }
    
             // Return the total count of odd numbers
             return odds;
         }
     }
     // Time Complexity: O(n^2)
     // As each element is checked exactly once.Check if the array is sorted
    

Advantages:

  1. Efficient access: Arrays allow quick access to elements by index, making it efficient for looking up values.

  2. Fixed size: Arrays have a predefined size, which can be advantageous for memory management in situations where the number of elements is known in advance.

  3. Memory efficient: In comparison to strings (especially immutable strings), arrays can be more memory-efficient when working with a collection of items that need to be modified frequently.

Disadvantages:

  1. Fixed size (in some languages): Many languages require arrays to have a fixed size, which can be restrictive if the size of the array is unknown at compile time.

  2. Homogeneous data: Arrays generally store elements of the same type, limiting flexibility when dealing with mixed data types.

  3. Limited functionality: Basic arrays don't offer as many built-in helper methods as strings or more advanced data structures like lists.

When to use Arrays:

  • When you need to store multiple items of the same type (e.g., numbers, strings, objects).

  • When the number of elements is known in advance or can be fixed.

  • When you need fast, random access to data via an index.

  • When you need to perform repetitive operations on collections of data (like sorting, searching).

Strings

Strings are one of the most frequently used data structures in programming.

Let’s begin with the fundamentals and gradually dive into more advanced concepts.

1. What is a String in Java?

In Java, a String is a sequence of characters. It is one of the most commonly used data types for text manipulation. Strings are immutable in Java, which means once a string is created, its value cannot be changed. Instead, any operation that modifies a string creates a new string.

Key Features of Strings:

  • Immutable: Once a string is created, its value cannot be changed.

  • Index-based: You can access each character in a string by its index. The first character is at index 0.

  • Varied length: Strings can vary in length and can hold any number of characters, including special characters, spaces, and punctuation marks.

2. Declaring and Initializing Strings in Java

Declaring a String:

In Java, you declare a string by specifying the String type.

public class StringExample {
    public static void main(String[] args) {
        // Declaring and initializing a String
        String greeting = "Hello, World!";

        // Printing the String to the console
        System.out.println(greeting);
    }
}

Initializing a String:

You can initialize a string in several ways:

  • Using string literals:
String message = "Hello, World!"; // String initialized with a literal
  • Using the new keyword:
String message = new String("Hello, World!"); // Creates a new String object

3. Common String Operations

Here are some common string methods. Let's understand them before we move on to the questions.

// Demonstrating String operations in Java
public class Main {
    public static void main(String[] args) {
        // Declare a String variable
        String greeting = "Hello, World!";

        // Display the String
        System.out.println(greeting);

        // String operations

        // 1. Accessing characters in a String
        char firstCharacter = greeting.charAt(0); // Get the first character
        char fifthCharacter = greeting.charAt(4); // Get the fifth character
        System.out.println("First character: " + firstCharacter);
        System.out.println("Fifth character: " + fifthCharacter);

        // 2. Concatenating Strings
        String name = "Java";
        String fullGreeting = greeting + " Welcome to " + name + " programming!";
        System.out.println("Concatenated String: " + fullGreeting);

        // Alternatively, using concat() method
        String additionalText = " Have fun coding!";
        String concatenatedMessage = fullGreeting.concat(additionalText);
        System.out.println("Concatenated using concat(): " + concatenatedMessage);

        // 3. Find the length of the String
        int length = greeting.length();
        System.out.println("Length of the String: " + length);

        // 4. Convert the String to uppercase
        String uppercaseGreeting = greeting.toUpperCase();
        System.out.println("Uppercase: " + uppercaseGreeting);

        // 5. Replace a word in the String
        String replacedGreeting = greeting.replace("World", "Java");
        System.out.println("Replaced String: " + replacedGreeting);

        // 6. Extract a substring
        String substring = greeting.substring(7, 12); // "World"
        System.out.println("Substring: " + substring);

        // 7. Check if the String contains a specific word
        boolean containsHello = greeting.contains("Hello");
        System.out.println("Contains 'Hello': " + containsHello);

        // 7. Removing extra spaces
       String message = "   Hello   ";
       System.out.println(message.trim()); // Outputs: Hello
    }
}

4. String Questions

1 Reverse a String:

Reversing a string involves swapping characters from both ends towards the centre.

// Demonstrating how to reverse a String in Java
public class Main {
    public static void main(String[] args) {
        // Declare and initialize a String variable
        String original = "Hello, World!";

        // 1. Reverse a String using a loop
        String reversedUsingLoop = reverseStringUsingLoop(original);
        System.out.println("Reversed String (using loop): " + reversedUsingLoop);

        // 2. Reverse a String using StringBuilder
        String reversedUsingStringBuilder = reverseStringUsingStringBuilder(original);
        System.out.println("Reversed String (using StringBuilder): " + reversedUsingStringBuilder);

    }

    /**
     * Method to reverse a string using a loop
     * @param str the original string
     * @return the reversed string
     */
    public static String reverseStringUsingLoop(String str) {
        StringBuilder reversed = new StringBuilder(); // To store the reversed string
        for (int i = str.length() - 1; i >= 0; i--) { // Loop from the end of the string
            reversed.append(str.charAt(i)); // Append each character to the result
        }
        return reversed.toString(); // Convert StringBuilder to String and return
    }

    /**
     * Method to reverse a string using StringBuilder's reverse() method
     * @param str the original string
     * @return the reversed string
     */
    public static String reverseStringUsingStringBuilder(String str) {
        StringBuilder sb = new StringBuilder(str); // Initialize StringBuilder with the string
        return sb.reverse().toString(); // Reverse and convert to String
    }

}

2 Palindrome Check:

A palindrome is a word that reads the same backward as forward. To check if a string is a palindrome:

// Demonstrating how to check if a String is a palindrome in Java
public class Main {
    public static void main(String[] args) {
        // Declare and initialize a String variable
        String input = "madam"; // Change this value to test different strings

        // 1. Palindrome check using a loop
        boolean isPalindromeUsingLoop = checkPalindromeUsingLoop(input);
        System.out.println("Is Palindrome (using loop): " + isPalindromeUsingLoop);

        // 2. Palindrome check using StringBuilder
        boolean isPalindromeUsingStringBuilder = checkPalindromeUsingStringBuilder(input);
        System.out.println("Is Palindrome (using StringBuilder): " + isPalindromeUsingStringBuilder);

    }

    /**
     * Method to check if a string is a palindrome using a loop
     * @param str the input string
     * @return true if the string is a palindrome, false otherwise
     */
    public static boolean checkPalindromeUsingLoop(String str) {
        int start = 0;               // Pointer at the beginning of the string
        int end = str.length() - 1;  // Pointer at the end of the string

        while (start < end) {        // Loop until the pointers meet
            if (str.charAt(start) != str.charAt(end)) {
                return false;       // Return false if characters don't match
            }
            start++;
            end--;
        }
        return true;                // Return true if the loop completes without mismatch
    }

    /**
     * Method to check if a string is a palindrome using StringBuilder
     * @param str the input string
     * @return true if the string is a palindrome, false otherwise
     */
    public static boolean checkPalindromeUsingStringBuilder(String str) {
        StringBuilder sb = new StringBuilder(str);  // Initialize StringBuilder with the input string
        String reversed = sb.reverse().toString(); // Reverse the string and convert to String
        return str.equals(reversed);              // Compare original and reversed strings
    }

}

3 Largest Odd Number in a String:

To find the largest odd number in a string, you need to extract all numbers and check if they are odd.

// Demonstrating how to find the largest odd number in a string in Java
public class Main {
    public static void main(String[] args) {
        // Declare and initialize a String containing digits
        String input = "57483902"; // Change this value to test different strings

        // 1. Find the largest odd number using a loop
        String largestOddUsingLoop = findLargestOddNumberUsingLoop(input);
        System.out.println("Largest Odd Number (using loop): " + largestOddUsingLoop);

        // 2. Find the largest odd number using String manipulation
        String largestOddUsingStringManipulation = findLargestOddNumberUsingStringManipulation(input);
        System.out.println("Largest Odd Number (using String manipulation): " + largestOddUsingStringManipulation);
    }

    /**
     * Method to find the largest odd number using a loop
     * @param str the input string
     * @return the largest odd number as a string, or an empty string if none exists
     */
    public static String findLargestOddNumberUsingLoop(String str) {
        // Loop from the last character to the first
        for (int i = str.length() - 1; i >= 0; i--) {
            char currentChar = str.charAt(i); // Get the current character
            int digit = Character.getNumericValue(currentChar); // Convert it to an integer

            // Check if the digit is odd
            if (digit % 2 != 0) {
                // Return the substring from the beginning to this character (inclusive)
                return str.substring(0, i + 1);
            }
        }
        // Return an empty string if no odd number is found
        return "";
    }

    /**
     * Method to find the largest odd number using String manipulation
     * @param str the input string
     * @return the largest odd number as a string, or an empty string if none exists
     */
    public static String findLargestOddNumberUsingStringManipulation(String str) {
        int lastOddIndex = -1; // To store the index of the last odd digit

        // Loop through the string to find the last odd digit
        for (int i = 0; i < str.length(); i++) {
            char currentChar = str.charAt(i); // Get the current character
            int digit = Character.getNumericValue(currentChar); // Convert it to an integer

            if (digit % 2 != 0) { // Check if the digit is odd
                lastOddIndex = i; // Update the last odd index
            }
        }

        // If an odd digit was found, return the substring up to the last odd index
        return lastOddIndex != -1 ? str.substring(0, lastOddIndex + 1) : "";
    }
}

4 Longest Common Prefix:

To find the longest common prefix in an array of strings, you can compare characters at each index.

// Demonstrating how to find the longest common prefix in Java
public class Main {
    public static void main(String[] args) {
        // Declare and initialize an array of strings
        String[] words = {"flower", "flow", "flight"}; // Change this array to test different inputs

        // Find the longest common prefix
        String longestCommonPrefix = findLongestCommonPrefix(words);
        System.out.println("Longest Common Prefix: " + longestCommonPrefix);
    }

    /**
     * Method to find the longest common prefix among an array of strings
     * @param strs the array of strings
     * @return the longest common prefix as a string, or an empty string if no common prefix exists
     */
    public static String findLongestCommonPrefix(String[] strs) {
        // Return an empty string if the input array is null or empty
        if (strs == null || strs.length == 0) {
            return "";
        }

        // Start with the first string as the initial prefix
        String prefix = strs[0];

        // Iterate through the remaining strings in the array
        for (int i = 1; i < strs.length; i++) {
            // Compare the current prefix with each string
            while (strs[i].indexOf(prefix) != 0) {
                // Shorten the prefix by removing the last character
                prefix = prefix.substring(0, prefix.length() - 1);

                // If the prefix becomes empty, return an empty string
                if (prefix.isEmpty()) {
                    return "";
                }
            }
        }
        return prefix; // Return the final common prefix
    }
}

5 Rotate String:

To rotate a string by a given number of positions, you can split the string at the rotation index and concatenate the parts.

// Demonstrating how to rotate a string in Java
public class Main {
    public static void main(String[] args) {
        // Example input string and rotation value
        String input = "abcdef"; // The string to rotate
        int rotation = 2;        // Number of positions to rotate

        // Perform the rotation
        String rotatedString = rotateString(input, rotation);
        System.out.println("Rotated String: " + rotatedString);
    }

    /**
     * Method to rotate a string by a given number of positions
     * @param str the input string
     * @param positions the number of positions to rotate
     * @return the rotated string
     */
    public static String rotateString(String str, int positions) {
        // Handle edge cases
        if (str == null || str.isEmpty()) {
            return str; // Return as-is if the string is null or empty
        }

        // Ensure the rotation value is within the string's length
        int length = str.length();
        positions = positions % length; // Reduce positions if greater than the string length

        // If no rotation is needed, return the original string
        if (positions == 0) {
            return str;
        }

        // Split the string into two parts: (length - positions) and positions
        String part1 = str.substring(0, length - positions); // Characters from the start
        String part2 = str.substring(length - positions);   // Characters to rotate to the front

        // Concatenate the two parts to form the rotated string
        return part2 + part1;
    }
}

7 anagrams - Two strings are anagrams if they have the same characters in the same frequency. You can check this by sorting both strings and comparing them.

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        // Example input strings
        String str1 = "listen"; // Change these values to test different cases
        String str2 = "silent";

        // Check if the strings are anagrams
        boolean areAnagrams = checkAnagrams(str1, str2);
        System.out.println("Are the strings anagrams? " + areAnagrams);
    }

    /**
     * Method to check if two strings are anagrams
     * @param s1 the first string
     * @param s2 the second string
     * @return true if the strings are anagrams, false otherwise
     */
    public static boolean checkAnagrams(String s1, String s2) {
        // Edge case: If lengths differ, they cannot be anagrams
        if (s1.length() != s2.length()) {
            return false;
        }

        // Convert both strings to character arrays
        char[] charArray1 = s1.toCharArray();
        char[] charArray2 = s2.toCharArray();

        // Sort the character arrays
        Arrays.sort(charArray1);
        Arrays.sort(charArray2);

        // Compare the sorted arrays
        return Arrays.equals(charArray1, charArray2);
    }
}

8 Sort Characters by Frequency:

To sort characters in a string by frequency, you can use a frequency map and a priority queue.

public String frequencySort(String s) {
    Map<Character, Integer> map = new HashMap<>();
    for (char c : s.toCharArray()) {
        map.put(c, map.getOrDefault(c, 0) + 1);
    }

    PriorityQueue<Map.Entry<Character, Integer>> pq = new PriorityQueue<>(
        (a, b) -> b.getValue() - a.getValue());
    pq.addAll(map.entrySet());

    StringBuilder result = new StringBuilder();
    while (!pq.isEmpty()) {
        Map.Entry<Character, Integer> entry = pq.poll();
        for (int i = 0; i < entry.getValue(); i++) {
            result.append(entry.getKey());
        }
    }
    return result.toString();
}

Advantages:

  1. Immutability: In many programming languages, strings are immutable, meaning their value cannot be changed once created. This helps in preventing accidental changes.

  2. Easy to use: Strings are built-in data types in most languages and come with many helper functions (like concatenation, search, split) that make string manipulation easier.

  3. Readable: String data is often more human-readable, as it represents textual information directly.

Disadvantages:

  1. Fixed length: In some languages, strings may have a fixed length, making operations like resizing or inserting characters more challenging.

  2. Memory inefficient for large data: Since strings are immutable, any modification creates a new string, which can be less memory-efficient when working with large text data.

  3. Limited operations: For complex data manipulation, strings may not be the best choice (e.g., numeric computations, complex transformations).

When to use Strings:

  • When you need to work with textual data (e.g., names, descriptions, messages).

  • When you need to manipulate, format, or analyze text.

  • When the size of the data is not likely to change significantly.

Hashing

Hashing is a way to turn large data into smaller, fixed-size values using a hash function. These values are kept in a hash table, which makes it easy to quickly find data using the hash code.

Hash Functions

A hash function takes input data (keys) and produces a hash code, an integer that serves as an index in the hash table. The efficiency of hashing depends on the quality of the hash function. In Java, commonly used hash functions include:

  1. Object.hashCode(): Every Java object has a hashCode() method that returns an integer hash code.

  2. Custom Hash Functions: Developers can define their own hash functions tailored to specific requirements.

We don't need to dive deep into this right now, especially regarding DSA itself, but we will cover it when we need to create a custom one.

Hashing Data Structures in Java

  1. HashMap: Stores key-value pairs and allows for efficient retrieval, insertion, and deletion.

  2. HashSet: Implements the Set interface using a hash table and ensures no duplicate elements.

  3. LinkedHashMap: Maintains insertion order while leveraging a hash table.

  4. Hashtable: Similar to HashMap but synchronized and thread-safe.

Again, we will deep dive into this later.

Lets see the example of how it looks

// HashMap: Declaration 

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        // Declare a HashMap to store names (key) and their ages (value)
        HashMap<String, Integer> ageMap = new HashMap<>();

        // Add key-value pairs to the HashMap
        ageMap.put("Alice", 25);
        ageMap.put("Bob", 30);
        ageMap.put("Charlie", 22);

        // Access and print a value using a key
        System.out.println("Age of Bob: " + ageMap.get("Bob")); // Output: Age of Bob: 30

        // Iterate through the HashMap and print all key-value pairs
        for (String key : ageMap.keySet()) {
            System.out.println(key + " is " + ageMap.get(key) + " years old.");
        }
    }
}
// Output
// Age of Bob: 30
// Alice is 25 years old.
// Bob is 30 years old.
// Charlie is 22 years old.
// HashSet: Declaration 

import java.util.HashSet;

public class HashSetExample {
    public static void main(String[] args) {
        // Declare a HashSet to store unique numbers
        HashSet<Integer> numberSet = new HashSet<>();

        // Add elements to the HashSet
        numberSet.add(10);
        numberSet.add(20);
        numberSet.add(30);
        numberSet.add(10); // Duplicate, won't be added

        // Check if an element exists in the HashSet
        System.out.println("Contains 20? " + numberSet.contains(20)); // Output: Contains 20? true

        // Iterate through the HashSet and print all elements
        System.out.println("HashSet elements:");
        for (int num : numberSet) {
            System.out.println(num);
        }
    }
}

// Output
// Contains 20? true
// HashSet elements:
// 10
// 20
// 30
import java.util.HashSet;

// Class to check if an array contains duplicates
public class ContainsDuplicate {

    /**
     * Method to determine if an array contains duplicates.
     * @param nums The array of integers to check.
     * @return true if duplicates are found, otherwise false.
     */
    public static boolean hasDuplicate(int[] nums) {
        // Create a HashSet to store unique elements from the array
        HashSet<Integer> set = new HashSet<>();

        // Iterate through each element in the array
        for (int num : nums) {
            // Attempt to add the current number to the HashSet
            // If add() returns false, it means the number is already in the set
            if (!set.add(num)) {
                return true; // Duplicate found, return true
            }
        }

        // If the loop completes without finding duplicates, return false
        return false;
    }

    // Main method to test the hasDuplicate function
    public static void main(String[] args) {
        // Define a sample array to check for duplicates
        int[] nums = {1, 2, 3, 4, 5, 1};

        // Call the hasDuplicate method and print the result
        System.out.println(hasDuplicate(nums)); // Output: true
    }
}
import java.util.HashMap;

// Class to count and display frequencies of elements in an array
public class FrequencyCounter {

    /**
     * Method to count and print the frequencies of elements in an array.
     * @param nums The array of integers whose frequencies are to be counted.
     */
    public static void countFrequencies(int[] nums) {
        // Create a HashMap to store each number as a key and its frequency as the value
        HashMap<Integer, Integer> frequencyMap = new HashMap<>();

        // Iterate through the array
        for (int num : nums) {
            // Update the frequency of the current number in the HashMap
            // If the number is already in the map, increment its value by 1
            // If not, add it with an initial frequency of 1
            frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
        }

        // Iterate through the keys of the HashMap to display the frequencies
        for (int key : frequencyMap.keySet()) {
            // Print each key (number) and its associated frequency
            System.out.println(key + ": " + frequencyMap.get(key));
        }
    }

    // Main method to test the countFrequencies function
    public static void main(String[] args) {
        // Define a sample array to count frequencies
        int[] nums = {1, 2, 2, 3, 3, 3};

        // Call the countFrequencies method with the sample array
        countFrequencies(nums); 
        // Output:
        // 1: 1
        // 2: 2
        // 3: 3
    }
}
import java.util.HashSet;

// Class to find the intersection of two arrays
public class ArrayIntersection {

    /**
     * Method to find the intersection of two arrays.
     * @param nums1 The first array of integers.
     * @param nums2 The second array of integers.
     * @return An array containing the unique elements that are present in both arrays.
     */
    public static int[] intersection(int[] nums1, int[] nums2) {
        // HashSet to store unique elements from the first array
        HashSet<Integer> set1 = new HashSet<>();
        // HashSet to store the intersection of the two arrays
        HashSet<Integer> resultSet = new HashSet<>();

        // Add all elements from the first array to set1
        for (int num : nums1) {
            set1.add(num);
        }

        // Check each element in the second array
        // If it exists in set1, add it to resultSet
        for (int num : nums2) {
            if (set1.contains(num)) {
                resultSet.add(num);
            }
        }

        // Convert the resultSet to an array
        int[] result = new int[resultSet.size()];
        int index = 0;
        for (int num : resultSet) {
            result[index++] = num; // Add each element to the result array
        }

        // Return the resulting intersection array
        return result;
    }

    // Main method to test the intersection function
    public static void main(String[] args) {
        // Define two sample arrays
        int[] nums1 = {1, 2, 2, 3};
        int[] nums2 = {2, 2, 3, 4};

        // Call the intersection method and store the result
        int[] intersection = intersection(nums1, nums2);

        // Print the elements of the intersection array
        for (int num : intersection) {
            System.out.print(num + " "); // Output: 2 3
        }
    }
}
// Demonstrating how to check if two strings are isomorphic in Java
import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        // Example input strings
        String s1 = "egg";   // Change these values to test different cases
        String s2 = "add";

        // Check if the strings are isomorphic
        boolean isIsomorphic = areIsomorphic(s1, s2);
        System.out.println("Are the strings isomorphic? " + isIsomorphic);
    }

    /**
     * Method to check if two strings are isomorphic
     * @param s1 the first string
     * @param s2 the second string
     * @return true if the strings are isomorphic, false otherwise
     */
    public static boolean areIsomorphic(String s1, String s2) {
        // If the lengths are different, they cannot be isomorphic
        if (s1.length() != s2.length()) {
            return false;
        }

        // HashMap to store character mappings from s1 to s2
        HashMap<Character, Character> mapS1ToS2 = new HashMap<>();
        // HashMap to store character mappings from s2 to s1
        HashMap<Character, Character> mapS2ToS1 = new HashMap<>();

        // Iterate through the characters of both strings
        for (int i = 0; i < s1.length(); i++) {
            char charS1 = s1.charAt(i); // Character from s1
            char charS2 = s2.charAt(i); // Corresponding character from s2

            // Check if there is already a mapping for charS1 in mapS1ToS2
            if (mapS1ToS2.containsKey(charS1)) {
                // If the mapping doesn't match charS2, return false
                if (mapS1ToS2.get(charS1) != charS2) {
                    return false;
                }
            } else {
                // Add a new mapping from charS1 to charS2
                mapS1ToS2.put(charS1, charS2);
            }

            // Check if there is already a mapping for charS2 in mapS2ToS1
            if (mapS2ToS1.containsKey(charS2)) {
                // If the mapping doesn't match charS1, return false
                if (mapS2ToS1.get(charS2) != charS1) {
                    return false;
                }
            } else {
                // Add a new mapping from charS2 to charS1
                mapS2ToS1.put(charS2, charS1);
            }
        }

        // If all characters map correctly, the strings are isomorphic
        return true;
    }
}

Common Methods in Hashing

HashMap Methods:

  • put(K key, V value): Inserts a key-value pair.

  • get(Object key): Retrieves the value associated with the key.

  • remove(Object key): Removes the key-value pair.

  • containsKey(Object key): Checks if a key exists.

  • containsValue(Object value): Checks if a value exists.

HashSet Methods:

  • add(E element): Adds an element to the set.

  • remove(Object o): Removes the specified element.

  • contains(Object o): Checks if the set contains the specified element.

Time Complexity of Hashing Operations

OperationAverage CaseWorst Case
SearchO(1)O(n)
InsertO(1)O(n)
DeleteO(1)O(n)
  • Average Case: Most operations take constant time due to efficient hash functions.

  • Worst Case: Occurs when there are too many collisions, leading to a linear time complexity.

Advantages of Hashing

  1. Fast Access: Provides O(1) average-time complexity for search, insert, and delete operations.

  2. Efficient Memory Usage: Compact storage for data.

  3. Flexibility: Supports dynamic resizing and custom hash functions.

Disadvantages of Hashing

  1. Collisions: When two keys produce the same hash code, it can lead to performance degradation.

  2. Memory Overhead: Large hash tables may require significant memory.

  3. Complex Implementation: Designing an efficient hash function can be challenging.

Collision Handling Techniques

  1. Chaining: Uses a linked list to store all elements that hash to the same index.

  2. Open Addressing:

    • Linear Probing: Places the item in the next available slot.

    • Quadratic Probing: Uses a quadratic function to find the next slot.

    • Double Hashing: Uses a second hash function to determine the probe sequence.

When to Use Hashing

  1. Fast Data Retrieval: Ideal for scenarios requiring quick lookups, such as caching.

  2. Unique Key Management: Ensures unique keys in data storage.

  3. Dynamic Data Storage: Works well with datasets that grow dynamically.

Why Use Hashing?

Hashing is indispensable for applications requiring rapid data access and efficient management of large datasets. It powers systems such as:

  1. Databases: Indexing for fast queries.

  2. Compilers: Symbol tables for variable lookups.

  3. Web Applications: Caching and session management.

In this blog, we've covered the key concepts of arrays, strings, and hashing in Java, providing you with a solid foundation to tackle DSA problems effectively. By mastering these topics, we can now progress to more detailed sections on problem-solving and learning DSA.