Algorithm Search⌘K

Proofs: Self-organizing list

These are the human proofs of Self-organizing list, at full length.

Each operation of a self-organizing list keeps the invariant and returns the right result full

This proof shows: correctness, termination.

Statement. Let LL satisfy the invariant (I1), (I2) and, under the rule COUNT, (I3), for a map MM from keys to values. Then each of ACCESS, GET, PUT and DELETE stops, the invariant holds after it, and:

  • ACCESS(L,k)(L, k) returns the node of kk when k∈Mk \in M and nil otherwise, and the map is still MM;
  • GET(L,k)(L, k) returns M(k)M(k) when k∈Mk \in M and nil otherwise, and the map is still MM;
  • after PUT(L,k,v)(L, k, v) the map is MM with kk mapped to vv;
  • after DELETE(L,k)(L, k) the map is MM without kk.

Setup. Let the nodes be N1,…,NnN_1, \ldots, N_n in list order, with n=L.nn = L.n, before the operation. By (I1) this order is well defined and Nn.next=nilN_n.next = nil.

Lemma 1 (the scan of ACCESS). The loop of lines L2 and L3 stops. At its exit, either cur=nilcur = nil and no node has key kk, or cur=Nicur = N_i is the only node with key kk, prev=Ni−1prev = N_{i-1} and pp=Ni−2pp = N_{i-2}, where N0N_0 and N−1N_{-1} mean nil.

Proof. Each time line L2 tests its condition, the invariant is: cur=Njcur = N_j for some 1≤j≤n+11 \le j \le n + 1 (with Nn+1N_{n+1} meaning nil), prev=Nj−1prev = N_{j-1}, pp=Nj−2pp = N_{j-2}, and no node among N1,…,Nj−1N_1, \ldots, N_{j-1} has key kk. Line L1 makes it true with j=1j = 1. If the test succeeds, cur=Njcur = N_j with j≤nj \le n and Nj.key≠kN_j.key \ne k, and line L3 moves the three pointers one node forward, so the invariant holds for j+1j + 1. The number n+1−j≥0n + 1 - j \ge 0 drops by exactly 1 in each round, so the loop runs at most nn rounds. At the exit, either cur=nilcur = nil, so j=n+1j = n + 1 and no node has key kk, or cur.key=kcur.key = k, and by (I2) NjN_j is the only such node. □\square

Lemma 2 (the move). If ACCESS finds cur=Nicur = N_i, then after line L11 the list holds the same nn nodes, each once, and ends in nil. Under COUNT, the counts do not increase along the list, and the count of NiN_i is 1 more than before.

Proof. Line L5 makes the predecessor of NiN_i (or L.headL.head when i=1i = 1) point to Ni+1N_{i+1}, so the list from L.headL.head is N1,…,Ni−1,Ni+1,…,NnN_1, \ldots, N_{i-1}, N_{i+1}, \ldots, N_n, a list of n−1n - 1 nodes that ends in nil. Lines L6 to L10 do not change any nextnext field. They set pp to nil or to a node of this shorter list: under MTF p=nilp = nil; under TRANSPOSE p=pp=Ni−2p = pp = N_{i-2}, a node of the shorter list or nil; under COUNT, pp is nil or a node reached from L.headL.head by line L10. Line L11 links NiN_i at the front when p=nilp = nil, and else between pp and the node after pp. In both cases the list is the shorter list with NiN_i put in at one place, so it holds each of the nn nodes once and ends in nil.

Under COUNT, let cc be the old count of NiN_i. By (I3), the counts of N1,…,Ni−1N_1, \ldots, N_{i-1} are at least cc, and those of Ni+1,…,NnN_{i+1}, \ldots, N_n are at most cc. Line L9 sets the count of NiN_i to c+1c + 1. The loop of line L10 walks the shorter list, which has non-increasing counts, and stops at the first node qq with q.count<c+1q.count < c + 1, or at nil; pp is the node just before qq, or nil if qq is the first node. This loop also stops: it advances one node per round along a list of n−1n - 1 nodes. Every node before the new place of NiN_i has count at least c+1c + 1, and every node after it has count at most cc, since the counts are non-increasing and q.count≤cq.count \le c. So the counts along the new list do not increase. The other counts do not change, so the second part of (I3) holds: the count of kk grows by 1 for this successful access. □\square

ACCESS. If k∉Mk \notin M, Lemma 1 gives cur=nilcur = nil, line L4 returns nil, and nothing changes. If k∈Mk \in M, Lemma 2 shows that (I1) and (I3) hold after the move. The pairs stored in the nodes do not change (only nextnext and countcount fields change), so (I2) holds for the same map MM. Line L12 returns NiN_i, the node of kk.

GET. By the result of ACCESS, line P2 returns M(k)M(k) or nil.

PUT. If ACCESS returns a node xx, it is the node of kk, and line P4 sets its value to vv. The keys do not change, so (I1) to (I3) still hold, and the map is MM with k↦vk \mapsto v. Otherwise k∉Mk \notin M, and line P5 makes one new node with key kk, value vv and count 1, and adds 1 to L.nL.n. Line P6 or P7 links it at the front or at the end, so the list has L.nL.n nodes and ends in nil (I1). The keys are still distinct, since kk was absent, and the pairs are those of MM plus (k,v)(k, v) (I2). Under COUNT the new node is last, and its count 1 is at most every other count, which are all at least 1; its count is 1 plus its 0 accesses (I3).

DELETE. The scan of lines D1 and D2 is the scan of Lemma 1 without pppp, so it stops at the node of kk or at nil. If k∉Mk \notin M, line D3 returns with no change. Otherwise line D4 unlinks NiN_i as line L5 does, and line D5 lowers L.nL.n by 1. The list holds the other n−1n - 1 nodes in their old order (I1), the pairs are those of MM without kk (I2), and a sublist of a non-increasing sequence is non-increasing, with each count unchanged (I3). ■\blacksquare

Back to Self-organizing list