What Is a String? Alphabets, Languages, and Core Relations
Formally, an alphabet $\Sigma$ is a non-empty finite set of symbols (for example, $\Sigma = \{0,1\}$ or $\Sigma = \{a,b,c\}$). A string $S$ over $\Sigma$ is a finite sequence of symbols drawn from $\Sigma$. The length of $S$, denoted $\lvert S\rvert$, is the number of symbol positions in the sequence.
The unique string containing no symbols is the empty string, denoted $\varepsilon$ (or sometimes $\lambda$), and $\lvert\varepsilon\rvert = 0$. The set of all finite strings over $\Sigma$, including the empty string, is denoted $\Sigma^*$. The set of all non-empty finite strings is
$$ \Sigma^+ = \Sigma^* \setminus \{\varepsilon\}. $$
A language over $\Sigma$ is any set of strings over that alphabet; formally, $L \subseteq \Sigma^*$.
The fundamental binary operation on strings is concatenation. Given two strings $U = u_1 u_2 \dots u_m$ and $V = v_1 v_2 \dots v_n$, their concatenation $UV$ (or $U \cdot V$) is the string $u_1 \dots u_m v_1 \dots v_n$ of length $m + n$. Concatenation is associative—$(UV)W = U(VW)$—and has $\varepsilon$ as its identity element ($S\varepsilon = \varepsilon S = S$), making $\Sigma^*$ a free monoid under concatenation.
Structural relationships between strings form the foundation of pattern matching and text indexing:
- Prefix: A string $P$ is a prefix of $S$ if there exists a string $X \in \Sigma^*$ such that $S = PX$. If $X \neq \varepsilon$, then $P$ is a proper prefix.
- Suffix: A string $F$ is a suffix of $S$ if there exists a string $Y \in \Sigma^*$ such that $S = YF$. If $Y \neq \varepsilon$, then $F$ is a proper suffix.
- Border: A string $B$ is a border of $S$ if it is both a proper prefix and a proper suffix of $S$. Under the convention that empty borders are allowed, $\varepsilon$ is a trivial border of every non-empty string. For $S = \text{"abracadabra"}$, the borders are $\varepsilon$, $\text{"a"}$, and $\text{"abra"}$.
- Substring: A string $T$ is a substring (or factor) of $S$ if there exist strings $X,Y \in \Sigma^*$ such that $S = XTY$. Thus, a substring consists of contiguous positions in $S$. An occurrence of $T$ in $S$ is specified by a starting index $i$ such that $$ S[i \dots i+\lvert T\rvert-1] = T. $$ Two occurrences occupying the inclusive ranges $[i_1,j_1]$ and $[i_2,j_2]$ overlap if $$ \max(i_1,i_2) \leq \min(j_1,j_2). $$
- Subsequence: A string $W$ is a subsequence of $S$ if it can be obtained by deleting zero or more symbols from $S$ without changing the relative order of the remaining symbols. Substrings are contiguous; subsequences need not be.
- Reversal: If $S = s_1s_2\dots s_n$, its reversal is $$ S^R = s_ns_{n-1}\dots s_1. $$
- Rotation and cyclic equivalence: A string $R$ is a rotation (or cyclic shift) of $S$ if there exist strings $U,V \in \Sigma^*$ such that $S = UV$ and $R = VU$. Two strings are cyclically equivalent if one is a rotation of the other.
For a string $S \in \Sigma^*$ of length $n$, write its positions as $S[0 \dots n-1]$.
A substring $S[i \dots j]$ is contiguous, where $0 \leq i \leq j \lt n$. A subsequence is selected using increasing indices $0 \leq i_1 \lt i_2 \lt \dots \lt i_k \lt n$ and has the form $S[i_1]S[i_2]\dots S[i_k]$.
A border of length $k$ satisfies $$ S[0 \dots k-1] = S[n-k \dots n-1], \qquad 0 \leq k \lt n. $$ The restriction $k \lt n$ makes the border proper; $k=0$ represents the empty border $\varepsilon$.
Ordering Strings: Lexicographic, Shortlex, and Colexicographic
Ordering strings requires total order relations built from a total order on the underlying alphabet $\Sigma$. Different orderings yield distinct topological properties and are suited for different computational tasks:
- Lexicographic Order ($\le_{\text{lex}}$): The standard dictionary order. $U \le_{\text{lex}} V$ if $U$ is a prefix of $V$, or if at the first position $i$ where $U[i] \neq V[i]$, we have $U[i] \lt V[i]$ in $\Sigma$. Under lexicographic order, a shorter string is not necessarily smaller than a longer string (e.g., $\text{"b"} \gt_{\text{lex}} \text{"aaaa"}$). As a result, $\Sigma^*$ under lexicographic order is not well-founded over non-singleton alphabets because infinite descending chains like $\text{"b"} \gt \text{"ab"} \gt \text{"aab"} \gt \text{"aaab"} \dots$ can exist.
- Shortlex (Radix / Length-Lexicographic) Order ($\le_{\text{shortlex}}$): Strings are compared first by length, and ties are broken lexicographically. $U \lt_{\text{shortlex}} V$ if $\lvert U\rvert \lt \lvert V\rvert$, or if $\lvert U\rvert = \lvert V\rvert$ and $U \lt_{\text{lex}} V$. Shortlex produces a well-ordering of $\Sigma^*$ where every string has a finite index in the enumeration.
- Colexicographic Order ($\le_{\text{colex}}$): Strings are compared from right to left (i.e., comparing their reversals under lexicographic order). $U \le_{\text{colex}} V$ if $U^R \le_{\text{lex}} V^R$. Colexicographic ordering clusters strings sharing common suffixes, making it useful in combinatorics and backward string matching algorithms.
| String Set | Lexicographic Order ($\le_{\text{lex}}$) | Shortlex Order ($\le_{\text{shortlex}}$) | Colexicographic Order ($\le_{\text{colex}}$) |
|---|---|---|---|
{"b", "aa", "a", "ab", "ba"} | a, aa, ab, b, ba | a, b, aa, ab, ba | a, aa, ba, b, ab |
{"10", "2", "1", "100", "20"} | 1, 10, 100, 2, 20 | 1, 2, 10, 20, 100 | 1, 10, 100, 20, 2 |
{"cat", "car", "at", "bar"} | at, bar, car, cat | at, bar, car, cat | bar, car, cat, at |
| Primary Comparison Criteria | First mismatch from left to right | Length first ($\vert{}U\vert{} \text{ vs } \vert{}V\vert{}$), then left-to-right | First mismatch from right to left |
| Well-Founded over $\Sigma^*$ | No (allows infinite descending chains) | Yes (every non-empty subset has a minimal element) | No (unless restricted to fixed lengths) |
Bytes, Code Units, Code Points, and Grapheme Clusters
Modern text processing operates across four distinct abstraction layers. Conflating these layers leads to index out-of-bounds errors, corrupted multi-byte sequences, and broken user interfaces.
- Bytes (8-bit Octets): The physical unit of storage and transmission. A single character may span 1 to 4 bytes depending on the encoding format.
- Code Units: The fundamental storage unit of a specific character encoding scheme. For UTF-8, a code unit is an 8-bit byte; for UTF-16, a 16-bit word; for UTF-32, a 32-bit dword.
- Unicode Code Points: Abstract numerical values assigned by the Unicode Standard, written as
U+XXXX(ranging fromU+0000toU+10FFFF). UTF-32 represents each code point directly in one 32-bit code unit, whereas UTF-16 uses 1 or 2 code units (surrogate pairs) and UTF-8 uses 1 to 4 code units. - Extended Grapheme Clusters: What a human user perceives as a single "character" (e.g.,
e+◌́=é, or a base emoji combined with zero-width joiners and skin-tone modifiers). A single grapheme cluster may consist of multiple code points, code units, and bytes.
flowchart TD SubGraph1["Layer 4: Extended Grapheme Cluster"] --> G1["'é' (User-Perceived Character)"] G1 --> SubGraph2["Layer 3: Unicode Code Points"] SubGraph2 --> CP1["U+0065 (LATIN SMALL LETTER E)"] SubGraph2 --> CP2["U+0301 (COMBINING ACUTE ACCENT)"] CP1 --> SubGraph3["Layer 2: UTF-16 Code Units"] CP2 --> SubGraph3 SubGraph3 --> CU1["0x0065 (16-bit)"] SubGraph3 --> CU2["0x0301 (16-bit)"] CU1 --> SubGraph4["Layer 1: UTF-8 Bytes"] CU2 --> SubGraph4 SubGraph4 --> B1["0x65"] SubGraph4 --> B2["0xCC"] SubGraph4 --> B3["0x81"]
Encodings and Unicode Normalization
Character encodings map sequence structures to abstract values. ASCII maps 0–127 to single bytes. UTF-8 uses variable-length encoding (1 to 4 bytes per code point), maintaining backward compatibility with ASCII. UTF-16 uses 2 or 4 bytes (1 or 2 16-bit code units, using surrogate pairs U+D800–U+DFFF for code points above U+FFFF). UTF-32 uses a fixed 4 bytes per code point.
Validation ensures byte streams obey structural invariants (e.g., UTF-8 continuation bytes matching 10xxxxxx). Overlong UTF-8 encodings (such as encoding U+0000 as 0xC0 0x80) are illegal for security reasons and must be rejected during validation.
Because Unicode permits equivalent text to be represented via different code point sequences, equivalence forms and normalization algorithms are necessary:
- Canonical Equivalence: Visual and functional identity (e.g.,
U+00E9[é]vsU+0065 U+0301[e + ◌́]). - Compatibility Equivalence: Visual variation preserving semantic meaning (e.g., superscript
5U+2075vs standard5U+0035, or ligatures likefiU+FB01vsf i). - NFC (Normalization Form C): Canonical Decomposition followed by Canonical Composition. Replaces combining sequences with precomposed characters where available.
- NFD (Normalization Form D): Canonical Decomposition. Fully decomposes precomposed characters into base characters and combining marks, sorted by Canonical Combining Class (CCC).
- NFKC (Normalization Form KC): Compatibility Decomposition followed by Canonical Composition.
- NFKD (Normalization Form KD): Compatibility Decomposition.
Case folding removes case distinctions for case-insensitive matching (e.g., mapping German ß to ss), which differs from locale-aware uppercase/lowercase conversion (such as Turkish i $\to$ İ).
Worked Example
Consider checking equality between two strings representing the name "Café":
- String A:
\x43\x61\x66\xC3\xA9(UTF-8 bytes forC,a,f,é[precomposedU+00E9]) - String B:
\x43\x61\x66\x65\xCC\x81(UTF-8 bytes forC,a,f,e, combining acuteU+0301)
- Naive Byte Comparison:
Comparing String A (43 61 66 C3 A9) directly to String B (43 61 66 65 CC 81) returns False because lengths (5 bytes vs 6 bytes) and byte patterns differ. 2. Code Point Inspection: String A code points: [U+0043, U+0061, U+0066, U+00E9] String B code points: [U+0043, U+0061, U+0066, U+0065, U+0301] Comparing code point arrays directly yields False.
- Normalization (NFC Transformation): Apply
unicodedata.normalize('NFC', ...)to both strings. String B decomposes then recomposesU+0065+U+0301intoU+00E9. Post-normalization Code Points for both:[U+0043, U+0061, U+0066, U+00E9]. Post-normalization Byte Comparison returnsTrue.
String Representations: Arrays, Builders, Ropes, and Piece Tables
Choosing an internal string representation involves trade-offs between contiguous memory layout, immutability, cache locality, and edit operation performance.
| Representation | Random Access ($S[i]$) | Concatenation ($S_1 + S_2$) | Slicing ($S[i \dots j]$) | Insertion / Deletion at Index $i$ | Memory Overhead |
|---|---|---|---|---|---|
| Contiguous Array (Mutable) | $O(1)$ | $O(n + m)$ | $O(j - i)$ | $O(n)$ | Low (capacity buffer padding) |
| Immutable String | $O(1)$ | $O(n + m)$ | $O(j - i)$ or $O(1)$ view | $O(n)$ (requires full copy) | Minimal (header + contiguous bytes) |
| String Builder / Gap Buffer | $O(1)$ | $O(m)$ amortized (append) | $O(j - i)$ | $O(1)$ at gap, $O(n)$ to move gap | Low to moderate |
| Interned String Pool | $O(1)$ access, $O(1)$ equality | $O(n + m)$ (plus table lookup) | $O(j - i)$ | $O(n)$ | Higher (global hash table overhead) |
| Rope (Balanced Tree) | $O(\log n)$ | $O(\log n)$ or $O(1)$ | $O(\log n)$ split | $O(\log n)$ (split + concatenate) | High (tree nodes, pointers, height balance) |
| Piece Table | $O(\log k)$ ($k$ = pieces) | $O(1)$ append to append-buffer | $O(1)$ descriptor | $O(k)$ or $O(\log k)$ piece split | Minimal (Original + Add buffers + piece list) |
| Persistent String (Finger Tree/Trie) | $O(\log n)$ | $O(\log n)$ | $O(\log n)$ | $O(\log n)$ structural sharing | High (path copying, node allocations) |
Core Operations, Complexity, and Correctness Hazards
Implementing string operations requires attention to encoding boundaries, non-copying views, and edge cases.
Never assume 1 byte = 1 code unit = 1 code point = 1 character. Naive array slicing risks splitting surrogate pairs or byte sequences, causing invalid encodings or security vulnerabilities. Always validate external byte buffers for structural correctness, normalize strings before comparison or hashing, isolate sentinel characters to prevent collision attacks, and explicitly handle zero-length inputs ($\varepsilon$).
Next in the Series
In Core Problem-Solving Patterns & String DP, we'll explore Frequency counting, sliding windows, prefix states, and dynamic-programming patterns for string problems.