769. Max Chunks To Make Sorted
Problem Description
You have an integer array arr
of length n
that contains a permutation of all integers from 0
to n-1
(each number appears exactly once).
Your task is to split this array into chunks (consecutive subarrays), where each chunk can be sorted independently. After sorting each chunk individually and then concatenating them back together, the final result should be the same as if you had sorted the entire array.
For example, if arr = [1,0,2,3,4]
, you could split it into chunks [1,0]
and [2,3,4]
. After sorting each chunk, you get [0,1]
and [2,3,4]
, which when concatenated gives [0,1,2,3,4]
- the sorted array.
The goal is to find the maximum number of chunks you can create while still maintaining this property.
The key insight is that since the array contains a permutation of [0, 1, 2, ..., n-1]
, in the final sorted array, each element i
should be at position i
. A chunk can end at position i
if and only if all elements from 0
to i
have been seen within the first i+1
positions. This happens when the maximum value encountered so far equals the current index i
.
Intuition
Let's think about what makes a valid chunk. Since we want the concatenated sorted chunks to equal the fully sorted array, we need to understand when we can safely "cut" the array.
In a sorted permutation of [0, 1, 2, ..., n-1]
, element 0
should be at index 0
, element 1
at index 1
, and so on. Each element's value equals its target position.
Consider this key observation: if we've seen all numbers from 0
to i
within the first i+1
positions of the array, then we can make a cut after position i
. Why? Because after sorting this chunk, all these numbers will be in their correct final positions (0
through i
), and they won't need to move beyond this chunk.
How can we check if we've seen all numbers from 0
to i
? Since our array is a permutation, if the maximum value we've encountered up to position i
is exactly i
, then we must have seen all values from 0
to i
. This is because:
- We have
i+1
positions (from index0
toi
) - The maximum value seen is
i
- Since it's a permutation with no duplicates, we must have exactly the numbers
0, 1, 2, ..., i
in some order
For example, if at index 2
the maximum value seen so far is 2
, we know we've seen values 0
, 1
, and 2
in the first three positions. These can be sorted independently without affecting the rest of the array.
This leads to our greedy approach: traverse the array, track the maximum value seen so far, and whenever this maximum equals the current index, we can create a chunk boundary there.
Learn more about Stack, Greedy, Sorting and Monotonic Stack patterns.
Solution Approach
The solution implements a greedy one-pass algorithm that tracks the maximum value seen so far and counts valid chunk boundaries.
Algorithm Steps:
-
Initialize two variables:
mx = 0
: tracks the maximum value seen so farans = 0
: counts the number of chunks
-
Iterate through the array using enumeration to get both index
i
and valuev
:- Update the maximum:
mx = max(mx, v)
- Check if we can make a cut: if
i == mx
, it means all numbers from0
toi
are present in the firsti+1
positions - If we can make a cut, increment the chunk count:
ans += 1
- Update the maximum:
-
Return the total number of chunks
Why this works:
When i == mx
, it guarantees that:
- The largest number we've seen is
i
- We've processed
i+1
elements (indices0
throughi
) - Since the array is a permutation without duplicates, we must have exactly the numbers
{0, 1, 2, ..., i}
in some order - These numbers can be sorted independently to occupy positions
0
throughi
in the final sorted array
Example walkthrough:
For arr = [1, 0, 2, 3, 4]
:
i=0, v=1
:mx=1
,i≠mx
, no chunki=1, v=0
:mx=1
,i=mx=1
, create chunk →ans=1
i=2, v=2
:mx=2
,i=mx=2
, create chunk →ans=2
i=3, v=3
:mx=3
,i=mx=3
, create chunk →ans=3
i=4, v=4
:mx=4
,i=mx=4
, create chunk →ans=4
Result: 4 chunks [1,0]
, [2]
, [3]
, [4]
Time Complexity: O(n)
- single pass through the array
Space Complexity: O(1)
- only using two variables
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 solution with arr = [0, 2, 1, 4, 3]
:
Initial state: mx = 0
, ans = 0
Step 1: Index 0, Value 0
- Update maximum:
mx = max(0, 0) = 0
- Check if we can create a chunk:
i == mx
→0 == 0
✓ - We've seen all numbers from 0 to 0 (just the number 0) in the first position
- Increment chunk count:
ans = 1
- First chunk identified: [0]
Step 2: Index 1, Value 2
- Update maximum:
mx = max(0, 2) = 2
- Check if we can create a chunk:
i == mx
→1 == 2
✗ - Cannot create a chunk here because we've seen value 2 but we're only at index 1
- This means we need at least one more element to complete this chunk
Step 3: Index 2, Value 1
- Update maximum:
mx = max(2, 1) = 2
- Check if we can create a chunk:
i == mx
→2 == 2
✓ - We've seen all numbers from 0 to 2 (values: 0, 2, 1) in the first 3 positions
- Increment chunk count:
ans = 2
- Second chunk identified: [2, 1]
Step 4: Index 3, Value 4
- Update maximum:
mx = max(2, 4) = 4
- Check if we can create a chunk:
i == mx
→3 == 4
✗ - Cannot create a chunk because maximum value 4 indicates we need to reach index 4
Step 5: Index 4, Value 3
- Update maximum:
mx = max(4, 3) = 4
- Check if we can create a chunk:
i == mx
→4 == 4
✓ - We've seen all numbers from 0 to 4 (values: 0, 2, 1, 4, 3) in all 5 positions
- Increment chunk count:
ans = 3
- Third chunk identified: [4, 3]
Final Result: 3 chunks: [0] | [2, 1] | [4, 3]
Verification:
- Sort each chunk:
[0] | [1, 2] | [3, 4]
- Concatenate:
[0, 1, 2, 3, 4]
✓ (correctly sorted array)
Solution Implementation
1class Solution:
2 def maxChunksToSorted(self, arr: List[int]) -> int:
3 # Initialize maximum value seen so far and chunk count
4 max_value = 0
5 chunk_count = 0
6
7 # Iterate through array with index and value
8 for index, value in enumerate(arr):
9 # Update the maximum value encountered so far
10 max_value = max(max_value, value)
11
12 # If current index equals max value, we can form a valid chunk
13 # This means all elements from start to current position
14 # contain exactly the values 0 to index
15 if index == max_value:
16 chunk_count += 1
17
18 return chunk_count
19
1class Solution {
2 public int maxChunksToSorted(int[] arr) {
3 // Initialize counter for number of chunks and track maximum value seen so far
4 int chunkCount = 0;
5 int maxValueSoFar = 0;
6
7 // Iterate through each element in the array
8 for (int i = 0; i < arr.length; i++) {
9 // Update the maximum value encountered up to current index
10 maxValueSoFar = Math.max(maxValueSoFar, arr[i]);
11
12 // Check if we can form a chunk at current position
13 // A chunk can be formed when the maximum value seen so far equals the current index
14 // This means all elements from 0 to i are present in the range [0, i]
15 if (i == maxValueSoFar) {
16 chunkCount++;
17 }
18 }
19
20 // Return the total number of chunks that can be formed
21 return chunkCount;
22 }
23}
24
1class Solution {
2public:
3 int maxChunksToSorted(vector<int>& arr) {
4 int chunkCount = 0; // Number of chunks that can be formed
5 int maxValueSoFar = 0; // Maximum value encountered up to current index
6
7 // Iterate through each element in the array
8 for (int i = 0; i < arr.size(); ++i) {
9 // Update the maximum value seen so far
10 maxValueSoFar = max(maxValueSoFar, arr[i]);
11
12 // If the maximum value equals the current index,
13 // it means all elements from 0 to i can form a valid chunk
14 // because after sorting this chunk, elements will be in positions [0, i]
15 if (i == maxValueSoFar) {
16 chunkCount++;
17 }
18 }
19
20 return chunkCount;
21 }
22};
23
1/**
2 * Finds the maximum number of chunks that can be made to sort the array.
3 * Each chunk can be sorted independently, and concatenating them results in a sorted array.
4 *
5 * @param arr - Input array containing integers from 0 to n-1
6 * @returns The maximum number of chunks
7 */
8function maxChunksToSorted(arr: number[]): number {
9 const arrayLength: number = arr.length;
10 let chunkCount: number = 0;
11 let maxValueSoFar: number = 0;
12
13 // Iterate through the array to find valid chunk boundaries
14 for (let currentIndex: number = 0; currentIndex < arrayLength; currentIndex++) {
15 // Update the maximum value encountered so far
16 maxValueSoFar = Math.max(arr[currentIndex], maxValueSoFar);
17
18 // If the maximum value equals the current index, we can create a chunk
19 // This works because all values from 0 to currentIndex must be present
20 // in the subarray [0...currentIndex] for it to be sortable independently
21 if (maxValueSoFar === currentIndex) {
22 chunkCount++;
23 }
24 }
25
26 return chunkCount;
27}
28
Time and Space Complexity
The time complexity of this algorithm is O(n)
, where n
is the length of the array arr
. This is because the code iterates through the array exactly once using a single for loop with enumerate(arr)
. Each operation inside the loop (comparison with max()
, checking equality, and incrementing counter) takes constant time O(1)
.
The space complexity is O(1)
as the algorithm only uses a fixed amount of extra space regardless of the input size. The variables mx
and ans
are simple integers that don't grow with the input size, and the loop variables i
and v
also use constant space.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding the Chunk Boundary Condition
The Mistake:
Developers often incorrectly assume that a chunk can end whenever the current element equals its index (arr[i] == i
), thinking this means the element is "in the right place."
# INCORRECT approach
def maxChunksToSorted(self, arr: List[int]) -> int:
chunk_count = 0
for i, v in enumerate(arr):
if v == i: # Wrong condition!
chunk_count += 1
return chunk_count
Why it fails:
Consider arr = [2, 0, 1, 3]
. At index 3, arr[3] == 3
, but you cannot split here into [2, 0, 1]
and [3]
because sorting [2, 0, 1]
gives [0, 1, 2]
, not the required positions.
The Fix:
Use the maximum value condition (max_value == index
) instead, which ensures all values from 0 to index are present in the first index+1 positions.
Pitfall 2: Off-by-One Errors with Array Bounds
The Mistake:
Confusion about whether to check max_value == index
or max_value == index + 1
, especially when thinking about "we've seen index+1 elements."
# INCORRECT - using wrong comparison
def maxChunksToSorted(self, arr: List[int]) -> int:
max_value = 0
chunk_count = 0
for i, v in enumerate(arr):
max_value = max(max_value, v)
if max_value == i + 1: # Wrong! This doesn't align with 0-indexed arrays
chunk_count += 1
return chunk_count
The Fix: Remember that in a 0-indexed array containing values 0 to n-1:
- At index
i
, we've seeni+1
elements - The maximum possible value among these elements should be
i
(noti+1
) - Therefore, the correct condition is
max_value == i
Pitfall 3: Forgetting to Handle Edge Cases
The Mistake: Not considering special cases like single-element arrays or already sorted arrays.
# Potentially problematic if not careful
def maxChunksToSorted(self, arr: List[int]) -> int:
if not arr: # Unnecessary check - problem guarantees non-empty array
return 0
# ... rest of code
Why the original solution works: The algorithm naturally handles all edge cases:
- Single element
[0]
: Returns 1 chunk correctly - Already sorted
[0,1,2,3]
: Returns n chunks (maximum possible) - Reverse sorted
[3,2,1,0]
: Returns 1 chunk (minimum possible)
Pitfall 4: Incorrect Maximum Value Initialization
The Mistake: Initializing the maximum value to a wrong starting value, such as the first element or negative infinity.
# INCORRECT initialization
def maxChunksToSorted(self, arr: List[int]) -> int:
max_value = arr[0] # Wrong! Should start at 0 or handle first element specially
chunk_count = 0
for i in range(1, len(arr)): # Missing first element
max_value = max(max_value, arr[i])
if max_value == i:
chunk_count += 1
return chunk_count
The Fix:
Initialize max_value = 0
or -1
(since all values are non-negative), and process all elements from index 0. The algorithm works because we're guaranteed the array contains exactly the values 0 through n-1.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!