Chapter 5
18 min read
Section 24 of 261

Static vs Dynamic Arrays

Arrays

The Fundamental Question

Every programmer has typed something like arr.append(x) or arr.push(x) tens of thousands of times. It feels free. The list just gets longer, no questions asked. But arrays in physical memory are contiguous: a single block of bytes laid out one after another. There's no “just put it after the last element” — the slot after the last element belongs to some other variable, struct, or piece of code. So how does append work?

The answer is one of the most beautiful trade-offs in computer science: there are two fundamentally different kinds of array, and dynamic arrays are built on top of static ones. Understanding which is which — and the cost of converting between them — is the difference between code that scales and code that mysteriously stalls in production.

Why this matters: “Append is O(1)” is a half-truth. A single append can be O(n). What's actually true is the amortized cost is O(1) — and the amortized analysis itself is one of the most elegant arguments in algorithms.

Static Arrays: Fixed and Predictable

A static array has a size fixed at the moment of allocation. You ask the runtime for NN contiguous slots, you get them, and that's it forever. If you want a 101st element, tough — the slot after the 100th already belongs to someone else.

LanguageStatic array syntaxWhere it lives
Cint A[100];Stack (auto) or .bss / heap (malloc)
C++std::array<int, 100> A;Stack — size baked into type
Javaint[] A = new int[100];Heap, size fixed at construction
Rustlet A: [i32; 100] = [0; 100];Stack, size baked into type
NumPyA = np.zeros(100, dtype=np.int32)Heap, size fixed at construction
Govar A [100]intStack/heap, size part of the type

Notice how strict the typing is in C++, Rust, and Go: the size is part of the type. [100]int and [200]int are different types. This is intentional — it lets the compiler know the exact stack frame layout and emit instruction-level optimal code with zero allocation overhead.

The address formula

For an array AA with base address BB and element size ss bytes, accessing A[i]A[i] takes one arithmetic step:

address(A[i])=B+is\text{address}(A[i]) = B + i \cdot s

That's why static-array indexing is O(1)O(1): one multiply (often a shift, since ss is a power of two), one add, one memory load. The CPU does this in a single cycle.

Static arrays in real-time systems

Audio plug-ins, flight controllers, and video encoders love static arrays. No allocation, no copy pause, no GC, no surprises. The cost of every operation is known at compile time. You give up flexibility to gain determinism.

Dynamic Arrays: Capacity vs Size

A dynamic array (also called a growable array, vector, or list) hides a static array under the hood and adds bookkeeping to make it grow. The key insight is that there are now two sizes:

  1. Logical size nn — the number of elements the user actually stored.
  2. Capacity cc — the number of slots actually allocated. Always cnc \ge n.

Slots between nn and cc are real memory but are logically empty — they hold uninitialized garbage from the user's perspective. len(arr) returns nn, never cc.

Append works in two cases:

  • Cheap path (n<cn < c): write the new value at slot nn, then nn+1n \leftarrow n + 1. One store. O(1)O(1).
  • Expensive path (n=cn = c): allocate a larger backing array of size cc', copy all nn existing elements, free the old buffer, then take the cheap path. O(n)O(n).

Definition

A dynamic array is a static array AA of length cc together with a counter ncn \le c, plus the convention that append reallocates and copies when n=cn = c.

The Growth Strategy: Why Doubling?

When the buffer is full, how much should we grow it by? This single decision separates O(n)O(n) from O(n2)O(n^2) over a sequence of appends. There are three serious options.

Option A: Add a constant (e.g., +10 every time)

Every 1010 appends triggers a resize that copies 10,20,30,10, 20, 30, \ldots elements. After nn appends, the total copy cost is:

10+20+30++n  =  k=1n/1010k  =  n(n+10)20    Θ(n2)10 + 20 + 30 + \cdots + n \;=\; \sum_{k=1}^{n/10} 10k \;=\; \frac{n(n+10)}{20} \;\in\; \Theta(n^2)

That makes append Θ(n)\Theta(n) amortized — disaster. Never grow by an additive constant.

Option B: Multiply by a constant (e.g., ×2 every time)

Resizes happen at sizes 1,2,4,8,16,,2kn1, 2, 4, 8, 16, \ldots, 2^k \le n. The total copy cost is a geometric series:

