Algorithm Search⌘K

Proofs: Singly linked list

These are the human proofs of Singly linked list, at full length.

The operations of a singly linked list keep its invariant and are correct full

This proof shows: correctness.

We write σ(L)=⟨x1.key,…,xn.key⟩\sigma(L) = \langle x_1.key, \dots, x_n.key \rangle for the sequence that the list represents when the invariant (I) holds with the nodes x1,…,xnx_1, \dots, x_n.

(I) There are n=L.nn = L.n distinct nodes x1,…,xnx_1, \dots, x_n with L.head=x1L.head = x_1, xi.next=xi+1x_i.next = x_{i+1} for 1≤i<n1 \le i < n and xn.next=NILx_n.next = \text{NIL}. If n≥1n \ge 1 then L.tail=xnL.tail = x_n. If n=0n = 0 then L.head=L.tail=NILL.head = L.tail = \text{NIL}.

A new node is distinct from each node of the list, because lines L1, L6 and A1 make it fresh. Each operation below starts in a state where (I) holds.

The empty list. A list with L.head=L.tail=NILL.head = L.tail = \text{NIL} and L.n=0L.n = 0 meets (I) with no nodes, and σ(L)=⟨⟩\sigma(L) = \langle \rangle.

Lemma 1 (PUSH-FRONT). After PUSH-FRONT(L,k)(L, k), (I) holds and σ(L)=⟨k⟩⋅σ0\sigma(L) = \langle k \rangle \cdot \sigma_0, where σ0\sigma_0 is the sequence before the call.

Proof. Let x0x_0 be the new node, with key kk. Line L2 sets x0.next=x1x_0.next = x_1 if n≥1n \ge 1, and NIL if n=0n = 0. Line L3 sets L.head=x0L.head = x_0. No other link changes, so the chain is x0,x1,…,xnx_0, x_1, \dots, x_n, of n+1n + 1 distinct nodes, ending in NIL. If n≥1n \ge 1, then L.tail=xnL.tail = x_n was not NIL, line L4 does nothing, and the tail is still the last node. If n=0n = 0, then L.tail=NILL.tail = \text{NIL} and line L4 sets it to x0x_0, the only node. Line L5 makes the count n+1n + 1. □\square

Lemma 2 (PUSH-BACK). After PUSH-BACK(L,k)(L, k), (I) holds and σ(L)=σ0⋅⟨k⟩\sigma(L) = \sigma_0 \cdot \langle k \rangle.

Proof. Let xn+1x_{n+1} be the new node; line L6 gives it key kk and next NIL. If n=0n = 0, line L7 sets L.head=xn+1L.head = x_{n+1}, so the chain is the single node. If n≥1n \ge 1, then L.tail=xnL.tail = x_n by (I), and line L8 sets xn.next=xn+1x_n.next = x_{n+1}, which extends the chain x1,…,xnx_1, \dots, x_n by one node. In both cases line L9 makes the new node the tail and line L10 the count n+1n + 1. □\square

Lemma 3 (POP-FRONT). If n≥1n \ge 1, then POP-FRONT(L)(L) returns x1.keyx_1.key, and afterwards (I) holds and σ(L)=⟨x2.key,…,xn.key⟩\sigma(L) = \langle x_2.key, \dots, x_n.key \rangle.

Proof. Line L11 sets x=x1x = x_1 and line L12 sets L.head=x1.nextL.head = x_1.next, which is x2x_2 if n≥2n \ge 2 and NIL if n=1n = 1. No other link changes, so the chain from the head is x2,…,xnx_2, \dots, x_n. If n≥2n \ge 2 the head is not NIL, line L13 does nothing, and L.tail=xnL.tail = x_n is still the last node. If n=1n = 1 the head is NIL, and line L13 sets the tail to NIL. Line L14 makes the count n−1n - 1, and line L15 returns x1.keyx_1.key. □\square

Lemma 4 (SEARCH). SEARCH(L,k)(L, k) returns the node xix_i with the least ii such that xi.key=kx_i.key = k, or NIL if there is none.

Proof. We use this invariant of the loop of line S2: x=xjx = x_j for some jj with 1≤j≤n+11 \le j \le n + 1, where xn+1x_{n+1} means NIL, and no node xtx_t with t<jt < j has key kk. Line S1 sets x=x1x = x_1 (or NIL, which is x1x_1 when n=0n = 0), with no earlier node, so the invariant holds. If the loop body runs, then x=xj≠NILx = x_j \ne \text{NIL} and xj.key≠kx_j.key \ne k, and line S3 moves to xj+1x_{j+1}, so the invariant holds for j+1j + 1. The index jj grows by exactly 1 in each pass and cannot pass n+1n + 1, because at xn+1=NILx_{n+1} = \text{NIL} the test fails. So the loop stops after at most nn passes. When it stops, either x=NILx = \text{NIL}, and by the invariant no node has key kk, or x=xjx = x_j with xj.key=kx_j.key = k and no earlier match. Line S4 returns xx. □\square

