Core patterns · Lesson 15 of 62 · about 50 min
Arrays & matrices
Kadane's algorithm and its variations for maximum subarrays, matrix traversal and rotation, and in-place tricks like the Dutch national flag and the reversal rotation.
On this page · 8 sections
Maximum subarray: from O(n³) to O(n)#
"Find the contiguous subarray with the largest sum." The obvious solution tries every start and every end and adds up the middle: O(n³). Keeping a running sum for each start brings it to O(n²). Kadane's insight brings it to O(n):
The best subarray ending at index i is either
nums[i]alone, ornums[i]added to the best subarray ending ati-1.
If the best sum ending at i-1 is negative, adding it can only hurt, so you start fresh. Keep the best value seen anywhere as the answer.
cur, best := nums[0], nums[0]
for _, n := range nums[1:] {
cur = max(n, cur+n) // extend, or start over at n
best = max(best, cur)
}Trace [-2, 1, -3, 4, -1, 2, 1, -5, 4]:
| n | cur + n | n alone | cur = max | best |
|---|---|---|---|---|
| -2 | -2 | -2 | ||
| 1 | -1 | 1 | 1 (start over) | 1 |
| -3 | -2 | -3 | -2 | 1 |
| 4 | 2 | 4 | 4 (start over) | 4 |
| -1 | 3 | -1 | 3 | 4 |
| 2 | 5 | 2 | 5 | 5 |
| 1 | 6 | 1 | 6 | 6 |
| -5 | 1 | -5 | 1 | 6 |
| 4 | 5 | 4 | 5 | 6 |
The answer is 6, from [4, -1, 2, 1].
Kadane is really a tiny dynamic program: cur is "the best answer for subarrays that end exactly here", and each step depends only on the previous one. You will meet this shape again in Module 4.
Kadane variations#
The same "best ending here" idea adapts to several famous problems. This program solves four of them and returns the subarray's boundaries, not just the sum:
package main
import "fmt"
// maxSubarray returns the best sum and the half-open range [start, end) that achieves it.
func maxSubarray(nums []int) (best, start, end int) {
cur, curStart := nums[0], 0
best, start, end = nums[0], 0, 1
for i := 1; i < len(nums); i++ {
if cur+nums[i] < nums[i] {
cur, curStart = nums[i], i // start over
} else {
cur += nums[i]
}
if cur > best {
best, start, end = cur, curStart, i+1
}
}
return best, start, end
}
// maxProduct tracks the largest AND smallest product ending here,
// because a negative number swaps them.
func maxProduct(nums []int) int {
hi, lo, best := nums[0], nums[0], nums[0]
for _, n := range nums[1:] {
if n < 0 {
hi, lo = lo, hi
}
hi = max(n, hi*n)
lo = min(n, lo*n)
best = max(best, hi)
}
return best
}
// maxCircular allows the subarray to wrap from the end back to the start.
func maxCircular(nums []int) int {
total := 0
curMax, bestMax := 0, nums[0]
curMin, bestMin := 0, nums[0]
for _, n := range nums {
total += n
curMax = max(n, curMax+n)
bestMax = max(bestMax, curMax)
curMin = min(n, curMin+n)
bestMin = min(bestMin, curMin)
}
if bestMax < 0 {
return bestMax // all negative: removing everything would leave an empty subarray
}
return max(bestMax, total-bestMin)
}
// maxProfit is Kadane in disguise: keep the cheapest price so far.
func maxProfit(prices []int) int {
minPrice, best := prices[0], 0
for _, p := range prices[1:] {
best = max(best, p-minPrice)
minPrice = min(minPrice, p)
}
return best
}
func main() {
nums := []int{-2, 1, -3, 4, -1, 2, 1, -5, 4}
best, s, e := maxSubarray(nums)
fmt.Println("max subarray:", best, nums[s:e])
fmt.Println("all negative:", fmt.Sprint(maxSubarray([]int{-3, -1, -2})))
fmt.Println("max product:", maxProduct([]int{2, 3, -2, 4}), maxProduct([]int{-2, 3, -4}))
fmt.Println("circular:", maxCircular([]int{5, -3, 5}), maxCircular([]int{-3, -2, -3}))
fmt.Println("stock profit:", maxProfit([]int{7, 1, 5, 3, 6, 4}))
}max subarray: 6 [4 -1 2 1]
all negative: -1 1 2
max product: 6 24
circular: 10 -2
stock profit: 5Read the variations side by side:
| Problem | State kept per index | Twist |
|---|---|---|
| maximum subarray | best sum ending here | start over when the old run is negative |
| maximum product | best and worst product ending here | a negative number swaps best and worst |
| circular subarray | best and worst sum | a wrapping answer = total minus the worst middle piece |
| buy and sell stock once | cheapest price so far | profit if we sell today |
In-place tricks in one dimension#
Rotate by k with three reversals
Rotating [1 2 3 4 5 6 7] right by 3 should give [5 6 7 1 2 3 4]. Copying into a new slice is easy; doing it in place with O(1) memory uses a neat trick:
| step | slice |
|---|---|
| start | 1 2 3 4 5 6 7 |
| reverse all | 7 6 5 4 3 2 1 |
| reverse first k = 3 | 5 6 7 4 3 2 1 |
| reverse the rest | 5 6 7 1 2 3 4 |
Dutch national flag (sort three values in one pass)
To sort a slice holding only 0s, 1s and 2s, keep three regions: [0, low) holds 0s, [low, mid) holds 1s, (high, end] holds 2s, and [mid, high] is still unknown. Look at nums[mid]:
- 0: swap it to
low, advance bothlowandmid; - 1: it is already in the middle region, advance
mid; - 2: swap it to
highand shrinkhigh, but do not advancemid, because the value swapped in has not been examined yet.
package main
import (
"fmt"
"slices"
)
func rotate(nums []int, k int) {
k %= len(nums)
slices.Reverse(nums)
slices.Reverse(nums[:k])
slices.Reverse(nums[k:])
}
func sortColors(nums []int) {
low, mid, high := 0, 0, len(nums)-1
for mid <= high {
switch nums[mid] {
case 0:
nums[low], nums[mid] = nums[mid], nums[low]
low++
mid++
case 1:
mid++
case 2:
nums[mid], nums[high] = nums[high], nums[mid]
high-- // nums[mid] is new and unexamined, so mid stays
}
fmt.Println(" ", nums, "low", low, "mid", mid, "high", high)
}
}
// moveZeroes keeps the order of non-zero values: a fast/slow pointer pass.
func moveZeroes(nums []int) {
slow := 0
for _, n := range nums {
if n != 0 {
nums[slow] = n
slow++
}
}
clear(nums[slow:])
}
func main() {
a := []int{1, 2, 3, 4, 5, 6, 7}
rotate(a, 3)
fmt.Println("rotate right by 3:", a)
colors := []int{2, 0, 2, 1, 1, 0}
fmt.Println("sort colors:")
sortColors(colors)
z := []int{0, 1, 0, 3, 12}
moveZeroes(z)
fmt.Println("move zeroes:", z)
}rotate right by 3: [5 6 7 1 2 3 4]
sort colors:
[0 0 2 1 1 2] low 0 mid 0 high 4
[0 0 2 1 1 2] low 1 mid 1 high 4
[0 0 2 1 1 2] low 2 mid 2 high 4
[0 0 1 1 2 2] low 2 mid 2 high 3
[0 0 1 1 2 2] low 2 mid 3 high 3
[0 0 1 1 2 2] low 2 mid 4 high 3
move zeroes: [1 3 12 0 0]clear (Go 1.21) sets every element of a slice to its zero value, here zeroing the tail in one call.
Matrices#
In Go a matrix is usually a slice of slices, [][]int, indexed grid[row][col]. Three habits prevent most bugs:
- name the sizes once:
rows, cols := len(grid), len(grid[0]); - keep row in the first index and column in the second, always;
- check bounds before indexing neighbors:
r >= 0 && r < rows && c >= 0 && c < cols.
Rotate 90° clockwise, in place
Transpose (swap m[i][j] with m[j][i] above the diagonal), then reverse each row:
1 2 3 1 4 7 7 4 1
4 5 6 -> 2 5 8 -> 8 5 2
7 8 9 3 6 9 9 6 3
start transpose reverse rowsSpiral order
Keep four boundaries, top, bottom, left, right. Walk right along the top row, then down the right column, then left along the bottom, then up the left column, shrinking the matching boundary after each side. Stop when the boundaries cross.
Searching a sorted matrix
If every row and every column is sorted, start in the top-right corner. If the value there is too big, the whole column below is too big too: move left. If it is too small, the whole row to the left is too small: move down. Each step removes a row or a column, so the search is O(rows + cols).
package main
import "fmt"
func rotate(m [][]int) {
n := len(m)
for i := 0; i < n; i++ {
for j := i + 1; j < n; j++ {
m[i][j], m[j][i] = m[j][i], m[i][j]
}
}
for _, row := range m {
for l, r := 0, n-1; l < r; l, r = l+1, r-1 {
row[l], row[r] = row[r], row[l]
}
}
}
func spiral(m [][]int) []int {
var out []int
top, bottom, left, right := 0, len(m)-1, 0, len(m[0])-1
for top <= bottom left <= right {
for c := left; c <= right; c++ {
out = append(out, m[top][c])
}
top++
for r := top; r <= bottom; r++ {
out = append(out, m[r][right])
}
right--
if top <= bottom {
for c := right; c >= left; c-- {
out = append(out, m[bottom][c])
}
bottom--
}
if left <= right {
for r := bottom; r >= top; r-- {
out = append(out, m[r][left])
}
left++
}
}
return out
}
func searchSorted(m [][]int, target int) (int, int, int) {
r, c, steps := 0, len(m[0])-1, 0
for r < len(m) c >= 0 {
steps++
switch v := m[r][c]; {
case v == target:
return r, c, steps
case v > target:
c-- // everything below in this column is even bigger
default:
r++ // everything left in this row is even smaller
}
}
return -1, -1, steps
}
func main() {
m := [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
rotate(m)
fmt.Println("rotated:", m)
fmt.Println("spiral:", spiral([][]int{{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}))
sorted := [][]int{
{1, 4, 7, 11},
{2, 5, 8, 12},
{3, 6, 9, 16},
{10, 13, 14, 17},
}
fmt.Println(searchSorted(sorted, 9))
fmt.Println(searchSorted(sorted, 15))
}rotated: [[7 4 1] [8 5 2] [9 6 3]]
spiral: [1 2 3 4 8 12 11 10 9 5 6 7]
2 2 4
-1 -1 5Finding 9 took 4 steps and proving 15 is missing took 5, against 16 cells for a full scan. The spiral's two if checks matter for non-square matrices: without them, a single remaining row or column would be printed twice.
Complexity#
| Problem | Time | Extra space |
|---|---|---|
| maximum subarray / product / circular | O(n) | O(1) |
| rotate slice by k | O(n) | O(1) |
| Dutch national flag, move zeroes | O(n) | O(1) |
| rotate matrix | O(n²) | O(1) |
| spiral order | O(rows · cols) | O(1) besides the output |
| search sorted matrix | O(rows + cols) | O(1) |
Summary#
- Kadane: the best subarray ending here either extends the previous one or starts over. One pass, O(1) memory.
- Products need both a maximum and a minimum; circular arrays use total minus the minimum subarray.
- Three reversals rotate a slice in place; three pointers sort three values in one pass.
- Matrices: fix the
[row][col]convention, name the sizes, check bounds before indexing. - A sorted matrix is searched from a corner where one direction grows and the other shrinks.
Check your understanding
Answer before scrolling back up. If you miss one, reread the matching section, then try again.
Q1.In Kadane's algorithm, when do you start a new subarray at index i?
Q2.Why does maximum product subarray track both the maximum and the minimum product ending at each index?
Q3.The maximum circular subarray sum is the larger of the normal Kadane answer and:
Q4.Rotating an n×n matrix 90° clockwise in place can be done by:
Q5.Rotating a slice right by k with three reversals reverses:
Practice in Go
Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.
- Maximum Subarray ↗medium
Show hint
Kadane: cur = max(n, cur+n); best = max(best, cur).
Show hint
Track both hi and lo ending here; a negative number swaps their roles.
Show hint
max(kadaneMax, total - kadaneMin), unless every number is negative.
- Sort Colors ↗medium
Show hint
Dutch national flag with three pointers: low, mid, high.
- Rotate Image ↗medium
Show hint
Transpose, then reverse each row.
- Spiral Matrix ↗medium
Show hint
Keep four boundaries and shrink one after each side is printed.
Show hint
It is Kadane on the day-to-day price differences, or track the minimum price so far.
Finished this lesson?
Mark it done to track your progress. It is saved in this browser only.