1+2+4++2k  =  2k+11  <  2n    O(n)1 + 2 + 4 + \cdots + 2^k \;=\; 2^{k+1} - 1 \;<\; 2n \;\in\; O(n)

So nn appends total O(n)O(n) work, which means amortized O(1)O(1) per append. This is the magic.

Option C: Multiply by 1.5

Same asymptotic story as ×2 (still O(n)O(n) total), but with a twist that matters in practice. With factor 2, the new buffer is always strictly larger than the sum of all previously freed buffers — so the allocator can never reuse old slots. With factor ϕ1.618\phi \approx 1.618 or smaller, recently-freed memory is large enough to satisfy the next allocation, reducing fragmentation.

ImplementationGrowth factorWhy
GCC libstdc++ std::vector2.0Simple, slightly faster
Clang libc++ std::vector2.0Simple, slightly faster
Microsoft STL std::vector1.5Memory reuse on Windows allocator
Facebook Folly fbvector1.5Memory reuse
Java ArrayList1.5(oldCap + (oldCap >> 1))
Python list~1.125 + slackConservative — list_resize over-allocates by n >> 3
Go slice2.0 then 1.252x while small, then 1.25x past 1024
Rust Vec2.0Simple

The takeaway

Any growth factor α>1\alpha > 1 gives amortized O(1)O(1) append. Picking α\alpha is a memory-vs-speed tuning knob, not a complexity question.

Amortized Analysis: The Real Cost of Append

Let's prove the O(1)O(1) claim directly using the aggregate method. Suppose we start with an empty dynamic array (capacity 1) and perform nn appends with doubling.

A resize is triggered exactly when nn reaches a power of two: at appends number 1,2,4,8,,2log2n1, 2, 4, 8, \ldots, 2^{\lfloor \log_2 n \rfloor}. The resize at size 2k2^k copies 2k2^k elements.

Total cost T(n)T(n) = (n cheap writes) + (sum of all copy costs):

T(n)  =  n+k=0log2n2k    n+2n  =  3nT(n) \;=\; n + \sum_{k=0}^{\lfloor \log_2 n \rfloor} 2^k \;\le\; n + 2n \;=\; 3n

So the amortized cost per append is T(n)/n3=O(1)T(n)/n \le 3 = O(1). Three writes per append, no matter how large nn grows.

What amortized doesn't mean

Amortized O(1)O(1) does not mean every operation is O(1)O(1). The worst-case single append is still O(n)O(n): the unlucky one that triggers the copy. Amortized only means the average cost over a sequence is bounded by a constant.

When the worst case bites

For interactive systems, real-time audio (where you have ~5 ms per buffer), or high-frequency trading, that single O(n)O(n) append can blow your latency budget. Solutions: pre-reserve capacity (vec.reserve(N) in C++, list = [None] * N in Python), use a static array, or use a deque / linked structure that has true O(1) worst-case append.

Interactive: Dynamic Array Builder

Click append repeatedly and watch the buffer fill. When it's full, the next append triggers a resize: the strip widens, every existing element is copied (the cost spike), and amber-tinted free slots appear at the right. Toggle the growth factor between 1.5× and 2× to see how often each strategy resizes.

Growth factor:
size (n)
0
capacity (c)
1
total cost
0
amortized / op
0.00
← free = 1
0 resize events so far. Blue = used. Amber tint = newly allocated slots from the most recent grow. Each resize copies all nn existing elements then appends one new value, costing n+1n + 1 writes.

Two things to notice as you click:

  • The amortized cost per op stays close to a small constant (around 2–3) regardless of how many appends you do. That's the geometric series at work.
  • With factor 1.5×, resizes happen more often (about every 1.5x in size) but each one copies fewer elements. Total work is the same up to constants. The visible difference is how much “slack” (amber free space) you carry: factor 2 has up to 50% slack just after a resize; factor 1.5 has up to 33%.

Quick Check

Starting from capacity 1 and using doubling, how many resize events occur during the first 1000 appends?


Operation Costs Side by Side

Here is the table you should commit to memory. Worst-case is the cost of a single operation in the unluckiest case; amortized is the long-run average over a sequence.

