Algorithm Search⌘K

Proofs: Circular suffix array

These are the human proofs of Circular suffix array, at full length.

Cyclic prefix doubling sorts the rotations of a string full

This proof shows: correctness, termination, time.worst.

Notation. T=T[0…n−1]T = T[0 \ldots n-1] with n≥1n \ge 1, and all position arithmetic is modulo nn. The rotation Ri=T[i…n−1] T[0…i−1]R_i = T[i \ldots n-1]\,T[0 \ldots i-1]. For ℓ≥1\ell \ge 1, let Ri(ℓ)R_i^{(\ell)} be the string of length ℓ\ell whose kk-th symbol is T[(i+k) mod n]T[(i + k) \bmod n]; it is the prefix of length ℓ\ell of the infinite periodic word RiRiRi⋯R_i R_i R_i \cdots. The claim is that CYCLIC-SORT returns CACA with (RCA[0],CA[0])<(RCA[1],CA[1])<⋯(R_{CA[0]}, CA[0]) < (R_{CA[1]}, CA[1]) < \cdots in the order that compares the rotation first and the position second.

Lemma 1. For ℓ≥n\ell \ge n, Ri(ℓ)<Rj(ℓ)R_i^{(\ell)} < R_j^{(\ell)} if and only if Ri<RjR_i < R_j, and Ri(ℓ)=Rj(ℓ)R_i^{(\ell)} = R_j^{(\ell)} if and only if Ri=RjR_i = R_j.

Proof. Ri(n)=RiR_i^{(n)} = R_i. If Ri≠RjR_i \ne R_j, the two rotations have the same length nn, so they first differ at some offset k<nk < n, and Ri(ℓ)R_i^{(\ell)} and Rj(ℓ)R_j^{(\ell)} first differ at the same offset kk with the same two symbols. If Ri=RjR_i = R_j, then the two periodic words are equal, so all their prefixes are equal. □\square

Lemma 2 (doubling). Ri(2ℓ)=Ri(ℓ)Ri+ℓ(ℓ)R_i^{(2\ell)} = R_i^{(\ell)} R_{i+\ell}^{(\ell)}. So Ri(2ℓ)<Rj(2ℓ)R_i^{(2\ell)} < R_j^{(2\ell)} if and only if the pair (Ri(ℓ),Ri+ℓ(ℓ))(R_i^{(\ell)}, R_{i+\ell}^{(\ell)}) is smaller than (Rj(ℓ),Rj+ℓ(ℓ))(R_j^{(\ell)}, R_{j+\ell}^{(\ell)}) in the order that compares the first component first.

Proof. The symbol at offset k≥ℓk \ge \ell of Ri(2ℓ)R_i^{(2\ell)} is T[i+k]=T[(i+ℓ)+(k−ℓ)]T[i + k] = T[(i + \ell) + (k - \ell)], which is the symbol at offset k−ℓk - \ell of Ri+ℓ(ℓ)R_{i+\ell}^{(\ell)}. Two strings of the same length 2ℓ2\ell compare as the pairs of their halves. □\square

Invariant. At each test of line L4 with the current value ℓ\ell of lengthlength:

(D) pp is a permutation of 0,…,n−10, \ldots, n-1 with Rp[0](ℓ)≤Rp[1](ℓ)≤⋯≤Rp[n−1](ℓ)R_{p[0]}^{(\ell)} \le R_{p[1]}^{(\ell)} \le \cdots \le R_{p[n-1]}^{(\ell)}, and cls[i]cls[i] is the number of distinct strings among R0(ℓ),…,Rn−1(ℓ)R_0^{(\ell)}, \ldots, R_{n-1}^{(\ell)} that are smaller than Ri(ℓ)R_i^{(\ell)}. In particular 0≤cls[i]<n0 \le cls[i] < n, and cls[i]<cls[j]cls[i] < cls[j] if and only if Ri(ℓ)<Rj(ℓ)R_i^{(\ell)} < R_j^{(\ell)}.

Initialization. Before the first test, ℓ=1\ell = 1 and Ri(1)=T[i]R_i^{(1)} = T[i]. The symbols are mapped to their ranks among the distinct symbols, which is exactly the class of (D), and line L2 sorts the positions by this key.

Maintenance. Assume (D) for ℓ\ell, with ℓ<n\ell < n. Line L5 sets pn[r]=p[r]−ℓpn[r] = p[r] - \ell. As rr goes from 0 to n−1n-1, the position x=pn[r]x = pn[r] has x+ℓ=p[r]x + \ell = p[r], so the list pnpn is sorted by Rx+ℓ(ℓ)R_{x+\ell}^{(\ell)}, the second half of Rx(2ℓ)R_x^{(2\ell)} (Lemma 2). It is a permutation, because x↦x−ℓx \mapsto x - \ell is a bijection modulo nn. Line L6 sorts pnpn stably by cls[x]cls[x], that is, by the first half. A stable sort by the first key of a list already sorted by the second key gives the list sorted by the pair, so by Lemma 2 the new pp is sorted by Rx(2ℓ)R_x^{(2\ell)}. Line L7 walks through the new pp. Two neighbors have equal R(2ℓ)R^{(2\ell)} if and only if both halves are equal, which (D) lets us test by the classes, and the new class increases by 1 exactly when the string changes. Since pp is sorted, the new class of xx is the number of distinct values below Rx(2ℓ)R_x^{(2\ell)}. Line L8 doubles ℓ\ell, so (D) holds at the next test.

Termination. ℓ\ell takes the values 1,2,4,…1, 2, 4, \ldots, so the loop stops after ⌈log⁡2n⌉\lceil \log_2 n \rceil rounds, when ℓ≥n\ell \ge n. Then by (D) and Lemma 1, cls[i]<cls[j]cls[i] < cls[j] if and only if Ri<RjR_i < R_j, and cls[i]=cls[j]cls[i] = cls[j] if and only if Ri=RjR_i = R_j. Line L9 sorts the list 0,1,…,n−10, 1, \ldots, n-1 stably by clscls. The result is sorted by rotation, and equal rotations keep the increasing order of their positions. This is the claim. For n=0n = 0, line L1 returns the empty array. □\square

Theorem (time). CYCLIC-SORT takes O(nlog⁡n)O(n \log n) time after the symbols are mapped to 0,…,σ−10, \ldots, \sigma - 1.

Proof. Lines L5 to L8 take O(n)O(n) time: a counting sort with nn buckets is O(n)O(n), and line L7 makes O(1)O(1) class comparisons for each rr. There are ⌈log⁡2n⌉\lceil \log_2 n \rceil rounds. Lines L2 and L9 are counting sorts with at most nn buckets, since σ≤n\sigma \le n. The mapping of symbols takes O(n)O(n) time for an integer alphabet of size O(n)O(n) and O(nlog⁡n)O(n \log n) comparisons for a general ordered alphabet. ■\blacksquare

Back to Circular suffix array