Algorithm Search⌘K

Proofs: Doubly linked list

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

The doubly linked list operations keep the ring and return the right result full

This proof shows: correctness.

Setting. A list LL has a sentinel L.nilL.nil and a count L.nL.n. Write n=L.nn = L.n, and let v0,…,vn−1v_0, \ldots, v_{n-1} be its item nodes from the front, with v−1=vn=L.nilv_{-1} = v_n = L.nil.

Invariant (I). The nodes L.nil,v0,…,vn−1L.nil, v_0, \ldots, v_{n-1} are distinct, vi.next=vi+1v_i.next = v_{i+1} for −1≤i≤n−1-1 \le i \le n - 1, and vi.prev=vi−1v_i.prev = v_{i-1} for 0≤i≤n0 \le i \le n. The list held is (v0.key,…,vn−1.key)(v_0.key, \ldots, v_{n-1}.key).

Since v−1v_{-1} and vnv_n are both L.nilL.nil, (I) says L.nil.next=v0L.nil.next = v_0 and L.nil.prev=vn−1L.nil.prev = v_{n-1}, and for n=0n = 0 both are L.nilL.nil. Write ℓi=vi.key\ell_i = v_i.key. For each operation we assume (I) and the precondition, and show (I) for the list that the list type asks for.

INSERT-AFTER(L,p,x)(L, p, x), p=vjp = v_j with −1≤j≤n−1-1 \le j \le n - 1. Line L1 makes a node vv. It is new, so it is distinct from the nodes of the ring. By (I), p.next=vj+1p.next = v_{j+1}, which is L.nilL.nil when j=n−1j = n - 1. Line L2 sets v.next=vj+1v.next = v_{j+1} and v.prev=vjv.prev = v_j. Line L3 writes vj+1.prev=vv_{j+1}.prev = v, and line L4 writes vj.next=vv_j.next = v and sets the count to n+1n + 1. When j=n−1j = n - 1, the node vj+1v_{j+1} is the sentinel, and when j=−1j = -1, the node vjv_j is the sentinel; the lines are the same. Define the new sequence w−1,…,wnw_{-1}, \ldots, w_n by wi=viw_i = v_i for i≤ji \le j, wj+1=vw_{j+1} = v, and wi=vi−1w_i = v_{i-1} for i≥j+2i \ge j + 2, with wn+1=L.nilw_{n+1} = L.nil. Its n+2n + 2 nodes are distinct. Check the nextnext fields: for i<ji < j and for i≥j+2i \ge j + 2 the field was not written and keeps its old value, which is the old successor, and that is wi+1w_{i+1}; wj.next=v=wj+1w_j.next = v = w_{j+1} by line L4; wj+1.next=vj+1=wj+2w_{j+1}.next = v_{j+1} = w_{j+2} by line L2. Check the prevprev fields: wj+1.prev=vj=wjw_{j+1}.prev = v_j = w_j by line L2; wj+2.prev=vw_{j+2}.prev = v by line L3; the others were not written. So (I) holds for the list (ℓ0,…,ℓj,x,ℓj+1,…,ℓn−1)(\ell_0, \ldots, \ell_j, x, \ell_{j+1}, \ldots, \ell_{n-1}).

The order of the lines matters only in one place: line L2 reads p.nextp.next before line L4 writes it, and line L3 reads p.nextp.next (still vj+1v_{j+1}) before line L4. Both reads see the old value.

DELETE(L,v)(L, v), v=vjv = v_j with 0≤j≤n−10 \le j \le n - 1. By (I), v.prev=vj−1v.prev = v_{j-1} and v.next=vj+1v.next = v_{j+1}, where either can be the sentinel. Line L5 writes vj−1.next=vj+1v_{j-1}.next = v_{j+1}, and line L6 writes vj+1.prev=vj−1v_{j+1}.prev = v_{j-1} and sets the count to n−1n - 1. Line L5 does not change v.nextv.next or v.prevv.prev, so line L6 reads the old values. The new sequence v−1,…,vj−1,vj+1,…,vnv_{-1}, \ldots, v_{j-1}, v_{j+1}, \ldots, v_n has n+1n + 1 distinct nodes. The only changed fields are the nextnext field of the predecessor of vjv_j and the prevprev field of its successor, and both now skip vjv_j. All other fields keep their old values, which already match the new sequence. So (I) holds for (ℓ0,…,ℓj−1,ℓj+1,…,ℓn−1)(\ell_0, \ldots, \ell_{j-1}, \ell_{j+1}, \ldots, \ell_{n-1}), and line L7 returns ℓj\ell_j.

SEARCH(L,k)(L, k). Claim: each time line S2 tests the loop condition, x=vix = v_i for some 0≤i≤n0 \le i \le n and ℓt≠k\ell_t \ne k for all t<it < i. At the first test, x=L.nil.next=v0x = L.nil.next = v_0 and the claim holds with i=0i = 0. If the loop body runs, then x=vi≠L.nilx = v_i \ne L.nil, so i<ni < n, and ℓi≠k\ell_i \ne k; line S3 sets x=vi+1x = v_{i+1} by (I), and the claim holds for i+1i + 1. Each pass increases ii, and i≤ni \le n, so the loop stops after at most nn passes. When it stops, either x=L.nilx = L.nil, so i=ni = n and no item equals kk, and line S4 returns NIL; or x=vix = v_i with ℓi=k\ell_i = k and no earlier item equal to kk, and line S4 returns the first node with key kk. No field is written, so (I) still holds.

End operations. push_front, push_back and insert_before are INSERT-AFTER with p=L.nil=v−1p = L.nil = v_{-1}, p=L.nil.prev=vn−1p = L.nil.prev = v_{n-1} and p=q.prevp = q.prev for a node q=vjq = v_j, which is vj−1v_{j-1}. In each case pp is in the allowed range, and the new item lands at position 00, nn and jj. pop_front and pop_back are DELETE of v0v_0 and vn−1v_{n-1}, which are item nodes when n≥1n \ge 1. So all of them are correct by the two cases above. ■\blacksquare

Back to Doubly linked list