OperationStatic arrayDynamic array (worst)Dynamic array (amortized)
access A[i]O(1)O(1)O(1)
update A[i] = xO(1)O(1)O(1)
append (push_back)not supportedO(n)O(1)
pop_backnot supportedO(1)O(1)
prepend (push_front)not supportedO(n)O(n)
pop_frontnot supportedO(n)O(n)
insert at index knot supportedO(n - k)O(n - k)
delete at index knot supportedO(n - k)O(n - k)
search (unsorted)O(n)O(n)O(n)
binary search (sorted)O(log n)O(log n)O(log n)
len()O(1)O(1)O(1)

Three rows deserve a comment.

Why is prepend O(n)?

To insert at index 0, every existing element must shift one slot to the right to make room. That's nn moves regardless of capacity. No clever growth strategy fixes this. If you need fast prepend, use a deque (double-ended queue, Chapter 7) or a circular buffer.

Why is insert at index k actually O(n - k)?

Inserting at index kk shifts the suffix from kk to n1n-1 rightward — that's nkn - k moves. Inserting near the end is cheap; near the front is expensive. Same logic for delete.

Why is access still O(1) on a dynamic array?

Because the backing storage is still a contiguous static array. A[i]A[i] is one pointer dereference to the buffer, plus B+isB + i \cdot s. The dynamic-vs-static distinction only matters at the moment of growth.


Implementation: A Dynamic Array From Scratch (Python)

Python's list is already a dynamic array, so to see the machinery we have to build one against raw memory. We'll use ctypes to allocate a fixed-capacity backing buffer.

A faithful dynamic array implemented from scratch
🐍dynamic_array.py
1Import ctypes for raw memory

We use ctypes to allocate a fixed-size C-style array. Python lists already do this for us, but we want to see the machinery directly.

4Class definition

DynamicArray wraps a fixed-capacity backing buffer that we grow on demand.

5Constructor

Initialize an empty dynamic array with a small starting capacity.

EXECUTION STATE
self._n = 0
self._capacity = 1
6Logical size

_n is the number of elements the user has stored. Different from capacity.

EXECUTION STATE
self._n = 0
7Backing capacity

_capacity is the number of slots actually allocated in memory. We start at 1 to keep the doubling pattern obvious.

EXECUTION STATE
self._capacity = 1
8Allocate the backing array

_make_array(1) returns a contiguous C array of length 1 holding raw object pointers. This is real memory, not a Python list.

EXECUTION STATE
self._A = <ctypes array, len=1>
10__len__

len(arr) returns the LOGICAL size n, not capacity. The user shouldn't see the slack.

13__getitem__: bounds check

We compare against _n (logical), not _capacity. The slots between _n and _capacity hold uninitialized garbage.

EXAMPLE
If _n = 3 and _capacity = 8, asking for index 5 must raise IndexError.
15Random access

self._A[k] is O(1) — a single pointer dereference plus offset. Same as a static array.

17append: capacity check

Before writing, we ask: is there room? If _n already equals _capacity, every slot is full and we MUST resize first.

EXAMPLE
_n=4, _capacity=4 triggers _resize(8). _n=2, _capacity=4 takes the cheap path.
18Trigger resize with doubling

We call _resize with 2 * _capacity. This is the classical doubling strategy; it's why amortized append is O(1).

EXAMPLE
If _capacity = 4, this calls _resize(8).
19Write new element

After ensuring capacity, place the new value at index _n. This is the one cheap step — a single store.

EXECUTION STATE
self._A[self._n] = obj
20Increment logical size

Bump _n by 1. The user-visible length is now one larger.

EXECUTION STATE
self._n = self._n + 1
22_resize: allocate new backing array

Get a fresh contiguous block of size new_cap. The old block is still alive — we haven't freed it yet.

EXAMPLE
_resize(8) when _capacity=4: allocate B of length 8.
EXECUTION STATE
B = <ctypes array, len=new_cap>
23Copy loop — the expensive step

Walk every existing element and copy it from old buffer A to new buffer B. This is O(n) work and is the entire reason resize is expensive.

LOOP TRACE · 4 iterations
k = 0
B[0] = A[0]
k = 1
B[1] = A[1]
k = 2
B[2] = A[2]
k = 3
B[3] = A[3]
25Swap buffers

self._A now points at the new larger buffer. The old buffer becomes unreachable and is freed by Python's garbage collector.

EXECUTION STATE
self._A = B
26Update capacity field

_capacity catches up to the new buffer size. _n hasn't changed during resize.

