LeetCode 376 - Wiggle Subsequence
The problem asks us to find the length of the longest subsequence in an array such that the differences between consecutive elements alternate between positive and negative.
Difficulty: 🟡 Medium
Topics: Array, Dynamic Programming, Greedy
Solution
Problem Understanding
The problem asks us to find the length of the longest subsequence in an array such that the differences between consecutive elements alternate between positive and negative.
A wiggle sequence follows a strict alternating pattern:
- Positive difference, then negative difference, then positive again
- Or negative difference, then positive difference, then negative again
The actual values do not matter as much as the direction of change between adjacent elements.
For example, the sequence [1, 7, 4, 9, 2, 5] has differences:
7 - 1 = 6→ positive4 - 7 = -3→ negative9 - 4 = 5→ positive2 - 9 = -7→ negative5 - 2 = 3→ positive
Since the signs alternate perfectly, the entire sequence is a valid wiggle sequence.
The important detail is that we are looking for a subsequence, not necessarily a contiguous subarray. We are allowed to remove elements while preserving the original order.
The input is an integer array nums, and the output is a single integer representing the maximum possible length of a wiggle subsequence.
The constraints are:
1 <= nums.length <= 10000 <= nums[i] <= 1000
Since the array length is at most 1000, even an O(n^2) dynamic programming solution would pass. However, the follow-up explicitly asks whether we can achieve O(n) time, which strongly suggests there is a greedy optimization.
Several edge cases are important:
- Arrays with only one element should return
1 - Arrays where all elements are equal should return
1 - Strictly increasing or strictly decreasing arrays should return
2 - Consecutive duplicate values must be ignored because a difference of zero is neither positive nor negative
- Large alternating patterns should be handled efficiently without extra memory
Approaches
Brute Force Approach
A brute-force solution would generate every possible subsequence and check whether each one forms a valid wiggle sequence.
For each subsequence, we would compute consecutive differences and verify that their signs alternate strictly. We would then keep track of the maximum valid length.
This approach is correct because it exhaustively explores all possible subsequences. However, it is computationally infeasible because an array of length n has 2^n possible subsequences.
Even for n = 30, this becomes extremely large. Since the constraint allows up to 1000 elements, brute force is completely impractical.
Dynamic Programming Insight
The key observation is that we only care about the direction of the last difference.
At every position, we can maintain:
- The longest wiggle subsequence ending with a positive difference
- The longest wiggle subsequence ending with a negative difference
If the current number is larger than the previous one, we can extend a sequence that previously ended with a negative difference.
If the current number is smaller than the previous one, we can extend a sequence that previously ended with a positive difference.
This leads to a greedy optimization where we only track two values:
up→ longest wiggle subsequence ending in an upward movedown→ longest wiggle subsequence ending in a downward move
Because each element only depends on the previous state, we can solve the problem in linear time and constant space.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n * n) | O(n) | Generates every subsequence and validates each |
| Optimal Greedy DP | O(n) | O(1) | Tracks best upward and downward wiggle lengths |
Algorithm Walkthrough
Optimal Greedy Dynamic Programming Algorithm
- Initialize two variables:
up = 1down = 1
These represent the longest wiggle subsequence lengths ending in:
- A positive difference
- A negative difference
We start with 1 because a single element is trivially a wiggle sequence.
2. Iterate through the array starting from index 1.
3. Compare the current element with the previous element.
4. If nums[i] > nums[i - 1], we found a positive difference.
This means we can extend any sequence that previously ended with a negative difference.
Update:
up = down + 1
- If
nums[i] < nums[i - 1], we found a negative difference.
This means we can extend any sequence that previously ended with a positive difference.
Update:
down = up + 1
- If the values are equal, do nothing.
Equal values produce a difference of zero, which does not contribute to a wiggle sequence.
7. Continue until the end of the array.
8. Return the maximum of up and down.
Why it works
The algorithm maintains an invariant:
upalways stores the longest wiggle subsequence ending with a positive differencedownalways stores the longest wiggle subsequence ending with a negative difference
Whenever we encounter a positive difference, the only valid extension is from a sequence ending in a negative difference. Similarly, a negative difference can only extend a sequence ending in a positive difference.
Because we only need the best lengths so far, we never need to store the actual subsequences. This allows the solution to run in constant space while still guaranteeing optimality.
Python Solution
from typing import List
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
if not nums:
return 0
up = 1
down = 1
for i in range(1, len(nums)):
if nums[i] > nums[i - 1]:
up = down + 1
elif nums[i] < nums[i - 1]:
down = up + 1
return max(up, down)
The implementation begins by handling the empty array case, although the problem guarantees at least one element.
The variables up and down both start at 1 because any single element forms a valid wiggle subsequence.
The loop processes adjacent pairs of elements. When the current element is larger than the previous one, we extend a downward sequence into an upward sequence. When the current element is smaller, we extend an upward sequence into a downward sequence.
Equal values are ignored because they do not contribute to alternation.
Finally, the algorithm returns the larger of the two states since the optimal sequence could end in either direction.
Go Solution
func wiggleMaxLength(nums []int) int {
if len(nums) == 0 {
return 0
}
up := 1
down := 1
for i := 1; i < len(nums); i++ {
if nums[i] > nums[i-1] {
up = down + 1
} else if nums[i] < nums[i-1] {
down = up + 1
}
}
if up > down {
return up
}
return down
}
The Go implementation follows the same logic as the Python solution.
Go does not have a built-in max function for integers, so the final comparison is done manually.
Slices in Go behave similarly to dynamic arrays, and integer overflow is not a concern because the maximum subsequence length is at most 1000.
Worked Examples
Example 1
Input:
nums = [1,7,4,9,2,5]
| i | nums[i-1] | nums[i] | Difference | up | down |
|---|---|---|---|---|---|
| Start | - | - | - | 1 | 1 |
| 1 | 1 | 7 | Positive | 2 | 1 |
| 2 | 7 | 4 | Negative | 2 | 3 |
| 3 | 4 | 9 | Positive | 4 | 3 |
| 4 | 9 | 2 | Negative | 4 | 5 |
| 5 | 2 | 5 | Positive | 6 | 5 |
Final answer:
6
The entire sequence forms a valid wiggle sequence.
Example 2
Input:
nums = [1,17,5,10,13,15,10,5,16,8]
| i | nums[i-1] | nums[i] | Difference | up | down |
|---|---|---|---|---|---|
| Start | - | - | - | 1 | 1 |
| 1 | 1 | 17 | Positive | 2 | 1 |
| 2 | 17 | 5 | Negative | 2 | 3 |
| 3 | 5 | 10 | Positive | 4 | 3 |
| 4 | 10 | 13 | Positive | 4 | 3 |
| 5 | 13 | 15 | Positive | 4 | 3 |
| 6 | 15 | 10 | Negative | 4 | 5 |
| 7 | 10 | 5 | Negative | 4 | 5 |
| 8 | 5 | 16 | Positive | 6 | 5 |
| 9 | 16 | 8 | Negative | 6 | 7 |
Final answer:
7
Example 3
Input:
nums = [1,2,3,4,5,6,7,8,9]
| i | nums[i-1] | nums[i] | Difference | up | down |
|---|---|---|---|---|---|
| Start | - | - | - | 1 | 1 |
| 1 | 1 | 2 | Positive | 2 | 1 |
| 2 | 2 | 3 | Positive | 2 | 1 |
| 3 | 3 | 4 | Positive | 2 | 1 |
| 4 | 4 | 5 | Positive | 2 | 1 |
| 5 | 5 | 6 | Positive | 2 | 1 |
| 6 | 6 | 7 | Positive | 2 | 1 |
| 7 | 7 | 8 | Positive | 2 | 1 |
| 8 | 8 | 9 | Positive | 2 | 1 |
Final answer:
2
Only two elements can form a valid wiggle sequence because the differences never alternate.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each element is processed exactly once |
| Space | O(1) | Only two integer variables are maintained |
The algorithm performs a single pass through the array, making the runtime linear in the number of elements.
The space complexity is constant because no additional arrays or data structures are used. Regardless of input size, only the up and down variables are stored.
Test Cases
sol = Solution()
assert sol.wiggleMaxLength([1,7,4,9,2,5]) == 6 # classic alternating sequence
assert sol.wiggleMaxLength([1,17,5,10,13,15,10,5,16,8]) == 7 # mixed pattern
assert sol.wiggleMaxLength([1,2,3,4,5,6,7,8,9]) == 2 # strictly increasing
assert sol.wiggleMaxLength([9,8,7,6,5]) == 2 # strictly decreasing
assert sol.wiggleMaxLength([1]) == 1 # single element
assert sol.wiggleMaxLength([1,1,1,1]) == 1 # all duplicates
assert sol.wiggleMaxLength([1,2]) == 2 # two increasing elements
assert sol.wiggleMaxLength([2,1]) == 2 # two decreasing elements
assert sol.wiggleMaxLength([1,1,2]) == 2 # leading duplicates
assert sol.wiggleMaxLength([3,3,3,2,5]) == 3 # duplicates before valid wiggle
assert sol.wiggleMaxLength([1,7,7,7,4]) == 3 # repeated equal values in middle
assert sol.wiggleMaxLength([0,0]) == 1 # equal pair
assert sol.wiggleMaxLength([1,3,2,4,3,5]) == 6 # perfect alternation
assert sol.wiggleMaxLength([1,2,2,3,2]) == 3 # duplicates mixed with alternation
Test Summary
| Test | Why |
|---|---|
[1,7,4,9,2,5] |
Standard alternating sequence |
[1,17,5,10,13,15,10,5,16,8] |
Complex mixed pattern |
[1,2,3,4,5,6,7,8,9] |
Strictly increasing case |
[9,8,7,6,5] |
Strictly decreasing case |
[1] |
Minimum input size |
[1,1,1,1] |
All equal values |
[1,2] |
Two-element increasing case |
[2,1] |
Two-element decreasing case |
[1,1,2] |
Leading duplicates |
[3,3,3,2,5] |
Duplicate compression behavior |
[1,7,7,7,4] |
Equal values inside sequence |
[0,0] |
Equal adjacent pair |
[1,3,2,4,3,5] |
Ideal wiggle pattern |
[1,2,2,3,2] |
Alternation with duplicates |
Edge Cases
All Elements Are Equal
An input like [5,5,5,5] is tricky because every difference is zero. A naive implementation might incorrectly count equal values as part of the wiggle sequence.
The correct answer is 1 because only a single element can form a valid wiggle sequence when no positive or negative differences exist.
The implementation handles this naturally by ignoring equal adjacent values.
Strictly Increasing or Decreasing Arrays
Arrays like [1,2,3,4,5] or [5,4,3,2,1] never alternate direction.
A common mistake is to count the entire sequence, but the longest valid wiggle subsequence only has length 2.
The algorithm correctly keeps either up or down fixed at 2 while the other remains 1.
Duplicate Values Between Direction Changes
Inputs like [1,7,7,7,4] can expose bugs if equal values are treated incorrectly.
The repeated 7s should not affect the wiggle structure because they produce zero differences.
The implementation ignores equal values entirely, ensuring duplicates neither increase nor reset the sequence length.