Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 60

Hashing for Longest Consecutive Subsequence

Recommended resources

Hello! Welcome to our next lesson.

In our last session, we saw how HashMap can be used to solve lookup problems like "Two Sum" and "Subarray Sum Equals K" in optimal time. The core principle was leveraging the average time complexity for insertions and lookups.

Today, we'll continue exploring the power of hashing by tackling another classic interview question. We will use a HashSet—which, under the hood, is implemented using a hash map in Java—to solve the "Longest Consecutive Subsequence" problem. This problem is an excellent test of your ability to choose the right data structure and devise a clever algorithm to avoid unnecessary work.

Our learning outcome is to apply hashing to find the longest consecutive subsequence in an array.

1. Understanding the Problem

First, let's clearly define the problem. You are given an unsorted array of integers, and you need to find the length of the longest sequence of consecutive numbers. The numbers in the sequence do not need to be contiguous in the input array.

For example, given nums = [100, 4, 200, 1, 3, 2], the longest consecutive sequence is [1, 2, 3, 4]. Its length is 4.

The following video provides a great introduction to the problem and walks through a couple of initial, less-than-optimal ideas.

Longest Consecutive Sequence | Google Interview Question | Brute Better Optimal

Let's watch the beginning of the video 'Longest Consecutive Sequence' from the 'take U forward' channel. This will help us understand the problem statement and the most straightforward (but inefficient) brute-force approach.

Watch from 00:49 to 05:16. The first part defines the problem. The second part explains a brute-force approach where for each element, you linearly search the array for the next consecutive numbers. Note its O(n²) time complexity.

As you saw, the brute-force method is too slow. A better, but still not optimal, solution would be to sort the array first. Sorting takes time. After sorting, you could find the longest consecutive sequence in a single pass (). The total time complexity would be dominated by sorting, resulting in .

Our goal, driven by interview and competitive programming standards, is to achieve a linear, time solution. This is where hashing comes in.


2. The Optimal Solution with Hashing

The key to an solution is to perform fast lookups to check if a number's neighbors exist. A HashSet is the perfect data structure for this, providing average time for contains() checks.

The high-level plan is:

  1. Insert all elements of the array into a HashSet to get rid of duplicates and enable fast lookups.
  2. Iterate through the numbers and, for each number, try to build the longest possible sequence around it.

However, a naive implementation of step 2 would still be inefficient. If we check every number as a potential start of a sequence, we end up doing redundant work. For example, in [1, 2, 3, 4], if we start counting from 1, we get a length of 4. Then, if we start counting from 2, we recount [2, 3, 4]. This is wasteful.

The Key Insight: Only Start from the Beginning

The crucial optimization is to only start a sequence count from the true starting point of a sequence. How can we identify a starting point?

A number x is the start of a consecutive sequence if and only if x-1 is not present in the set.

This simple check prevents us from recounting sub-sequences. We will only ever start our while loop (the part that counts the length) for numbers like 1 (because 0 is not present) and 100 (because 99 is not present), but never for 2, 3, or 4.

This video from NeetCode provides an excellent visual intuition for this idea.

Leetcode 128 - LONGEST CONSECUTIVE SEQUENCE

This video, 'Leetcode 128 - LONGEST CONSECUTIVE SEQUENCE' by NeetCode, uses a number line to visualize the sequences and clearly explains the logic of identifying sequence starts.

Watch from 01:12 to 07:23. Pay close attention to how the number line visualization makes it easy to see distinct sequences. The core concept is identifying numbers that do not have a 'left neighbor' and treating only those as the start of a new sequence search.


3. Algorithm, Implementation, and Complexity

Now that we have the core idea, let's formalize the algorithm and look at its implementation in Java.