EXECUTION STATE
self._capacity = new_cap
28_make_array helper

Allocate a contiguous block of c raw PyObject* pointers. (c * py_object) creates a ctypes type descriptor; calling it allocates and returns the buffer.

14 lines without explanation
1import ctypes
2
3
4class DynamicArray:
5    def __init__(self):
6        self._n = 0
7        self._capacity = 1
8        self._A = self._make_array(self._capacity)
9
10    def __len__(self):
11        return self._n
12
13    def __getitem__(self, k):
14        if not 0 <= k < self._n:
15            raise IndexError("index out of range")
16        return self._A[k]
17
18    def append(self, obj):
19        if self._n == self._capacity:
20            self._resize(2 * self._capacity)
21        self._A[self._n] = obj
22        self._n += 1
23
24    def _resize(self, new_cap):
25        B = self._make_array(new_cap)
26        for k in range(self._n):
27            B[k] = self._A[k]
28        self._A = B
29        self._capacity = new_cap
30
31    def _make_array(self, c):
32        return (c * ctypes.py_object)()

Trace what happens with four appends starting from empty:

StepBeforeResize?ActionAfter
append('a')n=0, cap=1no (0 < 1)A[0]='a', n→1n=1, cap=1
append('b')n=1, cap=1YES (1 == 1)alloc cap=2, copy 1, A[1]='b'n=2, cap=2
append('c')n=2, cap=2YES (2 == 2)alloc cap=4, copy 2, A[2]='c'n=3, cap=4
append('d')n=3, cap=4no (3 < 4)A[3]='d', n→4n=4, cap=4

Total cost: 1 + (1 copy + 1 write) + (2 copies + 1 write) + 1 = 66 writes for 44 appends. Amortized 1.5 — well under our 3n upper bound.


Implementation: The Same Idea in C++

