665. Non-decreasing Array
Problem Description
You are given an array nums
containing n
integers. Your task is to determine whether this array can be made non-decreasing by modifying at most one element.
An array is considered non-decreasing if each element is less than or equal to the next element. Formally, nums[i] <= nums[i + 1]
must hold for all valid indices i
where 0 <= i <= n - 2
.
The key points to understand:
- You can modify at most one element (change its value to any number)
- You can also choose to modify zero elements if the array is already non-decreasing
- After the modification (if any), the entire array should satisfy the non-decreasing property
- Return
true
if it's possible to achieve a non-decreasing array with at most one modification, otherwise returnfalse
For example:
[4, 2, 3]
→ Can become[1, 2, 3]
or[4, 4, 4]
by changing one element, so returntrue
[4, 2, 1]
→ Cannot become non-decreasing by changing just one element, so returnfalse
[1, 2, 3]
→ Already non-decreasing, so returntrue
Intuition
When we scan through the array, we're looking for positions where the non-decreasing property is violated - that is, where nums[i] > nums[i + 1]
. If we find more than one such violation, it's impossible to fix the array with just one modification.
The key insight is that when we find a violation at position i
(where nums[i] > nums[i + 1]
), we have two choices to fix it:
- Lower
nums[i]
to matchnums[i + 1]
- This makes the current pair valid without affecting future comparisons - Raise
nums[i + 1]
to matchnums[i]
- This fixes the current pair but might create issues withnums[i + 1]
andnums[i + 2]
Why do we need to try both options? Consider these examples:
[3, 4, 2, 5]
: At the violation (4 > 2), if we raise 2 to 4, we get[3, 4, 4, 5]
✓[1, 4, 2, 3]
: At the violation (4 > 2), raising 2 to 4 gives[1, 4, 4, 3]
which is still invalid. But lowering 4 to 2 gives[1, 2, 2, 3]
✓
The strategy is straightforward: iterate through the array until we find the first violation. When found, try both possible fixes and check if either results in a valid non-decreasing array. If we never find a violation, the array is already non-decreasing.
This approach works because if there's exactly one violation, one of these two modifications must work if a solution exists. If neither works, or if we find multiple violations, then it's impossible to fix the array with just one change.
Solution Approach
The implementation follows a clean and efficient approach:
-
Helper Function for Validation: First, we define an
is_sorted
helper function that checks if an array is non-decreasing. It uses Python'spairwise
function to iterate through consecutive pairs and verifies that each pair satisfiesa <= b
. -
Main Algorithm:
-
Iterate through the array from index
0
ton-2
, checking each consecutive pair(nums[i], nums[i+1])
-
When we find the first violation where
nums[i] > nums[i+1]
:Try Option 1: Lower
nums[i]
to matchnums[i+1]
nums[i] = b # where b = nums[i+1]
- Check if the entire array is now sorted using
is_sorted(nums)
- If yes, return
True
Try Option 2: If Option 1 didn't work, raise
nums[i+1]
to matchnums[i]
nums[i] = nums[i + 1] = a # where a = original nums[i]
- This effectively restores
nums[i]
to its original value and setsnums[i+1]
equal to it - Check if the array is now sorted and return the result
- Check if the entire array is now sorted using
-
-
No Violation Found: If we complete the loop without finding any violation, the array is already non-decreasing, so return
True
Time Complexity: O(n)
- We iterate through the array at most twice (once in the main loop and once in is_sorted
)
Space Complexity: O(1)
- We only use a constant amount of extra space (modifying the array in-place)
The elegance of this solution lies in its simplicity - it directly tries both possible fixes when encountering a violation and immediately checks if either works, avoiding complex edge case handling.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the array [3, 4, 2, 5]
to demonstrate the solution approach.
Step 1: Initial scan for violations
- Compare pairs: (3,4) ✓, (4,2) ✗, (2,5) ✓
- Found violation at index 1:
nums[1] = 4 > nums[2] = 2
Step 2: Try Option 1 - Lower nums[1] to match nums[2]
- Set
nums[1] = 2
, array becomes[3, 2, 2, 5]
- Check if sorted: (3,2) ✗ - Not sorted!
- This option doesn't work
Step 3: Try Option 2 - Raise nums[2] to match nums[1]
- Restore
nums[1] = 4
and setnums[2] = 4
- Array becomes
[3, 4, 4, 5]
- Check if sorted: (3,4) ✓, (4,4) ✓, (4,5) ✓ - Sorted!
- Return
True
Let's trace through another example: [1, 4, 2, 3]
Step 1: Initial scan
- Compare pairs: (1,4) ✓, (4,2) ✗, (2,3) ✓
- Found violation at index 1:
nums[1] = 4 > nums[2] = 2
Step 2: Try Option 1 - Lower nums[1]
- Set
nums[1] = 2
, array becomes[1, 2, 2, 3]
- Check if sorted: (1,2) ✓, (2,2) ✓, (2,3) ✓ - Sorted!
- Return
True
For an already sorted array like [1, 2, 3, 4]
:
- Compare all pairs: (1,2) ✓, (2,3) ✓, (3,4) ✓
- No violations found
- Return
True
immediately
This approach efficiently handles all cases by trying both modification strategies only when needed.
Solution Implementation
1class Solution:
2 def checkPossibility(self, nums: List[int]) -> bool:
3 """
4 Check if array can become non-decreasing by modifying at most one element.
5
6 Args:
7 nums: List of integers to check
8
9 Returns:
10 True if array can be made non-decreasing with at most one modification
11 """
12
13 def is_non_decreasing(nums: List[int]) -> bool:
14 """
15 Helper function to check if array is non-decreasing.
16
17 Args:
18 nums: List of integers to check
19
20 Returns:
21 True if array is non-decreasing (each element <= next element)
22 """
23 from itertools import pairwise
24 return all(a <= b for a, b in pairwise(nums))
25
26 n = len(nums)
27
28 # Iterate through adjacent pairs to find first violation
29 for i in range(n - 1):
30 a, b = nums[i], nums[i + 1]
31
32 # If we find a decreasing pair (violation of non-decreasing order)
33 if a > b:
34 # Try strategy 1: Lower nums[i] to match nums[i+1]
35 nums[i] = b
36 if is_non_decreasing(nums):
37 return True
38
39 # Try strategy 2: Raise nums[i+1] to match nums[i]
40 # First restore nums[i] to original value
41 nums[i] = a
42 nums[i + 1] = a
43 return is_non_decreasing(nums)
44
45 # No violations found, array is already non-decreasing
46 return True
47
1class Solution {
2 /**
3 * Checks if array can become non-decreasing by modifying at most one element.
4 *
5 * @param nums The input array to check
6 * @return true if array can become non-decreasing with at most one modification, false otherwise
7 */
8 public boolean checkPossibility(int[] nums) {
9 // Iterate through the array to find the first violation of non-decreasing order
10 for (int i = 0; i < nums.length - 1; i++) {
11 int current = nums[i];
12 int next = nums[i + 1];
13
14 // Found a violation where current element is greater than next element
15 if (current > next) {
16 // Strategy 1: Try modifying current element to match next element
17 nums[i] = next;
18 if (isSorted(nums)) {
19 return true;
20 }
21
22 // Strategy 2: Restore original value and try modifying next element to match current
23 nums[i] = current;
24 nums[i + 1] = current;
25 return isSorted(nums);
26 }
27 }
28
29 // No violations found, array is already non-decreasing
30 return true;
31 }
32
33 /**
34 * Helper method to check if array is sorted in non-decreasing order.
35 *
36 * @param nums The array to check
37 * @return true if array is non-decreasing, false otherwise
38 */
39 private boolean isSorted(int[] nums) {
40 for (int i = 0; i < nums.length - 1; i++) {
41 if (nums[i] > nums[i + 1]) {
42 return false;
43 }
44 }
45 return true;
46 }
47}
48
1class Solution {
2public:
3 bool checkPossibility(vector<int>& nums) {
4 int n = nums.size();
5
6 // Iterate through the array to find the first violation
7 for (int i = 0; i < n - 1; ++i) {
8 int current = nums[i];
9 int next = nums[i + 1];
10
11 // Found a violation where current element is greater than next
12 if (current > next) {
13 // Strategy 1: Try modifying nums[i] to match nums[i+1]
14 nums[i] = next;
15 if (is_sorted(nums.begin(), nums.end())) {
16 return true;
17 }
18
19 // Strategy 2: Restore nums[i] and try modifying nums[i+1] to match nums[i]
20 nums[i] = current;
21 nums[i + 1] = current;
22 return is_sorted(nums.begin(), nums.end());
23 }
24 }
25
26 // No violations found, array is already non-decreasing
27 return true;
28 }
29};
30
1/**
2 * Checks if the array can become non-decreasing by modifying at most one element
3 * @param nums - The input array of numbers
4 * @returns true if the array can become non-decreasing with at most one modification, false otherwise
5 */
6function checkPossibility(nums: number[]): boolean {
7 /**
8 * Helper function to check if an array is sorted in non-decreasing order
9 * @param nums - The array to check
10 * @returns true if the array is sorted in non-decreasing order, false otherwise
11 */
12 const isSorted = (nums: number[]): boolean => {
13 for (let i = 0; i < nums.length - 1; i++) {
14 if (nums[i] > nums[i + 1]) {
15 return false;
16 }
17 }
18 return true;
19 };
20
21 // Iterate through the array to find the first violation of non-decreasing order
22 for (let i = 0; i < nums.length - 1; i++) {
23 const currentElement = nums[i];
24 const nextElement = nums[i + 1];
25
26 // Found a violation where current element is greater than next element
27 if (currentElement > nextElement) {
28 // Strategy 1: Try modifying the current element to match the next element
29 nums[i] = nextElement;
30 if (isSorted(nums)) {
31 return true;
32 }
33
34 // Strategy 2: Restore the current element and try modifying the next element instead
35 nums[i] = currentElement;
36 nums[i + 1] = currentElement;
37 return isSorted(nums);
38 }
39 }
40
41 // No violations found, array is already non-decreasing
42 return true;
43}
44
Time and Space Complexity
Time Complexity: O(n)
The algorithm iterates through the array once with the main loop from i = 0
to n - 2
, which takes O(n)
time. When a violation is found (where nums[i] > nums[i + 1]
), the algorithm performs at most two checks using the is_sorted
function. Each call to is_sorted
uses the pairwise
function which iterates through the array once, taking O(n)
time. Since we encounter at most one violation (if more than one exists, we return False
), the is_sorted
function is called at most twice. Therefore, the overall time complexity is O(n) + 2 * O(n) = O(n)
.
Space Complexity: O(1)
The algorithm modifies the input array in-place and only uses a constant amount of extra space for variables like i
, a
, b
, and n
. The pairwise
function in Python creates an iterator that doesn't store the entire paired sequence in memory, using only O(1)
additional space. The all()
function with a generator expression also operates with O(1)
space as it evaluates lazily. Therefore, the total space complexity is O(1)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Modifying the Original Array
The most critical pitfall in this solution is that it modifies the input array in-place. This can be problematic because:
- The caller might not expect their data to be mutated
- If the function is called multiple times with the same array, subsequent calls will operate on already-modified data
- It violates the principle of avoiding side effects in functions
Example of the problem:
nums = [4, 2, 3]
solution = Solution()
result1 = solution.checkPossibility(nums) # Returns True
print(nums) # Array has been modified! Could be [2, 2, 3] or [4, 4, 3]
result2 = solution.checkPossibility(nums) # Operates on modified array
2. Missing Import Statement
The pairwise
function from itertools
is used inside a nested function, but the import happens within that function. This can cause:
- Import errors if not handled properly
- Performance issues if the function is called repeatedly (though Python caches imports)
Solution to Fix These Pitfalls
from itertools import pairwise
from typing import List
class Solution:
def checkPossibility(self, nums: List[int]) -> bool:
"""
Check if array can become non-decreasing by modifying at most one element.
Args:
nums: List of integers to check
Returns:
True if array can be made non-decreasing with at most one modification
"""
def is_non_decreasing(arr: List[int]) -> bool:
"""Check if array is non-decreasing."""
return all(a <= b for a, b in pairwise(arr))
n = len(nums)
# Find the first violation
for i in range(n - 1):
if nums[i] > nums[i + 1]:
# Create copies to avoid modifying original array
# Strategy 1: Try lowering nums[i]
test_array1 = nums.copy()
test_array1[i] = nums[i + 1]
if is_non_decreasing(test_array1):
return True
# Strategy 2: Try raising nums[i+1]
test_array2 = nums.copy()
test_array2[i + 1] = nums[i]
return is_non_decreasing(test_array2)
# No violations found
return True
Alternative Space-Efficient Solution Without Copying: If you want to avoid creating copies but still not modify the original array, you can check the conditions without actually modifying any array:
class Solution:
def checkPossibility(self, nums: List[int]) -> bool:
n = len(nums)
violation_count = 0
for i in range(n - 1):
if nums[i] > nums[i + 1]:
violation_count += 1
if violation_count > 1:
return False
# Check if modifying nums[i] or nums[i+1] would work
# without actually modifying the array
if i > 0 and nums[i - 1] > nums[i + 1]:
# Can't lower nums[i] to nums[i+1] because it would violate
# the relationship with nums[i-1]
if i + 2 < n and nums[i] > nums[i + 2]:
# Can't raise nums[i+1] to nums[i] either
return False
return True
This alternative approach counts violations and checks feasibility without creating copies or modifying the original array, making it both space-efficient and side-effect free.
Which of the following is a min heap?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!