Dynamic arrays: how List<T> grows
Build your own List<T>, see why it doubles its capacity, and understand amortised O(1): why Add is fast on average even though resizing is slow.
List<T> in .NET is exactly this.
Count vs Capacity
Count: how many items you have added. Capacity: how many items fit in the hidden array before it must grow.
var list = new List<int>();Console.WriteLine($"Start: Count {list.Count}, Capacity {list.Capacity}"); for (int i = 1; i <= 9; i++){ list.Add(i); Console.Write($"{list.Capacity} ");}Start: Count 0, Capacity 0 4 4 4 4 8 8 8 8 16
Add allocates room for 4. The 5th finds it full, so a new array of 8 is created and the 4 items are copied over. The 9th grows it to 16. Try it, and compare with a list that grows by a fixed 4 slots each time:
Build your own List<T>
public class SimpleList<T>{ private T[] _items = new T[4]; private int _count; public int Count => _count; public int Capacity => _items.Length; public T this[int index] { get { if ((uint)index >= (uint)_count) throw new ArgumentOutOfRangeException(nameof(index)); return _items[index]; } set { if ((uint)index >= (uint)_count) throw new ArgumentOutOfRangeException(nameof(index)); _items[index] = value; } } public void Add(T item) { if (_count == _items.Length) Grow(); // rare: O(n) _items[_count++] = item; // usual case: O(1) } public void RemoveAt(int index) { if ((uint)index >= (uint)_count) throw new ArgumentOutOfRangeException(nameof(index)); _count--; // shift everything after index one slot left Array.Copy(_items, index + 1, _items, index, _count - index); _items[_count] = default!; // let the GC free the old reference } private void Grow() { var bigger = new T[_items.Length * 2]; Array.Copy(_items, bigger, _count); _items = bigger; // the old array becomes garbage }}Amortised O(1): why doubling works
Add that triggers a resize is O(n): it copies every item. So how can we say Add is O(1)? Look at the total work for n adds with doubling. Resizes happen at 4, 8, 16, 32… items, and copy 4 + 8 + 16 + … + n/2 items. That sum is less than n. So n adds cost at most n writes plus fewer than n copies: under 2n steps in total, about 2 per Add. The rare expensive Add is “paid for” by the many cheap ones. This is called amortised O(1).
| Growth rule | Copies for n adds | Cost per Add (average) |
|---|---|---|
O(1) amortised | ||
O(1) amortised | ||
O(n): the whole thing is O(n²) |
Practical tips
If you know roughly how many items you will add, say so: new List<Beam>(beamCount). No resizes at all.list.EnsureCapacity(n)grows once ahead of a big batch of adds.Capacity never shrinks on its own. After removing most items from a huge list, TrimExcess()gives the memory back.Removing many items with RemoveAtin a loop is O(n²). UseRemoveAll(predicate), which compacts the list in one O(n) pass.
| `List<T>` operation | Cost |
|---|---|
list[i], Count | O(1) |
Add | O(1) amortised |
Insert(i, x), RemoveAt(i) | O(n − i) |
Contains, IndexOf, Remove(x) | O(n) |
RemoveAll(pred) | O(n) |
Sort() | O(n log n) |
RemoveAll, and by hand with a read and a write index.
Show a hint
RemoveAt shifts all later items. For the manual version, copy every reading you keep to the write position, then remove the leftover tail once with RemoveRange.
Show the solution
var readings = new List<int> { 5, -2, 7, -1, -9, 3 }; // Slow: each RemoveAt shifts the rest → O(n²)// for (int i = readings.Count - 1; i >= 0; i--)// if (readings[i] < 0) readings.RemoveAt(i); // Fast 1: one pass inside .NETvar a = new List<int>(readings);a.RemoveAll(x => x < 0); // Fast 2: compact by handvar b = new List<int>(readings);int write = 0;for (int read = 0; read < b.Count; read++) if (b[read] >= 0) b[write++] = b[read];b.RemoveRange(write, b.Count - write); // drop the tail once Console.WriteLine(string.Join(", ", a));Console.WriteLine(string.Join(", ", b));5, 7, 3 5, 7, 3
Remember
A dynamic array is an array with spare room plus a count; List<T>is one.When full it allocates a bigger array (double the size in .NET) and copies everything. Doubling makes Addamortised O(1): n Adds cost fewer than 2n steps in total.Pre-size lists when you know the count, and use RemoveAllinstead ofRemoveAtin a loop.