In C++ we have to manage memory by hand with new[] and delete[] — exactly the work that std::vector hides. (A production version would use std::allocator, placement new, move semantics, exception safety, and an iterator interface; we're showing the bones.)

A toy std::vector — same idea, manual memory
📄vector.cpp
4Class template

Vector<T> is parameterized over the element type T, just like std::vector<T>.

6Three private fields

data_ is the heap pointer, size_ is logical n, cap_ is allocated capacity.

EXECUTION STATE
data_ = T* (heap)
size_ = size_t
cap_ = size_t
11Default constructor

Start with capacity 1 and zero elements. Allocate the initial buffer with new[].

EXECUTION STATE
size_ = 0
cap_ = 1
12Allocate first buffer

new T[1] gives us one raw, default-initialized slot on the heap.

16Destructor

delete[] is mandatory. Without it we leak every byte we ever allocated.

22operator[] — O(1) access

Pointer arithmetic. data_[i] is *(data_ + i). One add, one load. No bounds check (matches std::vector::operator[]).

26push_back: full?

If size equals capacity, every slot is occupied. We must grow before writing.

EXAMPLE
size_=4, cap_=4 → resize(8) first.
27Double the capacity

resize(2 * cap_) keeps amortized push_back at O(1). libstdc++ uses 2x; libc++ uses 1.5x.

29Construct in place

Write the value at data_[size_] then post-increment size_. Single store, O(1).

EXECUTION STATE
data_[size_] = value
size_ = size_ + 1
34resize: allocate new buffer

Heap-allocate the new block. Old block still owned by data_.

EXECUTION STATE
new_data = T* (new heap block)
35Copy old elements

The expensive O(n) loop. Real std::vector uses std::move when T's move constructor is noexcept — same memory pattern, cheaper per-element cost.

LOOP TRACE · 4 iterations
i = 0
new_data[0] = data_[0]
i = 1
new_data[1] = data_[1]
i = 2
new_data[2] = data_[2]
i = 3
new_data[3] = data_[3]
38Free old buffer

delete[] data_ releases the old heap allocation. CRITICAL: do this AFTER the copy, never before.

39Swap pointer

data_ now owns the new larger buffer.

40Update capacity

cap_ tracks the new allocation. size_ is unchanged.

28 lines without explanation
1#include <cstddef>
2
3template <typename T>
4class Vector {
5private:
6    T* data_;
7    std::size_t size_;
8    std::size_t cap_;
9
10public:
11    Vector() : size_(0), cap_(1) {
12        data_ = new T[1];
13    }
14
15    ~Vector() {
16        delete[] data_;
17    }
18
19    std::size_t size() const { return size_; }
20
21    T& operator[](std::size_t i) {
22        return data_[i];
23    }
24
25    void push_back(const T& value) {
26        if (size_ == cap_) {
27            resize(2 * cap_);
28        }
29        data_[size_++] = value;
30    }
31
32private:
33    void resize(std::size_t new_cap) {
34        T* new_data = new T[new_cap];
35        for (std::size_t i = 0; i < size_; ++i) {
36            new_data[i] = data_[i];
37        }
38        delete[] data_;
39        data_ = new_data;
40        cap_ = new_cap;
41    }
42};

Move semantics in real std::vector

Real std::vector::push_back uses std::move on the copy loop when TT's move constructor is noexcept. The asymptotic cost is the same — still O(n)O(n) per resize — but the constant factor is much smaller because moving a string or unique_ptr is essentially copying a pointer, not copying its contents.

Real-World Implementations

Every serious language ships a dynamic array as its default sequence type. Each one tunes growth differently for its target use case.

ImplementationElement typeNotes
Python listPyObject*Array of pointers — heterogeneous. Growth roughly n + (n >> 3) plus small slop.
NumPy ndarrayFixed dtypeTruly homogeneous, contiguous, fixed-size. np.resize copies. For appending, use lists then convert.
Java ArrayListObject[]1.5x growth. Boxed primitives — int autoboxes to Integer. For raw int[], use static arrays.
JavaScript ArrayTagged valuesV8 internally distinguishes packed-SMI, packed-doubles, holey, and dictionary modes.
Go sliceFixed typeA slice is a (ptr, len, cap) triple over a backing array. append() may or may not allocate.
Rust Vec<T>Fixed type T2x growth. Move semantics by default — push doesn't copy.
C++ std::vector<T>Fixed type TImplementation-defined growth (1.5x or 2x). Reserve up front for known sizes.

Beyond “just a list”: the dynamic-array pattern in disguise

  • Hash table internals — when load factor exceeds a threshold (typically 0.75), the bucket array is reallocated and every entry is rehashed into the larger array. Same allocate-copy-free pattern.
  • String builders — Java's StringBuilder, Python's io.StringIO, and Rust's String are all dynamic arrays of bytes/chars under the hood. That's why repeatedly concatenating with += in a loop can be O(n²) in some languages but O(n) when using a builder.
  • Database write-ahead-log buffers — bounded dynamic arrays. They grow up to a hard cap, then flush to disk and reset rather than grow further, giving worst-case bounds on memory.
  • GPU memory pools and game-engine object pools — pre-allocated static arrays acting as a free list. Game engines pay the static-array cost up front to avoid GC pauses during a frame.
  • Image and video buffers — almost always static. A 4K frame is a 3840×2160×4 byte chunk, allocated once at startup and reused every frame.

When NOT to Use Dynamic Arrays

Dynamic arrays are an excellent default, but they have failure modes. Reach for something else when:

ScenarioProblem with dynamic arrayBetter choice
Hard real-time systems (audio, flight control)Worst-case append is O(n) — unbounded latency spikeStatic array, pre-reserved
Embedded systems with no mallocResize requires the heap allocatorStatic array, fixed pool
Frequent insert/delete at the frontEach prepend is O(n)Deque / circular buffer
Frequent insert/delete in the middleShifting cost is O(n)Linked list or balanced tree
Very large, mostly-empty dataResizing copies everything, even sparse zerosSparse data structure (hash map, CSR matrix)
Multiple references to old sizeReallocation invalidates pointers/iteratorsIndex-based access, or use std::deque
Concurrent reads + writesReallocation moves the buffer mid-useLock-free queue, persistent structure

Pre-reserve when you know the size

If you know you'll push roughly NN elements, do vec.reserve(N) (C++) or list = [None] * N (Python, then write by index) up front. Every subsequent append is now strictly O(1)O(1) — no resize ever happens. This single line is one of the most common, easy, and impactful performance fixes in real code.

Pitfalls and Gotchas

1. Iterator and pointer invalidation

In C++, holding a pointer or iterator into a std::vector across a push_back is undefined behavior — the resize may have moved the entire buffer to a different address. The same trap exists in Rust (the borrow checker prevents it at compile time) and in Go slices (an append may or may not return a slice pointing to the original backing array).

📄cpp
1std::vector<int> v = {1, 2, 3};
2int* p = &v[0];      // points into v's buffer
3v.push_back(4);      // may reallocate — p is now dangling
4*p = 99;             // undefined behavior!

2. Memory holes after pop

Most implementations don't shrink on pop. After pushing 1 million elements then popping all of them, capacity is still ~1 million. To release memory you have to call shrink_to_fit() (C++), construct a new list (Python), or assign a freshly-sized slice (Go).

3. Off-by-one in capacity vs size

Beginners writing their own dynamic array often get the resize trigger wrong. The condition is n=cn = c, not n>cn > c or nc1n \ge c - 1. Resize before writing the new element, not after.

4. Assuming append is “always fast”

It's amortized fast, not worst-case fast. If you're writing a 60-fps game loop and a single resize copies 100 MB, you'll drop a frame. Profile, and pre-reserve.

5. Confusing dynamic arrays with linked lists

A “list” in Python, JavaScript, Java, or Ruby is a dynamic array, not a linked list. The name is a historical accident inherited from LISP. If you want O(1) middle insertion, you need a different data structure entirely (Chapter 6).

Quick Check

Why is prepending to a dynamic array O(n) even with a doubling growth strategy?


Summary

ConceptStatic arrayDynamic array
SizeFixed at allocationGrows on demand
Capacity vs sizeAlways equalcapacity ≥ size, with slack
AppendNot supportedAmortized O(1), worst-case O(n)
Index accessO(1)O(1)
Prepend / middle insertNot supportedO(n)
Memory overheadZeroUp to 50% slack (factor 2) or 33% (factor 1.5)
Worst-case latencyBounded, predictableUnbounded — resize spikes
Best forReal-time, embedded, fixed-size dataUnknown size, general-purpose

The two big ideas to take away:

  1. Capacity is not size. A dynamic array carries hidden slack so that most appends can skip allocation. The (n,c)(n, c) pair is the entire trick.
  2. Geometric growth makes amortized append O(1). Total cost over nn appends is bounded by 3n3n because the geometric series 1+2+4++2k<22k2n1 + 2 + 4 + \cdots + 2^k < 2 \cdot 2^k \le 2n sums to less than 2n2n. The choice of factor (1.5 or 2) is a memory-vs-latency knob, not a complexity one.

In the next section we'll examine Memory Allocation Strategies in depth — exactly how and where the runtime gets these contiguous blocks, and why malloc isn't actually free.


Exercises

Conceptual

  1. A dynamic array starts empty with capacity 1 and uses doubling. After 17 appends, what are the values of nn, cc, and the total number of writes performed (including resize copies)?
  2. Prove that any growth factor α>1\alpha > 1 gives amortized O(1)O(1) append. Where does the proof break for α=1\alpha = 1?
  3. Why does Python's list use a growth pattern of roughly 1.125x with constant slop, instead of 2x? Hint: think about typical Python program memory pressure vs C++ program memory pressure.

Implementation

  1. Extend the DynamicArray class above with insert(k, obj), pop(), and __setitem__. What is the worst-case complexity of each?
  2. Add automatic shrinking: when nc/4n \le c/4, halve the capacity. Why c/4c/4 and not c/2c/2? (Hint: the wrong threshold causes O(n)O(n) amortized behavior on a pathological alternating pattern.)
  3. Implement pop_front. Then implement a circular-buffer version that runs in O(1)O(1) amortized.

Analysis

  1. Suppose the growth factor is 1.5. Prove that nn appends cost at most 4n4n total writes. Then redo the argument for factor α\alpha in general — express the bound as a function of α\alpha.
  2. A user calls append exactly NN times, starting from capacity 1 with doubling. How many times does the backing buffer get reallocated? Express your answer in terms of NN.

Challenge

  1. Design a data structure that supports both append and prepend in amortized O(1)O(1), plus O(1)O(1) random access. (Hint: two stacks, or a circular buffer.) How does the constant factor compare to a plain dynamic array?
  2. Most implementations use a single backing array. Investigate the tiered-vector structure: an array of n\sqrt{n} arrays of size n\sqrt{n}. What complexities does it offer for append, prepend, insert, and access? Why is it not the default?
Loading comments...