The foundation. Contiguous memory, O(1) indexing, and the slice machinery Go builds on top of it.
An array is a block of same-sized slots laid out back to back in memory. Because every slot is the same size, the address of element i is just base + i × size, which is why indexing is O(1) no matter how big the array is. That layout is also cache-friendly: walking an array touches memory in order, and modern CPUs reward that heavily.
Almost everything later in this course sits on arrays. Strings are byte arrays. Hash tables are arrays of buckets. Heaps are trees packed into an array. DP tables are 2-D arrays. Get comfortable here and the rest gets easier.
Fixed vs. dynamic. A raw array has a fixed size. A dynamic array hides that by keeping a backing array with spare capacity; when it fills up, it allocates a bigger one (typically double) and copies everything over.
Amortized O(1) append. Copying on growth sounds expensive, but with doubling the copies form a series 1 + 2 + 4 + … + n < 2n. Spread across n appends, that's a constant amount of work each. Any single append can be O(n); the average is O(1).
Shifting is the real cost. Inserting or deleting in the middle means sliding every later element over by one slot. That's O(n), and it's the main reason other structures exist.
| Operation | Cost | Why |
|---|---|---|
Read/write a[i] | O(1) | Address arithmetic |
| Append at end | O(1) amortized | Occasional O(n) copy on growth |
| Insert/delete at front or middle | O(n) | Everything after it shifts |
| Search unsorted | O(n) | Must look at each element |
| Search sorted | O(log n) | Binary search |
Go has both, and the difference matters:
[5]int) are values. The length is part of the type, and assigning or passing one copies every element. You'll rarely use them directly, except as map keys or fixed buffers.[]int) are a three-word header — pointer, length, capacity — that views a backing array. Passing a slice copies the header, not the data.append grows the backing array when len == cap. For small slices it roughly doubles; for large ones the growth factor tapers toward 1.25×.b := a[2:5] doesn't copy; writes through b show up in a. Use slices.Clone when you need independence, or a full slice expression a[2:5:5] to cap b so a later append can't scribble on a.a := make([]int, 3, 4) // len 3, cap 4 — one spare slot b := append(a, 99) // fits in cap: writes a's backing array[3] c := append(a, 42) // also fits: overwrites the same slot! fmt.Println(b[3], c[3]) // 42 42
Both b and c share a's backing array, so the second append clobbered the first. When a function appends to a slice it didn't create, either clone it first or return the result and let the caller own it.
Open exercises.go. Part A has you build DynamicArray without append, so the growth and shifting are yours to write. Part B is seven classic slice problems.
go test ./01-arrays/ # all tests in the module go test -run TestRotateRight -v ./01-arrays/ # one test, verbose
Suggested order: Get → Push → InsertAt → RemoveAt, then Reverse (you'll reuse it in RotateRight), MoveZeroes, PrefixSums/RangeSum, TwoSumSorted, and MaxSubarraySum last.
Everything below is also in the course download, in 01-arrays/. Work in exercises.go; the tests decide when you’re done.
// Package arrays is Module 1: arrays and slices. // // Work in exercises.go. Run `go test ./01-arrays/` to check your work, or // `go test -tags solutions ./01-arrays/` to run the tests against the // reference solutions in solution.go. package arrays // DynamicArray is a growable array of ints built on a fixed-size backing // store — the same idea Go's append uses under the hood. // // Rules for the exercises: len(data) is the capacity, size is how many slots // are in use. Don't call append() inside your methods; the point is to do the // growing and shifting yourself. type DynamicArray struct { data []int // backing store; len(data) == capacity size int // number of elements in use } // NewDynamicArray returns an empty array with capacity 2. func NewDynamicArray() *DynamicArray { return &DynamicArray{data: make([]int, 2)} } // Len returns the number of elements stored. func (a *DynamicArray) Len() int { return a.size } // Cap returns the current capacity of the backing store. func (a *DynamicArray) Cap() int { return len(a.data) } // Values returns a copy of the elements in use. func (a *DynamicArray) Values() []int { out := make([]int, a.size) copy(out, a.data[:a.size]) return out }
//go:build !solutions package arrays // ============================================================ // Part A — build a dynamic array (see types.go for the struct) // ============================================================ // Get returns the element at index i, or (0, false) if i is out of range. // Target: O(1). func (a *DynamicArray) Get(i int) (int, bool) { // TODO return 0, false } // Push appends v. When the backing store is full, allocate one twice as // large and copy the elements over. Target: amortized O(1). func (a *DynamicArray) Push(v int) { // TODO } // InsertAt inserts v at index i (0 <= i <= Len()), shifting later elements // one slot to the right. Returns false if i is out of range. Target: O(n). func (a *DynamicArray) InsertAt(i, v int) bool { // TODO return false } // RemoveAt removes and returns the element at index i, shifting later // elements left. Returns (0, false) if i is out of range. Target: O(n). func (a *DynamicArray) RemoveAt(i int) (int, bool) { // TODO return 0, false } // ============================================================ // Part B — slice problems // ============================================================ // Reverse reverses nums in place. Pattern: two pointers from the ends. // Target: O(n) time, O(1) space. func Reverse(nums []int) { // TODO } // RotateRight rotates nums right by k steps in place. // [1 2 3 4 5], k=2 -> [4 5 1 2 3]. k may be larger than len(nums). // Hint: three reversals. Target: O(n) time, O(1) space. func RotateRight(nums []int, k int) { // TODO } // MoveZeroes moves all zeros to the end, keeping the order of the // non-zero elements. [0 1 0 3 12] -> [1 3 12 0 0]. // Pattern: read/write pointers. Target: O(n) time, O(1) space. func MoveZeroes(nums []int) { // TODO } // MaxSubarraySum returns the largest sum of any non-empty contiguous // subarray (Kadane's algorithm). Returns 0 for an empty slice. // [-2 1 -3 4 -1 2 1 -5 4] -> 6 (from [4 -1 2 1]). Target: O(n). func MaxSubarraySum(nums []int) int { // TODO return 0 } // PrefixSums returns p where p[0] = 0 and p[i+1] = nums[0] + ... + nums[i]. // len(p) == len(nums)+1. Target: O(n). func PrefixSums(nums []int) []int { // TODO return nil } // RangeSum returns nums[i] + ... + nums[j] (inclusive) using a slice made by // PrefixSums. Target: O(1). func RangeSum(prefix []int, i, j int) int { // TODO return 0 } // TwoSumSorted finds indices i < j with nums[i]+nums[j] == target in a // slice sorted ascending. Returns (0, 0, false) if no pair exists. // Pattern: two pointers moving inward. Target: O(n) time, O(1) space. func TwoSumSorted(nums []int, target int) (int, int, bool) { // TODO return 0, 0, false }
package arrays import ( "slices" "testing" ) func TestDynamicArrayPushGet(t *testing.T) { a := NewDynamicArray() for i := 0; i < 100; i++ { a.Push(i * 10) } if a.Len() != 100 { t.Fatalf("Len() = %d, want 100", a.Len()) } if a.Cap() < 100 || a.Cap() > 256 { t.Errorf("Cap() = %d, want doubling growth (between 100 and 256)", a.Cap()) } for i := 0; i < 100; i++ { if v, ok := a.Get(i); !ok || v != i*10 { t.Fatalf("Get(%d) = (%d, %v), want (%d, true)", i, v, ok, i*10) } } if _, ok := a.Get(100); ok { t.Error("Get(100) on a 100-element array should return ok=false") } if _, ok := a.Get(-1); ok { t.Error("Get(-1) should return ok=false") } } func TestDynamicArrayInsertRemove(t *testing.T) { a := NewDynamicArray() for _, v := range []int{1, 2, 4} { a.Push(v) } if !a.InsertAt(2, 3) { t.Fatal("InsertAt(2, 3) returned false") } if !a.InsertAt(0, 0) { t.Fatal("InsertAt(0, 0) returned false") } if !a.InsertAt(a.Len(), 5) { t.Fatal("InsertAt(Len(), 5) should append and return true") } if got, want := a.Values(), []int{0, 1, 2, 3, 4, 5}; !slices.Equal(got, want) { t.Fatalf("after inserts: %v, want %v", got, want) } if a.InsertAt(99, 1) { t.Error("InsertAt out of range should return false") } if v, ok := a.RemoveAt(0); !ok || v != 0 { t.Errorf("RemoveAt(0) = (%d, %v), want (0, true)", v, ok) } if v, ok := a.RemoveAt(2); !ok || v != 3 { t.Errorf("RemoveAt(2) = (%d, %v), want (3, true)", v, ok) } if got, want := a.Values(), []int{1, 2, 4, 5}; !slices.Equal(got, want) { t.Fatalf("after removes: %v, want %v", got, want) } if _, ok := a.RemoveAt(4); ok { t.Error("RemoveAt(Len()) should return ok=false") } } func TestReverse(t *testing.T) { for _, tc := range []struct{ in, want []int }{ {[]int{1, 2, 3, 4, 5}, []int{5, 4, 3, 2, 1}}, {[]int{1, 2}, []int{2, 1}}, {[]int{7}, []int{7}}, {[]int{}, []int{}}, } { got := slices.Clone(tc.in) Reverse(got) if !slices.Equal(got, tc.want) { t.Errorf("Reverse(%v) = %v, want %v", tc.in, got, tc.want) } } } func TestRotateRight(t *testing.T) { for _, tc := range []struct { in []int k int want []int }{ {[]int{1, 2, 3, 4, 5}, 2, []int{4, 5, 1, 2, 3}}, {[]int{1, 2, 3, 4, 5}, 7, []int{4, 5, 1, 2, 3}}, {[]int{1, 2, 3}, 0, []int{1, 2, 3}}, {[]int{1, 2, 3}, 3, []int{1, 2, 3}}, {[]int{}, 4, []int{}}, } { got := slices.Clone(tc.in) RotateRight(got, tc.k) if !slices.Equal(got, tc.want) { t.Errorf("RotateRight(%v, %d) = %v, want %v", tc.in, tc.k, got, tc.want) } } } func TestMoveZeroes(t *testing.T) { for _, tc := range []struct{ in, want []int }{ {[]int{0, 1, 0, 3, 12}, []int{1, 3, 12, 0, 0}}, {[]int{0, 0, 1}, []int{1, 0, 0}}, {[]int{1, 2}, []int{1, 2}}, } { got := slices.Clone(tc.in) MoveZeroes(got) if !slices.Equal(got, tc.want) { t.Errorf("MoveZeroes(%v) = %v, want %v", tc.in, got, tc.want) } } } func TestMaxSubarraySum(t *testing.T) { for _, tc := range []struct { in []int want int }{ {[]int{-2, 1, -3, 4, -1, 2, 1, -5, 4}, 6}, {[]int{5, 4, -1, 7, 8}, 23}, {[]int{-3, -1, -2}, -1}, {[]int{1}, 1}, {[]int{}, 0}, } { if got := MaxSubarraySum(tc.in); got != tc.want { t.Errorf("MaxSubarraySum(%v) = %d, want %d", tc.in, got, tc.want) } } } func TestPrefixAndRangeSum(t *testing.T) { nums := []int{3, -1, 4, 1, 5, 9} p := PrefixSums(nums) if want := []int{0, 3, 2, 6, 7, 12, 21}; !slices.Equal(p, want) { t.Fatalf("PrefixSums(%v) = %v, want %v", nums, p, want) } for _, tc := range []struct{ i, j, want int }{ {0, 5, 21}, {1, 3, 4}, {2, 2, 4}, {4, 5, 14}, } { if got := RangeSum(p, tc.i, tc.j); got != tc.want { t.Errorf("RangeSum(p, %d, %d) = %d, want %d", tc.i, tc.j, got, tc.want) } } } func TestTwoSumSorted(t *testing.T) { for _, tc := range []struct { nums []int target int ok bool }{ {[]int{1, 2, 4, 7, 11, 15}, 15, true}, {[]int{2, 7, 11, 15}, 9, true}, {[]int{-3, 0, 2, 5}, 2, true}, {[]int{1, 2, 3}, 100, false}, {[]int{}, 0, false}, } { i, j, ok := TwoSumSorted(tc.nums, tc.target) if ok != tc.ok { t.Errorf("TwoSumSorted(%v, %d) ok = %v, want %v", tc.nums, tc.target, ok, tc.ok) continue } if ok && (i >= j || tc.nums[i]+tc.nums[j] != tc.target) { t.Errorf("TwoSumSorted(%v, %d) = (%d, %d): not a valid pair", tc.nums, tc.target, i, j) } } }
Try not to open solution.go until your tests pass, or until you’ve been stuck for a solid half hour. Then compare: different-but-correct is fine; notice where the reference is simpler. go test -tags solutions ./01-arrays/ runs the same tests against it.
solutions · 133 lines›//go:build solutions package arrays // ---------- DynamicArray ---------- func (a *DynamicArray) Get(i int) (int, bool) { if i < 0 || i >= a.size { return 0, false } return a.data[i], true } func (a *DynamicArray) grow() { newCap := len(a.data) * 2 if newCap == 0 { newCap = 2 } bigger := make([]int, newCap) copy(bigger, a.data[:a.size]) a.data = bigger } func (a *DynamicArray) Push(v int) { if a.size == len(a.data) { a.grow() } a.data[a.size] = v a.size++ } func (a *DynamicArray) InsertAt(i, v int) bool { if i < 0 || i > a.size { return false } if a.size == len(a.data) { a.grow() } // Shift right, starting from the end so nothing is overwritten. for j := a.size; j > i; j-- { a.data[j] = a.data[j-1] } a.data[i] = v a.size++ return true } func (a *DynamicArray) RemoveAt(i int) (int, bool) { if i < 0 || i >= a.size { return 0, false } v := a.data[i] for j := i; j < a.size-1; j++ { a.data[j] = a.data[j+1] } a.size-- a.data[a.size] = 0 // clear the stale slot return v, true } // ---------- Slice problems ---------- func Reverse(nums []int) { for i, j := 0, len(nums)-1; i < j; i, j = i+1, j-1 { nums[i], nums[j] = nums[j], nums[i] } } func RotateRight(nums []int, k int) { n := len(nums) if n == 0 { return } k %= n if k < 0 { k += n } Reverse(nums) Reverse(nums[:k]) Reverse(nums[k:]) } func MoveZeroes(nums []int) { w := 0 // write position for the next non-zero for _, v := range nums { if v != 0 { nums[w] = v w++ } } for ; w < len(nums); w++ { nums[w] = 0 } } func MaxSubarraySum(nums []int) int { if len(nums) == 0 { return 0 } best, cur := nums[0], nums[0] for _, v := range nums[1:] { cur = max(v, cur+v) best = max(best, cur) } return best } func PrefixSums(nums []int) []int { p := make([]int, len(nums)+1) for i, v := range nums { p[i+1] = p[i] + v } return p } func RangeSum(prefix []int, i, j int) int { return prefix[j+1] - prefix[i] } func TwoSumSorted(nums []int, target int) (int, int, bool) { lo, hi := 0, len(nums)-1 for lo < hi { switch s := nums[lo] + nums[hi]; { case s == target: return lo, hi, true case s < target: lo++ default: hi-- } } return 0, 0, false }
DynamicArray.Push with plain append (go test -bench .). Then try growing by 1.5× instead of 2× and compare.Four questions. Answer each one in your head — or out loud — before you open the answer.
A dynamic array doubles its capacity whenever it's full. What's the total cost of n appends, counting the copying?
A — The copies form 1 + 2 + 4 + … + n, which is less than 2n. Spread over n appends that's O(1) each, amortized.
a := []int{1, 2, 3, 4}; b := a[1:3]; b[0] = 9. What is a now?
B — Slicing doesn't copy. b views a's backing array starting at index 1, so b[0] is a[1].
MaxSubarraySum([-3, -1, -2]) should return…
B — The subarray must be non-empty, so the best you can do is the single element -1. A Kadane's that starts best at 0 gets this wrong.
Inserting at index 0 of a 1,000,000-element dynamic array costs about…
C — Every existing element moves one slot to make room, whether or not the array grows.
Commit your work before you move on to module 2, so git log shows your own progress through the course.