Algorithm:

  1. If the input array is empty, return 0.
  2. Create a HashSet and add all elements from the input array to it.
  3. Initialize a variable longestStreak = 0.
  4. Iterate through each unique number (num) in the HashSet.
  5. For each num, check if set.contains(num - 1).
    • If it does, do nothing and move to the next number. This num is not the start of a sequence.
    • If it does not, then num is the start of a new sequence.
      • Initialize currentNum = num and currentStreak = 1.
      • Enter a while loop that continues as long as set.contains(currentNum + 1).
      • Inside the loop, increment currentNum and currentStreak.
      • After the loop finishes, update longestStreak = Math.max(longestStreak, currentStreak).
  6. Return longestStreak.

This approach ensures that the inner while loop (the counting part) is executed only for the starting elements of each consecutive sequence.

Java Implementation

The following article provides a clean, well-commented Java implementation of this algorithm.

Longest Consecutive Sequence in an Array - Tutorial

Let's study the Java code and complexity analysis from the 'Longest Consecutive Sequence in an Array' tutorial on takeuforward.org. This will solidify your understanding of the practical implementation.

Please read the 'Optimal Approach' section, followed by the 'Code' section (which contains the Java solution), and finally the 'Complexity Analysis' section. Focus on how the logic we just discussed translates directly into the Java code using a HashSet.

Complexity Analysis

  • Time Complexity: .

    • Adding n elements to the HashSet takes time.
    • The outer for loop runs n times (for n unique elements). The inner while loop seems to make it , but it's not. Each number is visited by the while loop at most once over the entire execution. The total number of times the while loop's body is executed across all iterations of the outer for loop is at most n. Therefore, the total time is .
  • Space Complexity: .

    • In the worst case (all elements are unique), the HashSet will store all n elements from the input array.
GeeksforGeeks Problem: Longest Consecutive Subsequence with Java Solution
A Java solution for the 'Longest Consecutive Subsequence' problem, as seen on a coding platform. It uses a `HashSet` to efficiently check for consecutive numbers, implementing the optimal O(n) algorithm we've discussed.
Test your understanding!

Given the input array nums = [0, 3, 7, 2, 5, 8, 4, 6, 0, 1], trace the optimal algorithm.

  1. What will the HashSet contain after initialization?
  2. Which numbers will trigger the start of a sequence search (i.e., for which num is !set.contains(num - 1) true)?
  3. What is the final longestStreak returned?
Show answer
  1. The HashSet will contain the unique elements: {0, 1, 2, 3, 4, 5, 6, 7, 8}.

  2. The algorithm will iterate through the numbers in the set. The numbers that trigger a sequence search are those without a predecessor in the set:

    • When the algorithm checks 0, it finds that -1 is not in the set. So, a search starts.
    • When it checks 1, it finds 0 is in the set, so it skips.
    • When it checks 2, it finds 1 is in the set, so it skips.
    • ... and so on for 3, 4, 5, 6, 7, 8.
    • The only number that starts a search is 0.
  3. The search starting from 0 will find 1, 2, 3, 4, 5, 6, 7, 8 consecutively. The currentStreak will become 9. This will update longestStreak to 9. Since no other searches are started, the final returned value is 9.


Conclusion

In this lesson, you've learned how to solve the "Longest Consecutive Subsequence" problem optimally using a HashSet. This is a powerful demonstration of how choosing the correct data structure and applying a clever algorithmic trick can lead to significant performance improvements.

Key Takeaways:

  • A HashSet is ideal for problems requiring fast checks for the existence of an element.
  • The key to optimizing this problem is to avoid redundant work. By only starting a sequence count from a true "start" element (one whose predecessor is not in the set), we reduce the complexity from or down to .
  • Always analyze the time complexity carefully. Loops nested within each other don't automatically mean a polynomial complexity; you must consider how many times the inner loop's body executes in total across all iterations.

Preview of the Next Lesson:
We will be moving on to a new, specialized data structure within the "Hashing and Tries" module: the Trie (also known as a prefix tree). We will learn what a Trie is, how to implement its basic operations like insert and search, and see why it's incredibly efficient for problems involving strings and prefixes, such as implementing an autocomplete feature.