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:
- Insert all elements of the array into a
HashSetto get rid of duplicates and enable fast lookups. - 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:
- If the input array is empty, return 0.
- Create a
HashSetand add all elements from the input array to it. - Initialize a variable
longestStreak = 0. - Iterate through each unique number (
num) in theHashSet. - For each
num, check ifset.contains(num - 1).- If it does, do nothing and move to the next number. This
numis not the start of a sequence. - If it does not, then
numis the start of a new sequence.- Initialize
currentNum = numandcurrentStreak = 1. - Enter a
whileloop that continues as long asset.contains(currentNum + 1). - Inside the loop, increment
currentNumandcurrentStreak. - After the loop finishes, update
longestStreak = Math.max(longestStreak, currentStreak).
- Initialize
- If it does, do nothing and move to the next number. This
- 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
nelements to theHashSettakes time. - The outer
forloop runsntimes (fornunique elements). The innerwhileloop seems to make it , but it's not. Each number is visited by thewhileloop at most once over the entire execution. The total number of times thewhileloop's body is executed across all iterations of the outerforloop is at mostn. Therefore, the total time is .
- Adding
-
Space Complexity: .
- In the worst case (all elements are unique), the
HashSetwill store allnelements from the input array.
- In the worst case (all elements are unique), the
Test your understanding!
Given the input array nums = [0, 3, 7, 2, 5, 8, 4, 6, 0, 1], trace the optimal algorithm.
- What will the
HashSetcontain after initialization? - Which numbers will trigger the start of a sequence search (i.e., for which
numis!set.contains(num - 1)true)? - What is the final
longestStreakreturned?
Show answer
-
The
HashSetwill contain the unique elements:{0, 1, 2, 3, 4, 5, 6, 7, 8}. -
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-1is not in the set. So, a search starts. - When it checks
1, it finds0is in the set, so it skips. - When it checks
2, it finds1is 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.
- When the algorithm checks
-
The search starting from
0will find1, 2, 3, 4, 5, 6, 7, 8consecutively. ThecurrentStreakwill become 9. This will updatelongestStreakto 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
HashSetis 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.