From 40e7bfd067aec6d830f6cd83cc07db9b8e5fb599 Mon Sep 17 00:00:00 2001 From: Hasnain <161151300+hasnain607@users.noreply.github.com> Date: Tue, 3 Nov 2026 11:46:01 +0500 Subject: [PATCH 1/4] feat(strings): add suffix array algorithm --- strings/suffix_array.py | 70 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 strings/suffix_array.py diff --git a/strings/suffix_array.py b/strings/suffix_array.py new file mode 100644 index 000000000000..4f726dbbacf3 --- /dev/null +++ b/strings/suffix_array.py @@ -0,0 +1,70 @@ +""" +https://en.wikipedia.org/wiki/Suffix_array +https://cp-algorithms.com/string/suffix-array.html + +A suffix array lists the starting positions of every suffix of a string in +lexicographic order. It needs one integer per suffix, where a suffix tree +needs a node per character, so it is the compact structure used for pattern +search and for the LCP-based algorithms built on top of it. + +Construction here uses prefix doubling (Manber-Myers). + +Time Complexity: O(n * log(n)^2) - where n is the length of the text +Space Complexity: O(n) +""" + + +def suffix_array(text: str) -> list[int]: + """ + Return the starting indices of all suffixes of `text`, sorted. + + >>> suffix_array("banana") + [5, 3, 1, 0, 4, 2] + >>> suffix_array("mississippi") + [10, 7, 4, 1, 0, 9, 8, 6, 3, 5, 2] + >>> suffix_array("abababab") + [6, 4, 2, 0, 7, 5, 3, 1] + >>> suffix_array("aaaa") + [3, 2, 1, 0] + >>> suffix_array("aça") + [2, 0, 1] + >>> suffix_array("a") + [0] + >>> suffix_array("") + [] + """ + n = len(text) + if n == 0: + return [] + + suffixes = list(range(n)) + rank = list(map(ord, text)) + k = 1 + + while k < n: + keys = [ + (rank[i], rank[i + k] if i + k < n else -1, i) + for i in range(n) + ] + keys.sort() + suffixes = [i for _, _, i in keys] + + new_rank = [0] * n + for position in range(1, n): + previous, current = suffixes[position - 1], suffixes[position] + previous_key = ( + rank[previous], + rank[previous + k] if previous + k < n else -1, + ) + current_key = ( + rank[current], + rank[current + k] if current + k < n else -1, + ) + new_rank[current] = new_rank[previous] + int(previous_key != current_key) + + rank = new_rank + if rank[suffixes[-1]] == n - 1: + break + k *= 2 + + return suffixes From 8be06b211dfc48ddc20d8f0d4b457fed59a5d60a Mon Sep 17 00:00:00 2001 From: Hasnain <161151300+hasnain607@users.noreply.github.com> Date: Tue, 3 Nov 2026 11:46:44 +0500 Subject: [PATCH 2/4] style: format suffix array implementation --- strings/suffix_array.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/strings/suffix_array.py b/strings/suffix_array.py index 4f726dbbacf3..d3477a4a374d 100644 --- a/strings/suffix_array.py +++ b/strings/suffix_array.py @@ -42,10 +42,7 @@ def suffix_array(text: str) -> list[int]: k = 1 while k < n: - keys = [ - (rank[i], rank[i + k] if i + k < n else -1, i) - for i in range(n) - ] + keys = [(rank[i], rank[i + k] if i + k < n else -1, i) for i in range(n)] keys.sort() suffixes = [i for _, _, i in keys] From cacfbac19a987ffe133b1185ce6f4cc991166b7d Mon Sep 17 00:00:00 2001 From: Hasnain <161151300+hasnain607@users.noreply.github.com> Date: Tue, 3 Nov 2026 11:50:00 +0500 Subject: [PATCH 3/4] docs: explain the non-obvious steps in suffix array --- strings/suffix_array.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/strings/suffix_array.py b/strings/suffix_array.py index d3477a4a374d..c8967ef9b7c1 100644 --- a/strings/suffix_array.py +++ b/strings/suffix_array.py @@ -38,14 +38,22 @@ def suffix_array(text: str) -> list[int]: return [] suffixes = list(range(n)) + # Ranking by code point is enough to start: Python orders strings by code + # point too, so one comparison of the first character is already right. rank = list(map(ord, text)) k = 1 while k < n: + # Sort by the first 2k characters of each suffix, which are exactly the + # pair (rank of the first k, rank of the next k). A second half that + # runs past the end gets -1: ranks are never negative, so a shorter + # suffix sorts before a longer one that shares its prefix. keys = [(rank[i], rank[i + k] if i + k < n else -1, i) for i in range(n)] keys.sort() suffixes = [i for _, _, i in keys] + # Renumber so that equal 2k-prefixes share a rank and every distinct + # prefix gets the next one. This keeps ranks dense in range(n). new_rank = [0] * n for position in range(1, n): previous, current = suffixes[position - 1], suffixes[position] @@ -60,6 +68,9 @@ def suffix_array(text: str) -> list[int]: new_rank[current] = new_rank[previous] + int(previous_key != current_key) rank = new_rank + # Dense ranks ending at n - 1 means every suffix has a unique rank, so + # the 2k-prefixes already order them completely; doubling cannot change + # the order, and the next round always breaks too, since 2k >= n. if rank[suffixes[-1]] == n - 1: break k *= 2 From 1e02edade860c6f83689f2c04290cd7aa59cf367 Mon Sep 17 00:00:00 2001 From: Hasnain <161151300+hasnain607@users.noreply.github.com> Date: Tue, 3 Nov 2026 11:50:26 +0500 Subject: [PATCH 4/4] docs: clarify the suffix array docstring --- strings/suffix_array.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/strings/suffix_array.py b/strings/suffix_array.py index c8967ef9b7c1..90f06f815b5e 100644 --- a/strings/suffix_array.py +++ b/strings/suffix_array.py @@ -9,14 +9,15 @@ Construction here uses prefix doubling (Manber-Myers). -Time Complexity: O(n * log(n)^2) - where n is the length of the text +Time Complexity: O(n log² n) - where n is the length of the text Space Complexity: O(n) """ def suffix_array(text: str) -> list[int]: """ - Return the starting indices of all suffixes of `text`, sorted. + Return the starting indices of all suffixes of `text`, in lexicographic + order. >>> suffix_array("banana") [5, 3, 1, 0, 4, 2]