376. Wiggle Subsequence
376. Wiggle Subsequence
Problem Statement
A wiggle sequence is a sequence where the differences between successive numbers strictly alternate between positive and negative. The first difference (if one exists) may be either positive or negative.
- For example,
[1, 7, 4, 9, 2, 5]is a wiggle sequence because the differences(6, -3, 5, -7, 3)alternate between positive and negative. - In contrast,
[1, 4, 7, 2, 5]and[1, 7, 4, 5, 5]are not wiggle sequences. The first is not because its first two differences are positive, and the second is not because its last difference is zero.
A subsequence is obtained by deleting some (possibly zero) elements from the original sequence, leaving the remaining elements in their original order.
Given an integer array nums, return the length of the longest wiggle subsequence of nums.
Examples
Example 1:
Input: nums = [1,7,4,9,2,5]
Output: 6
Explanation: The entire sequence is a wiggle sequence because differences (6, -3, 5, -7, 3) alternate.
Example 2:
Input: nums = [1,17,5,10,13,15,10,5,16,8]
Output: 7
Explanation: There are several subsequences that achieve this length.
One is [1,17,10,13,10,16,8] with differences (16, -7, 3, -3, 6, -8).
Example 3:
Input: nums = [1,2,3,4,5,6,7,8,9]
Output: 2
Explanation: The longest wiggle subsequence is [1, 2] or [1, 9] with difference 1 or 8.
Constraints
1 <= nums.length <= 10000 <= nums[i] <= 1000
Clarification Questions
Before diving into the solution, here are 5 important clarifications and assumptions to discuss during an interview:
-
Wiggle definition: What makes a sequence “wiggle”? (Assumption: Differences between consecutive elements alternate between positive and negative - up-down-up or down-up-down pattern)
-
Subsequence definition: Does subsequence need to be contiguous? (Assumption: No - subsequence maintains relative order but can skip elements)
-
Length requirement: What is the minimum length of a wiggle subsequence? (Assumption: Length 1 is valid, length 2 is valid if numbers differ)
-
Equal values: What if consecutive elements are equal? (Assumption: Equal values break the wiggle pattern - need to skip them or clarify handling)
-
Return value: Should we return the length or the subsequence itself? (Assumption: Return the length - integer representing longest wiggle subsequence length)
Interview Deduction Process (20 minutes)
Step 1: Brute-Force Approach (5 minutes)
Try all possible subsequences and check which ones are wiggle sequences. For each subsequence, verify it alternates between increasing and decreasing. This approach has exponential complexity as we try all 2^n subsequences, which is infeasible for large arrays.
Step 2: Semi-Optimized Approach (7 minutes)
Use dynamic programming: dp[i][direction] = longest wiggle subsequence ending at index i with given direction (up or down). For each position, check all previous positions and update dp based on whether we can extend the wiggle sequence. This requires O(n²) time and O(n) space, which works but can be optimized further.
Step 3: Optimized Solution (8 minutes)
Use greedy approach: track the current direction (up or down) and the last element in the wiggle sequence. For each new element, if it continues the wiggle pattern (e.g., was going up, now going down), extend the sequence. If it breaks the pattern, decide whether to replace the last element (if it’s better) or skip. Alternatively, count the number of direction changes: whenever the difference changes sign (positive to negative or vice versa), we have a wiggle. This achieves O(n) time with O(1) space, which is optimal. The key insight is that we only need to track the last direction and the last value, not all previous states.
Solution Approach
This is a greedy algorithm problem. The key insight is to count the number of times the difference between consecutive elements changes sign. We only need to track when the sign alternates, not the actual values.
Key Insights:
- Difference Tracking: Track the difference between consecutive elements
- Sign Alternation: Count when the sign of the difference changes
- Greedy Choice: Include an element if it causes a sign change
- Skip Equal Elements: If difference is 0, don’t count it (no sign change)
Algorithm:
- Handle Edge Cases: If array has less than 2 elements, return its length
- Initialize: Calculate first difference, set count to 2 if non-zero, else 1
- Process Remaining: For each element, calculate difference and check if sign alternates
- Count Alternations: Increment count when sign changes
Solution
Solution: Greedy with Difference Tracking
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
const int N = nums.size();
if(N < 2) return N;
int prevDiff = nums[1] - nums[0];
int rtn = prevDiff != 0? 2 : 1;
for(int i = 2; i < N; i++) {
int diff = nums[i] - nums[i - 1];
if((diff > 0 && prevDiff <= 0) ||
diff < 0 && prevDiff >= 0) {
rtn++;
prevDiff = diff;
}
}
return rtn;
}
};
Algorithm Explanation:
- Edge Case Check (Lines 4-5):
- If array has less than 2 elements, return its length
- Single element is always a valid wiggle sequence
- Initialize (Lines 7-8):
prevDiff: Difference between first two elementsrtn: Length of wiggle subsequence- If
prevDiff != 0: Start with 2 elements (both are part of wiggle) - If
prevDiff == 0: Start with 1 element (only first element counts)
- If
- Process Remaining Elements (Lines 9-16):
- For each element from index 2 to end:
- Calculate difference:
diff = nums[i] - nums[i - 1] - Check sign alternation:
- If
diff > 0andprevDiff <= 0: Sign changed from non-positive to positive - If
diff < 0andprevDiff >= 0: Sign changed from non-negative to negative
- If
- If sign alternates:
- Increment count:
rtn++ - Update previous difference:
prevDiff = diff
- Increment count:
- If sign doesn’t alternate: Skip this element (don’t update
prevDiff)
- Calculate difference:
- For each element from index 2 to end:
- Return (Line 17):
- Return the length of the longest wiggle subsequence
Why This Works:
Key Insight: We only need to count elements where the difference sign alternates. Elements that don’t cause a sign change can be skipped.
Greedy Choice:
- If the current difference has a different sign than the previous difference, include the element
- This locally optimal choice (include when sign changes) leads to a globally optimal solution
Why Skip Equal Differences:
- If
diff == 0, there’s no sign change, so we don’t include the element - We also don’t update
prevDiffwhendiff == 0, so we keep the last non-zero difference
Example Walkthrough:
Example 1: nums = [1,7,4,9,2,5]
Execution:
Initial: N = 6
prevDiff = nums[1] - nums[0] = 7 - 1 = 6 (positive)
rtn = 2 (since prevDiff != 0)
i=2: nums[2]=4
diff = 4 - 7 = -3 (negative)
Check: diff < 0 && prevDiff >= 0? Yes (-3 < 0 && 6 >= 0)
rtn = 2 + 1 = 3
prevDiff = -3
State: rtn=3, prevDiff=-3
i=3: nums[3]=9
diff = 9 - 4 = 5 (positive)
Check: diff > 0 && prevDiff <= 0? Yes (5 > 0 && -3 <= 0)
rtn = 3 + 1 = 4
prevDiff = 5
State: rtn=4, prevDiff=5
i=4: nums[4]=2
diff = 2 - 9 = -7 (negative)
Check: diff < 0 && prevDiff >= 0? Yes (-7 < 0 && 5 >= 0)
rtn = 4 + 1 = 5
prevDiff = -7
State: rtn=5, prevDiff=-7
i=5: nums[5]=5
diff = 5 - 2 = 3 (positive)
Check: diff > 0 && prevDiff <= 0? Yes (3 > 0 && -7 <= 0)
rtn = 5 + 1 = 6
prevDiff = 3
State: rtn=6, prevDiff=3
Result: 6
Wiggle sequence: [1, 7, 4, 9, 2, 5]
Differences: (6, -3, 5, -7, 3) - all alternate
Example 2: nums = [1,17,5,10,13,15,10,5,16,8]
Execution:
Initial: N = 10
prevDiff = 17 - 1 = 16 (positive)
rtn = 2
i=2: nums[2]=5
diff = 5 - 17 = -12 (negative)
Check: diff < 0 && prevDiff >= 0? Yes
rtn = 3, prevDiff = -12
i=3: nums[3]=10
diff = 10 - 5 = 5 (positive)
Check: diff > 0 && prevDiff <= 0? Yes
rtn = 4, prevDiff = 5
i=4: nums[4]=13
diff = 13 - 10 = 3 (positive)
Check: diff > 0 && prevDiff <= 0? No (both positive)
Skip (don't update prevDiff)
i=5: nums[5]=15
diff = 15 - 13 = 2 (positive)
Check: diff > 0 && prevDiff <= 0? No (prevDiff is still 5, positive)
Skip
i=6: nums[6]=10
diff = 10 - 15 = -5 (negative)
Check: diff < 0 && prevDiff >= 0? Yes (prevDiff is 5, positive)
rtn = 5, prevDiff = -5
i=7: nums[7]=5
diff = 5 - 10 = -5 (negative)
Check: diff < 0 && prevDiff >= 0? No (both negative)
Skip
i=8: nums[8]=16
diff = 16 - 5 = 11 (positive)
Check: diff > 0 && prevDiff <= 0? Yes
rtn = 6, prevDiff = 11
i=9: nums[9]=8
diff = 8 - 16 = -8 (negative)
Check: diff < 0 && prevDiff >= 0? Yes
rtn = 7, prevDiff = -8
Result: 7
Wiggle sequence: [1, 17, 5, 10, 13, 10, 16, 8] (or similar)
Example 3: nums = [1,2,3,4,5,6,7,8,9]
Execution:
Initial: N = 9
prevDiff = 2 - 1 = 1 (positive)
rtn = 2
i=2: nums[2]=3
diff = 3 - 2 = 1 (positive)
Check: diff > 0 && prevDiff <= 0? No (both positive)
Skip
i=3: nums[3]=4
diff = 4 - 3 = 1 (positive)
Check: diff > 0 && prevDiff <= 0? No
Skip
... (all differences are positive, so no sign changes)
Result: 2
Wiggle sequence: [1, 2] or [1, 9]
Algorithm Breakdown
Why Greedy Works
Greedy Choice Property: At each position, if the sign of the difference alternates, include the element. This is optimal because:
- No Regret: Including an element that causes a sign change always increases the wiggle length
- Optimal Substructure: The longest wiggle subsequence ending at position
idepends only on the last sign - Local Optimality: Making the locally optimal choice (include when sign changes) leads to global optimum
Why Skip Equal Differences
Zero Difference: When diff == 0, there’s no sign change, so:
- We don’t include the element in the wiggle sequence
- We don’t update
prevDiff(keep the last non-zero difference) - This allows us to “skip” equal elements and wait for the next sign change
Example: nums = [1, 2, 2, 3]
- Differences:
(1, 0, 1) - We skip the middle
2(diff = 0) - Result:
[1, 2, 3]with differences(1, 1)- but wait, that’s not a wiggle…
Actually, let me reconsider. If we have [1, 2, 2, 3]:
prevDiff = 1(positive)- At
2, 2:diff = 0, we skip,prevDiffstays1 - At
2, 3:diff = 1(positive), same sign asprevDiff, so we skip - Result:
[1, 2, 3]is not a wiggle, but the algorithm returns 2 (correct:[1, 2]or[1, 3])
Sign Alternation Logic
Condition: (diff > 0 && prevDiff <= 0) || (diff < 0 && prevDiff >= 0)
Why <= and >= instead of < and >?
- We want to count when sign changes from non-positive to positive, or non-negative to negative
- This handles the case where
prevDiff == 0(we want to count the first non-zero difference after zeros) - Example:
[1, 1, 2]→ differences:(0, 1)prevDiff = 0,diff = 1diff > 0 && prevDiff <= 0→ true, so we count it
Time & Space Complexity
- Time Complexity: O(n) where n is the length of
nums- Single pass through the array
- Each iteration does O(1) work
- Space Complexity: O(1)
- Only using a few variables (
prevDiff,rtn,diff,i) - No additional data structures
- Only using a few variables (
Key Points
- Greedy Algorithm: Count elements where difference sign alternates
- Difference Tracking: Track the sign of differences, not the values
- Skip Zeros: Don’t count elements with zero difference
- Sign Alternation: Use
<=and>=to handle zero differences correctly - Optimal: This greedy strategy finds the longest wiggle subsequence
Alternative Approaches
Approach 1: Greedy with Difference Tracking (Current Solution)
- Time: O(n)
- Space: O(1)
- Best for: Optimal solution, most efficient
Approach 2: Dynamic Programming
- Time: O(n)
- Space: O(n)
- Idea: Track two states:
up[i](ending with up) anddown[i](ending with down) - Code:
int wiggleMaxLength(vector<int>& nums) { int n = nums.size(); vector<int> up(n, 1), down(n, 1); for(int i = 1; i < n; i++) { if(nums[i] > nums[i-1]) { up[i] = down[i-1] + 1; down[i] = down[i-1]; } else if(nums[i] < nums[i-1]) { down[i] = up[i-1] + 1; up[i] = up[i-1]; } else { up[i] = up[i-1]; down[i] = down[i-1]; } } return max(up[n-1], down[n-1]); }
Approach 3: Space-Optimized DP
- Time: O(n)
- Space: O(1)
- Idea: Only track
upanddownfor current position (similar to greedy)
Edge Cases
- Single element:
[1]→ return 1 - Two elements (different):
[1, 2]→ return 2 - Two elements (same):
[1, 1]→ return 1 - All increasing:
[1,2,3,4,5]→ return 2 - All decreasing:
[5,4,3,2,1]→ return 2 - All equal:
[1,1,1,1]→ return 1 - Perfect wiggle:
[1,7,4,9,2,5]→ return 6
Common Mistakes
- Wrong sign check: Using
>and<instead of>=and<= - Not handling zeros: Forgetting to skip elements with zero difference
- Not updating prevDiff: Updating
prevDiffeven when sign doesn’t change - Wrong initialization: Not handling the case where first difference is zero
- Off-by-one errors: Incorrect loop bounds or initialization
Related Problems
- 300. Longest Increasing Subsequence - Similar subsequence problem
- 53. Maximum Subarray - Subarray problem
- 392. Is Subsequence - Subsequence matching
- 121. Best Time to Buy and Sell Stock - Similar pattern
Follow-Up: Why <= and >= Instead of < and >?
Question: Why do we use prevDiff <= 0 and prevDiff >= 0 instead of prevDiff < 0 and prevDiff > 0?
Answer:
- We want to count the first non-zero difference after a sequence of zeros
- If
prevDiff == 0, we haven’t established a sign yet - When we see the first non-zero difference, we should count it
- Example:
[1, 1, 2]→ differences:(0, 1)prevDiff = 0,diff = 1- With
prevDiff <= 0:1 > 0 && 0 <= 0→ true, count it ✓ - With
prevDiff < 0:1 > 0 && 0 < 0→ false, don’t count ✗
Therefore: Using <= and >= correctly handles the transition from zero to non-zero differences.
Tags
Array, Dynamic Programming, Greedy, Medium