Lemma 5 (INSERT-AFTER). If y=xiy = x_i is a node of LL, then after INSERT-AFTER(L,y,k)(L, y, k), (I) holds and σ(L)=⟨x1.key,…,xi.key,k,xi+1.key,…,xn.key⟩\sigma(L) = \langle x_1.key, \dots, x_i.key, k, x_{i+1}.key, \dots, x_n.key \rangle.

Proof. Let zz be the new node with key kk. Line A2 sets z.next=xi.nextz.next = x_i.next, which is xi+1x_{i+1}, or NIL if i=ni = n. Line A3 sets xi.next=zx_i.next = z. No other link changes, so the chain is x1,…,xi,z,xi+1,…,xnx_1, \dots, x_i, z, x_{i+1}, \dots, x_n, of n+1n + 1 distinct nodes, ending in NIL. If i=ni = n, then L.tail=xn=yL.tail = x_n = y and line A4 makes zz, the new last node, the tail. If i<ni < n, the tail xnx_n is still the last node and line A4 does nothing. Line A5 makes the count n+1n + 1. □\square

Lemma 6 (DELETE). DELETE(L,k)(L, k) returns false and changes nothing if no node has key kk. Otherwise, let xix_i be the first node with key kk; it returns true, and afterwards (I) holds and σ(L)\sigma(L) is σ0\sigma_0 with its ii-th item removed.

Proof. The loop of lines D2 and D3 is the loop of SEARCH, with one more variable. It keeps the invariant of Lemma 4 and also: prev=xj−1prev = x_{j-1} if j≥2j \ge 2, and prev=NILprev = \text{NIL} if j=1j = 1. Line D1 makes this true for j=1j = 1, and line D3 sets prevprev to the old x=xjx = x_j when xx moves to xj+1x_{j+1}. By Lemma 4, the loop stops at the first node with key kk, or at NIL.

If it stops at NIL, line D4 returns false, and no field was written.

Else x=xix = x_i. If i=1i = 1, then prev=NILprev = \text{NIL}, and line D5 sets L.head=x1.nextL.head = x_1.next, the node x2x_2 or NIL. If i≥2i \ge 2, then prev=xi−1prev = x_{i-1}, and line D5 sets xi−1.next=xi.nextx_{i-1}.next = x_i.next, which is xi+1x_{i+1} or NIL. In both cases, the chain from the head is x1,…,xi−1,xi+1,…,xnx_1, \dots, x_{i-1}, x_{i+1}, \dots, x_n, of n−1n - 1 distinct nodes, and it ends in NIL. For the tail: if i=ni = n, then L.tail=xn=xL.tail = x_n = x, and line D6 sets the tail to prevprev, which is xn−1x_{n-1}, the new last node, or NIL when n=1n = 1 and the list is now empty. If i<ni < n, then L.tail=xn≠xL.tail = x_n \ne x and is still the last node. Line D7 makes the count n−1n - 1, and line D8 returns true. □\square

Theorem. Starting from the empty list, after each finite sequence of the operations PUSH-FRONT, PUSH-BACK, POP-FRONT on a nonempty list, SEARCH, INSERT-AFTER on a node of the list, and DELETE, the invariant (I) holds, and the list represents the sequence that the same operations give on the abstract type, and each result is the one that the abstract type gives.

Proof. By induction on the number of operations. The empty list meets (I) and represents ⟨⟩\langle \rangle. If (I) holds and the list represents the right sequence before an operation, then Lemmas 1 to 6 show that (I) holds after it, that the new sequence is the one the abstract operation gives, and that the result is correct. SEARCH changes no field, so it keeps (I) trivially. ■\blacksquare

Cost. Lines L1 to L15 and A1 to A5 have no loop, so each of PUSH-FRONT, PUSH-BACK, POP-FRONT and INSERT-AFTER takes Θ(1)\Theta(1) time. By the proof of Lemma 4, the loop of SEARCH, and the same loop in DELETE, makes at most nn passes, and exactly nn on a miss, so each takes Θ(n)\Theta(n) time in the worst case.

Back to Singly linked list