<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Neil's Space</title><link>https://neilmin.com/</link><description>Recent content on Neil's Space</description><image><title>Neil's Space</title><url>https://neilmin.com/images/papermod-cover.png</url><link>https://neilmin.com/images/papermod-cover.png</link></image><generator>Hugo</generator><language>en-US</language><lastBuildDate>Sat, 13 Jun 2026 07:00:00 -0700</lastBuildDate><atom:link href="https://neilmin.com/index.xml" rel="self" type="application/rss+xml"/><item><title>How RocksDB Works: A Minimal LSM-Tree Primer</title><link>https://neilmin.com/posts/how-rocksdb-works/</link><pubDate>Sat, 13 Jun 2026 07:00:00 -0700</pubDate><guid>https://neilmin.com/posts/how-rocksdb-works/</guid><description>I spent some time really learning how RocksDB works while prepping for interviews, and these are my notes: what RocksDB is, how data gets written and read, what compaction does in the background, and the unavoidable trade-off between the three amplification factors. Not exhaustive — just the core LSM-tree ideas, shared for anyone else trying to get it.</description><content:encoded><![CDATA[<p>While prepping for interviews, I spent some time really digging into how RocksDB works — how its storage engine is designed, how data gets written, and how it gets read back. RocksDB (and the LSM-tree underneath it) is one of those things a lot of people have heard of but can&rsquo;t quite explain — I couldn&rsquo;t either, before I sat down with it. Once it clicked, I wrote up the core ideas as these notes, to share with anyone else trying to get it.</p>
<p>I won&rsquo;t claim this is exhaustive or deeply expert, but I hope it leaves you (and future me) with a clear overall picture of how RocksDB actually turns.</p>
<h2 id="what-rocksdb-is">What RocksDB is</h2>
<p>In one line: <strong>an embeddable, persistent key-value store</strong>.</p>
<ul>
<li><strong>Embeddable</strong>: it isn&rsquo;t a standalone server like MySQL — it&rsquo;s a library you compile directly into your program, which cuts out inter-process communication overhead.</li>
<li><strong>Persistent</strong>: data lives on disk; nothing is lost on a crash.</li>
<li>Forked from Google&rsquo;s <strong>LevelDB</strong> in 2012, written in C++, optimized specifically for <strong>SSDs</strong> and <strong>write-heavy</strong> workloads. Meta, Microsoft, Netflix, and Uber all use it.</li>
<li>It is <strong>not distributed</strong> — replication and sharding are your job at a higher layer.</li>
</ul>
<p>The operations it exposes are humble: <code>put(key, value)</code> to write, <code>get(key)</code> to read, <code>delete(key)</code> to remove, <code>merge(key, value)</code> to combine, and <code>iterator.seek()</code> for range scans.</p>
<h2 id="the-core-idea-the-lsm-tree">The core idea: the LSM-tree</h2>
<p>Everything in RocksDB is built on the <strong>LSM-tree (Log-Structured Merge-Tree)</strong>.</p>
<p>The core tension it tackles: <strong>disks hate random writes and love sequential ones</strong>. The LSM-tree&rsquo;s trick is to buffer writes in memory, keep them sorted, then flush them to disk sequentially all at once. In other words, it <strong>batches a flood of random writes into sequential writes</strong> — and that&rsquo;s the fundamental reason it writes so fast.</p>
<p>Structurally, data is split across many levels: the top level lives in memory, and below it sit level after level on disk, numbered L0, L1, L2… The deeper you go, the older and larger the data (each level is typically ~10× the one above it).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>memory   ┌──────────────────────────────┐
</span></span><span style="display:flex;"><span>         │  MemTable (writable, sorted)  │  ← new data lands here first
</span></span><span style="display:flex;"><span>         └──────────────────────────────┘
</span></span><span style="display:flex;"><span>- - - - - - - - - - - - - - - - - - - - - -  flush
</span></span><span style="display:flex;"><span>disk     L0   [SST] [SST] [SST]      ← newest; key ranges may overlap across files
</span></span><span style="display:flex;"><span>         L1   [SST][SST][SST][SST]   ← no overlap within a level, and bigger
</span></span><span style="display:flex;"><span>         L2   [SST][SST] ......      ← older and larger the deeper you go (~×10)
</span></span><span style="display:flex;"><span>         ...
</span></span></code></pre></div><p>This structure dates back to 1996 and was designed for write-intensive workloads. Besides RocksDB, Bigtable, HBase, Cassandra, and MongoDB&rsquo;s WiredTiger engine are all LSM-tree based.</p>
<h2 id="writing-how-data-gets-in">Writing: how data gets in</h2>
<p>A single write lands in <strong>two</strong> places at once:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>put(key, value)
</span></span><span style="display:flex;"><span>      │
</span></span><span style="display:flex;"><span>      ├──► WAL       (appended sequentially to disk, for crash safety)
</span></span><span style="display:flex;"><span>      │
</span></span><span style="display:flex;"><span>      └──► MemTable  (kept sorted in memory)
</span></span><span style="display:flex;"><span>                  │  fills up at ~64MB
</span></span><span style="display:flex;"><span>                  ▼
</span></span><span style="display:flex;"><span>            turns read-only; a background thread flushes it to one SST file → L0
</span></span></code></pre></div><p><strong>MemTable</strong>: the in-memory write buffer where every insert, update, and delete goes first. It&rsquo;s kept <strong>sorted by key</strong> internally (the default implementation is a <strong>skip list</strong>), which is what makes the later flush and range queries efficient. One detail: a delete doesn&rsquo;t actually erase anything — it writes a <strong>tombstone</strong> record meaning &ldquo;this key is deleted.&rdquo; The real cleanup is left to compaction later.</p>
<p><strong>WAL (Write-Ahead Log)</strong>: the MemTable is in memory, so a power loss would wipe it. So every write also <strong>appends</strong> a record to a WAL file on disk — key, value, operation type, and a checksum. After a crash, RocksDB replays the WAL to reconstruct the MemTable. Note the WAL is <strong>appended in write order, not sorted</strong> — it&rsquo;s optimizing purely for speed.</p>
<p><strong>Flush</strong>: once a MemTable fills up, it turns read-only and a fresh one takes over; a background thread then flushes the read-only MemTable into a single <strong>SST file</strong> on L0. Once that&rsquo;s done, the corresponding WAL can be discarded. Because the MemTable was already sorted, this flush is one <strong>sequential write</strong> — which is the whole point of the LSM-tree.</p>
<h2 id="what-an-sst-file-looks-like">What an SST file looks like</h2>
<p>An <strong>SST (Static Sorted Table)</strong> is the file that actually holds data on disk, and it&rsquo;s never modified once written. Inside is a pile of <strong>sorted key-value pairs</strong>, laid out in a carefully designed block format (blocks default to 4KB and can be compressed with Snappy, LZ4, ZSTD, etc.).</p>
<p>An SST is roughly split into a few sections:</p>
<ul>
<li><strong>Data blocks</strong>: the sorted key-value pairs. Since adjacent keys are similar, only the differences need to be stored (delta encoding) to save space.</li>
<li><strong>Index</strong>: records, for each data block, &ldquo;last key → offset in the file,&rdquo; so a lookup can <strong>binary-search</strong> straight to the right block instead of scanning the whole file.</li>
<li><strong>Bloom filter (optional)</strong>: a probabilistic structure that very quickly answers &ldquo;this key is <strong>definitely not</strong> in this file.&rdquo; It may give a false &ldquo;yes,&rdquo; but never a false &ldquo;no&rdquo; — perfect for skipping, on a read, a whole batch of files you don&rsquo;t need to touch.</li>
</ul>
<h2 id="reading-how-data-gets-found">Reading: how data gets found</h2>
<p>To read a key, you search <strong>newest to oldest</strong>, level by level — newer values sit higher, older ones lower, so the first hit is the latest value:</p>
<ol>
<li>Check the active MemTable;</li>
<li>Then the read-only MemTables not yet flushed;</li>
<li>Then each SST file in L0 (L0 files can overlap in key range, so you have to check them one by one, newest to oldest);</li>
<li>From L1 down, each level has non-overlapping key ranges, so you only need to <strong>locate and check one file per level</strong>.</li>
</ol>
<p>And within a <strong>single SST file</strong>, it&rsquo;s again three steps: first ask the <strong>Bloom filter</strong> whether the key is present — if not, skip the file entirely; if so, use the <strong>index</strong> to binary-search to the right data block; finally read that block and find the key inside it.</p>
<p>So the cost of a read comes down to how many levels and files you have to wade through — which leads straight into the next section.</p>
<h2 id="compaction-the-background-cleanup-that-never-stops">Compaction: the background cleanup that never stops</h2>
<p>As noted, a delete just writes a tombstone, and an update just writes a new value on top of the old one. Over time, the disk fills up with <strong>stale old versions and tombstones</strong>: they waste space <em>and</em> force reads to wade through more files.</p>
<p><strong>Compaction</strong> is the background job that cleans this up: it takes some SST files from one level, merges them with the overlapping files in the next level, <strong>throws away the shadowed old values and deleted keys</strong>, and writes fresh, clean SSTs into the lower level. Since every file is already sorted, the merge uses a <strong>k-way merge</strong> — a scaled-up version of the &ldquo;merge&rdquo; step in merge sort. It all runs on background threads, so it doesn&rsquo;t block foreground reads and writes.</p>
<p>RocksDB defaults to <strong>leveled compaction</strong>:</p>
<ul>
<li><strong>L0</strong> is special: its files <strong>may overlap</strong> in key range (since they&rsquo;re flushed straight from MemTables); compaction triggers once the L0 file count hits a threshold (4 by default).</li>
<li><strong>L1 and below</strong>: within each level, all files have <strong>non-overlapping</strong> key ranges and are globally ordered; when a level&rsquo;s total size exceeds its target, the excess is merged down into the next level — sometimes cascading down several levels in a chain.</li>
</ul>
<h2 id="its-all-trade-offs-the-three-amplifications">It&rsquo;s all trade-offs: the three amplifications</h2>
<p>The key to understanding RocksDB tuning (really, all LSM engines) is three <strong>amplification</strong> factors:</p>
<ul>
<li><strong>Space amplification</strong>: disk space actually used ÷ size of the logical data. The more stale versions and tombstones pile up, the higher it gets.</li>
<li><strong>Read amplification</strong>: how many I/O operations a single logical read actually performs. The more levels and files to wade through, the higher it gets.</li>
<li><strong>Write amplification</strong>: how many times a single logical write is actually written. The same piece of data gets rewritten to lower levels over and over during compaction, so this can get large.</li>
</ul>
<p>These three are a game of whack-a-mole: <strong>the more aggressively you compact, the smaller your space and read amplification, but the larger your write amplification</strong> — and vice versa. The right balance depends entirely on your workload, and the knobs are many and interdependent. Even the RocksDB authors admit it&rsquo;s hard to pin down the exact effect of each parameter, and recommend <strong>benchmarking a lot while keeping an eye on those three amplification factors</strong>.</p>
<blockquote>
<p><strong>An aside: the merge operation</strong></p>
<p>Besides put and delete, RocksDB has <code>merge</code>. When you need to apply lots of <em>incremental</em> updates to a value (say, repeatedly appending to a counter or a list), the traditional approach is read-modify-write: read it out, change it, write it back — clunky. <code>merge</code> lets you write just the <em>increment</em> and hands off the combining to a merge function you define, computing the final value only at read or compaction time. The <strong>upside</strong> is lower write amplification, plus it&rsquo;s thread-safe; the <strong>cost</strong> is that reads get more expensive — until the increments are consolidated, every read has to recompute them.</p>
</blockquote>
<h2 id="the-bits-worth-remembering">The bits worth remembering</h2>
<p>If I keep just one mental map, it&rsquo;s this:</p>
<ul>
<li><strong>RocksDB</strong> = an embeddable, persistent KV store, descended from LevelDB, built on the <strong>LSM-tree</strong>;</li>
<li><strong>Writes</strong>: into the in-memory <strong>MemTable</strong> (sorted) + a sequential <strong>WAL</strong> (crash safety) → once full, flushed to an <strong>SST</strong> file on L0 → <strong>compaction</strong> slowly tidies things downward in the background;</li>
<li><strong>Reads</strong>: search newest to oldest, level by level, using a <strong>Bloom filter</strong> + <strong>index</strong> to skip and locate so you read as few stray files as possible;</li>
<li><strong>The essence</strong>: it trades &ldquo;write amplification&rdquo; for the high throughput of &ldquo;turning random writes into sequential ones&rdquo; — and <strong>between space, read, and write amplification, it&rsquo;s always a trade-off; there&rsquo;s no free lunch</strong>.</li>
</ul>
<p>Hold onto those few lines and the overall shape of RocksDB stands up. The finer details — skip lists, delta encoding, the various compaction strategies, how to tune the knobs — you can dive into whenever you actually need them.</p>
<blockquote>
<p>A lot of my understanding here comes from Artem Krylysov&rsquo;s <a href="https://artem.krylysov.com/blog/2023/04/19/how-rocksdb-works/">How RocksDB Works</a>, which goes into far more depth — highly recommended if you want to go deeper.</p>
</blockquote>
]]></content:encoded></item><item><title>Sorting Algorithms for Coding Interviews: A Python Reference from Bubble Sort to Timsort</title><link>https://neilmin.com/posts/sorting-algorithms-interview-reference/</link><pubDate>Sat, 13 Jun 2026 00:00:00 -0700</pubDate><guid>https://neilmin.com/posts/sorting-algorithms-interview-reference/</guid><description>A reference I put together while reviewing sorting algorithms for coding interviews: Python implementations of 11 sorts, their time and space complexity, stability, and when to use each — plus quicksort partition variants and the non-comparison sorts that are easy to forget. Skim it to self-check what you still remember.</description><content:encoded><![CDATA[<p>I&rsquo;ve been prepping for coding interviews lately, and I went back through the sorting algorithms from scratch. The process gave me a bit of a scare: a lot of this I genuinely <em>used to</em> know — how quicksort&rsquo;s partition actually works, why it degrades — and now I had to pause to remember it. By the time I got to the non-comparison sorts — counting, radix, bucket — I realized that whole area had become more or less a blank.</p>
<p>So I decided to write this review down. Partly as a reference for other people getting ready for interviews, and partly as a record for my future self: next time I need to interview, I can come back here, skim through, and quickly figure out &ldquo;this one I still know, this one I forgot, let me focus there.&rdquo;</p>
<p>How to use this post:</p>
<ul>
<li>First look at the <strong>cheat sheet</strong> below — one glance tells you which algorithms you&rsquo;ve forgotten;</li>
<li>For anything you want to dig into, use the table of contents (TOC) on the right to jump straight there;</li>
<li>Every algorithm follows the same template: <strong>one-line idea → Python implementation → complexity → stability and in-place → interview notes</strong>, so they&rsquo;re easy to compare.</li>
</ul>
<p>All the code is in Python, because it reads closest to pseudocode and makes the logic easiest to see.</p>
<h2 id="one-page-cheat-sheet">One-page cheat sheet</h2>
<p>Conclusions first. The table below covers all 11 sorts in this post. When an interviewer asks about complexity or stability, this is the table that should flash into your head.</p>
<table>
  <thead>
      <tr>
          <th>Algorithm</th>
          <th>Best</th>
          <th>Average</th>
          <th>Worst</th>
          <th>Space</th>
          <th style="text-align: center">Stable</th>
          <th style="text-align: center">In-place</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Bubble</td>
          <td>O(n)</td>
          <td>O(n²)</td>
          <td>O(n²)</td>
          <td>O(1)</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>Selection</td>
          <td>O(n²)</td>
          <td>O(n²)</td>
          <td>O(n²)</td>
          <td>O(1)</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>Insertion</td>
          <td>O(n)</td>
          <td>O(n²)</td>
          <td>O(n²)</td>
          <td>O(1)</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>Shell</td>
          <td>O(n log n)</td>
          <td>≈O(n^1.3)</td>
          <td>O(n²)</td>
          <td>O(1)</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>Merge</td>
          <td>O(n log n)</td>
          <td>O(n log n)</td>
          <td>O(n log n)</td>
          <td>O(n)</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Quick</td>
          <td>O(n log n)</td>
          <td>O(n log n)</td>
          <td>O(n²)</td>
          <td>O(log n)</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>Heap</td>
          <td>O(n log n)</td>
          <td>O(n log n)</td>
          <td>O(n log n)</td>
          <td>O(1)</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>Counting</td>
          <td>O(n+k)</td>
          <td>O(n+k)</td>
          <td>O(n+k)</td>
          <td>O(n+k)</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Radix</td>
          <td>O(d·(n+k))</td>
          <td>O(d·(n+k))</td>
          <td>O(d·(n+k))</td>
          <td>O(n+k)</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Bucket</td>
          <td>O(n+k)</td>
          <td>O(n+k)</td>
          <td>O(n²)</td>
          <td>O(n+k)</td>
          <td style="text-align: center">✅*</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Timsort</td>
          <td>O(n)</td>
          <td>O(n log n)</td>
          <td>O(n log n)</td>
          <td>O(n)</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
  </tbody>
</table>
<p>A few notes so the table doesn&rsquo;t mislead you:</p>
<ul>
<li><strong>Shell sort</strong>&rsquo;s complexity depends on the <em>gap sequence</em>; its best case changes with the sequence you pick, so the numbers here are just typical orders of magnitude.</li>
<li><strong>Quick sort</strong>&rsquo;s listed space is the average recursion-stack depth O(log n); the worst case degrades to O(n). It partitions in place, but the recursion itself uses the stack.</li>
<li><strong>Bucket sort</strong>&rsquo;s stability has an asterisk: it&rsquo;s only stable if the per-bucket sort (e.g. insertion sort) is stable.</li>
<li><strong>k</strong> is the range of values, <strong>d</strong> is the number of digits — the complexity of the non-comparison sorts is always tied to properties of the data itself, which I&rsquo;ll get into below.</li>
</ul>
<h2 id="before-we-start-a-few-unavoidable-concepts">Before we start: a few unavoidable concepts</h2>
<p>Before going through the algorithms one by one, there are four concepts that nearly every sorting interview question relies on. Getting them straight first means I won&rsquo;t have to keep re-explaining them.</p>
<h3 id="comparison-vs-non-comparison-sorts">Comparison vs. non-comparison sorts</h3>
<p>A <strong>comparison sort</strong> decides order using only one operation: &ldquo;which of these two elements is bigger?&rdquo; Bubble, insertion, merge, quick, and heap are all comparison sorts. What they share: their theoretical lower bound is O(n log n) — nothing can beat it (the reason is below).</p>
<p>A <strong>non-comparison sort</strong> doesn&rsquo;t compare; instead it uses the element values themselves to <em>compute</em> where each one belongs. Counting, radix, and bucket are all like this. Because they sidestep comparison, they can hit linear time O(n) — but the price is extra requirements on the data (e.g. it must be integers in a bounded range).</p>
<h3 id="stability">Stability</h3>
<p>If two elements have equal sort keys, and their <strong>relative order</strong> is preserved after sorting, the sort is <strong>stable</strong>.</p>
<p>A concrete example. Say you have a batch of orders already sorted by time, and now you want to re-sort by amount:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Before (sorted by time):  ($100, 9:00)  ($50, 9:01)  ($100, 9:02)
</span></span><span style="display:flex;"><span>Stable sort (by amount):  ($50, 9:01)  ($100, 9:00)  ($100, 9:02)   ← the two $100s keep their time order
</span></span><span style="display:flex;"><span>Unstable sort:            ($50, 9:01)  ($100, 9:02)  ($100, 9:00)   ← the two $100s got scrambled
</span></span></code></pre></div><p>Why do interviewers love this? Because <strong>multi-key sorting</strong> depends on it: sort by the secondary key first, then use a stable sort on the primary key, and the secondary order is preserved. Knowing which sorts are stable (bubble, insertion, merge, counting, radix, Timsort) and which aren&rsquo;t (selection, shell, quick, heap) is almost guaranteed to come up.</p>
<h3 id="in-place-sorting">In-place sorting</h3>
<p>If a sort needs only O(1) or O(log n) extra space, it&rsquo;s <strong>in-place</strong>. Merge sort allocates an extra O(n) array, so it isn&rsquo;t in-place; quick and heap only shuffle the original array, so they are. When an interviewer presses &ldquo;what if memory is tight?&rdquo;, this is usually what they&rsquo;re asking about.</p>
<h3 id="why-complexity-splits-into-best--average--worst">Why complexity splits into best / average / worst</h3>
<p>The same algorithm can behave wildly differently on different inputs. Quicksort is the classic case: O(n log n) on random input, but if the input is already sorted <em>and</em> you keep picking the worst pivot, it degrades to O(n²). When you state complexity in an interview, it&rsquo;s best to say which case you mean — that&rsquo;s exactly where you show how deeply you understand it.</p>
<blockquote>
<p><strong>Why can&rsquo;t comparison sorts beat O(n log n)?</strong>
Any comparison sort can be drawn as a <em>decision tree</em>: each internal node is one comparison, each leaf is one possible final arrangement. There are n! possible arrangements of n elements, so the tree must have at least n! leaves. A binary tree of height h has at most 2ʰ leaves, so 2ʰ ≥ n!, i.e. h ≥ log₂(n!). By Stirling&rsquo;s approximation, log₂(n!) ≈ n log n. The tree&rsquo;s height <em>is</em> the number of comparisons in the worst case, so the lower bound is Ω(n log n). This also explains why going faster means dropping &ldquo;comparison&rdquo; entirely — which is what the non-comparison sorts do.</p>
</blockquote>
<p>OK, concepts done. Let&rsquo;s go through them one by one.</p>
<h2 id="comparison-based-sorts">Comparison-based sorts</h2>
<h3 id="bubble-sort">Bubble Sort</h3>
<p><strong>One-line idea</strong>: compare adjacent elements pairwise, swap if out of order; each pass &ldquo;bubbles&rdquo; the current largest element to the end.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">bubble_sort</span>(arr):
</span></span><span style="display:flex;"><span>    n <span style="color:#f92672">=</span> len(arr)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(n <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>):
</span></span><span style="display:flex;"><span>        swapped <span style="color:#f92672">=</span> <span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># each pass bubbles the largest of the unsorted region to the right end</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> j <span style="color:#f92672">in</span> range(n <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span> <span style="color:#f92672">-</span> i):
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> arr[j] <span style="color:#f92672">&gt;</span> arr[j <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>]:
</span></span><span style="display:flex;"><span>                arr[j], arr[j <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>] <span style="color:#f92672">=</span> arr[j <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>], arr[j]
</span></span><span style="display:flex;"><span>                swapped <span style="color:#f92672">=</span> <span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> swapped:          <span style="color:#75715e"># a whole pass with no swaps means it&#39;s sorted; quit early</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span></code></pre></div><ul>
<li><strong>Complexity</strong>: worst and average are both O(n²); with the <code>swapped</code> early-exit, it&rsquo;s O(n) on already-sorted input. Space O(1).</li>
<li><strong>Stability / in-place</strong>: stable (only swaps on a strict greater-than), in-place.</li>
<li><strong>Interview notes</strong>: basically never used in practice, but it&rsquo;s the textbook example of &ldquo;stable + early-exit reaches O(n)&rdquo;. Watch out for that <code>swapped</code> optimization — it&rsquo;s a common gotcha.</li>
<li><strong>LeetCode</strong>: <a href="https://leetcode.com/problems/sort-an-array/">912. Sort an Array</a> — there&rsquo;s no problem dedicated to bubble sort, but this generic sorting problem is a fine sandbox to practice the implementation (pure O(n²) will time out on large inputs, so it&rsquo;s practice only).</li>
</ul>
<h3 id="selection-sort">Selection Sort</h3>
<p><strong>One-line idea</strong>: each pass picks the smallest element from the unsorted region and places it at the end of the sorted region.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">selection_sort</span>(arr):
</span></span><span style="display:flex;"><span>    n <span style="color:#f92672">=</span> len(arr)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(n <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>):
</span></span><span style="display:flex;"><span>        min_idx <span style="color:#f92672">=</span> i
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># find the index of the minimum in the unsorted region [i+1, n)</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> j <span style="color:#f92672">in</span> range(i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>, n):
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> arr[j] <span style="color:#f92672">&lt;</span> arr[min_idx]:
</span></span><span style="display:flex;"><span>                min_idx <span style="color:#f92672">=</span> j
</span></span><span style="display:flex;"><span>        arr[i], arr[min_idx] <span style="color:#f92672">=</span> arr[min_idx], arr[i]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span></code></pre></div><ul>
<li><strong>Complexity</strong>: O(n²) no matter what the input looks like — it never speeds up on sorted data. Space O(1).</li>
<li><strong>Stability / in-place</strong>: <strong>unstable</strong>, in-place. For example <code>[5a, 5b, 2]</code>: the first pass swaps <code>2</code> with <code>5a</code>, and the two 5s flip relative order.</li>
<li><strong>Interview notes</strong>: its one redeeming trait is the <strong>minimum number of swaps</strong> (at most n−1), which matters when writes are expensive. It&rsquo;s also the counterexample to &ldquo;best case can save you&rdquo; — it never does — and is often compared against insertion sort.</li>
<li><strong>LeetCode</strong>: <a href="https://leetcode.com/problems/sort-an-array/">912. Sort an Array</a> — practice the implementation on this generic problem; selection sort is a good way to feel &ldquo;few swaps, but no fewer comparisons.&rdquo;</li>
</ul>
<h3 id="insertion-sort">Insertion Sort</h3>
<p><strong>One-line idea</strong>: like sorting a hand of cards — go left to right, inserting each new card into its correct spot among the already-sorted cards on the left.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">insertion_sort</span>(arr):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">1</span>, len(arr)):
</span></span><span style="display:flex;"><span>        key <span style="color:#f92672">=</span> arr[i]
</span></span><span style="display:flex;"><span>        j <span style="color:#f92672">=</span> i <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># shift everything bigger than key one slot right to make room</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">while</span> j <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">and</span> arr[j] <span style="color:#f92672">&gt;</span> key:
</span></span><span style="display:flex;"><span>            arr[j <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>] <span style="color:#f92672">=</span> arr[j]
</span></span><span style="display:flex;"><span>            j <span style="color:#f92672">-=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        arr[j <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>] <span style="color:#f92672">=</span> key
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span></code></pre></div><ul>
<li><strong>Complexity</strong>: worst and average O(n²); close to O(n) on <strong>nearly-sorted</strong> input. Space O(1).</li>
<li><strong>Stability / in-place</strong>: stable (the <code>while</code> condition uses <code>&gt;</code>, not <code>&gt;=</code>), in-place.</li>
<li><strong>Interview notes</strong>: don&rsquo;t underestimate it. <strong>On small or nearly-sorted data, insertion sort beats quicksort</strong>, which is exactly why production-grade sorts like Timsort and Introsort fall back to it on small chunks. Of the three basic sorts, it&rsquo;s the most practically useful.</li>
<li><strong>LeetCode</strong>: <a href="https://leetcode.com/problems/insertion-sort-list/">147. Insertion Sort List</a> — a problem built for insertion sort: insert in place on a linked list.</li>
</ul>
<h3 id="shell-sort">Shell Sort</h3>
<p><strong>One-line idea</strong>: an upgraded insertion sort. First do insertion sort on elements spaced by a large &ldquo;gap&rdquo;, then shrink the gap step by step; the last pass uses gap 1 (plain insertion sort), but by then the array is &ldquo;mostly sorted&rdquo;, so it flies.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">shell_sort</span>(arr):
</span></span><span style="display:flex;"><span>    n <span style="color:#f92672">=</span> len(arr)
</span></span><span style="display:flex;"><span>    gap <span style="color:#f92672">=</span> n <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> gap <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># insertion sort on each subsequence with stride gap</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(gap, n):
</span></span><span style="display:flex;"><span>            key <span style="color:#f92672">=</span> arr[i]
</span></span><span style="display:flex;"><span>            j <span style="color:#f92672">=</span> i <span style="color:#f92672">-</span> gap
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">while</span> j <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">and</span> arr[j] <span style="color:#f92672">&gt;</span> key:
</span></span><span style="display:flex;"><span>                arr[j <span style="color:#f92672">+</span> gap] <span style="color:#f92672">=</span> arr[j]
</span></span><span style="display:flex;"><span>                j <span style="color:#f92672">-=</span> gap
</span></span><span style="display:flex;"><span>            arr[j <span style="color:#f92672">+</span> gap] <span style="color:#f92672">=</span> key
</span></span><span style="display:flex;"><span>        gap <span style="color:#f92672">//=</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span></code></pre></div><ul>
<li><strong>Complexity</strong>: depends on the gap sequence. The <code>n//2</code> halving sequence above is O(n²) in the worst case; better sequences (Knuth&rsquo;s <code>3k+1</code>, Sedgewick&rsquo;s) reach O(n^1.5) or better. Space O(1).</li>
<li><strong>Stability / in-place</strong>: <strong>unstable</strong> (gapped swaps scramble the relative order of equal elements), in-place.</li>
<li><strong>Interview notes</strong>: it&rsquo;s the poster child for &ldquo;making data roughly sorted first lets insertion sort go faster.&rdquo; Rarely asked directly, but worth knowing as the bridge — it pushes a simple O(n²) sort toward O(n log n).</li>
<li><strong>LeetCode</strong>: <a href="https://leetcode.com/problems/sort-an-array/">912. Sort an Array</a> — use it to practice shell sort and experiment with how different gap sequences affect runtime.</li>
</ul>
<h3 id="merge-sort">Merge Sort</h3>
<p><strong>One-line idea</strong>: divide and conquer. Split the array in half until you can&rsquo;t split further, then merge two <strong>already-sorted</strong> small arrays into one larger sorted array.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">merge_sort</span>(arr):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> len(arr) <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>    mid <span style="color:#f92672">=</span> len(arr) <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>    left <span style="color:#f92672">=</span> merge_sort(arr[:mid])
</span></span><span style="display:flex;"><span>    right <span style="color:#f92672">=</span> merge_sort(arr[mid:])
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> merge(left, right)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">merge</span>(left, right):
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    i <span style="color:#f92672">=</span> j <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># two pointers, take the smaller of the two each time; &lt;= keeps it stable</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> i <span style="color:#f92672">&lt;</span> len(left) <span style="color:#f92672">and</span> j <span style="color:#f92672">&lt;</span> len(right):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> left[i] <span style="color:#f92672">&lt;=</span> right[j]:
</span></span><span style="display:flex;"><span>            result<span style="color:#f92672">.</span>append(left[i])
</span></span><span style="display:flex;"><span>            i <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            result<span style="color:#f92672">.</span>append(right[j])
</span></span><span style="display:flex;"><span>            j <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    result<span style="color:#f92672">.</span>extend(left[i:])   <span style="color:#75715e"># whatever&#39;s left just gets appended</span>
</span></span><span style="display:flex;"><span>    result<span style="color:#f92672">.</span>extend(right[j:])
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result
</span></span></code></pre></div><ul>
<li><strong>Complexity</strong>: best, average, and worst are <strong>all O(n log n)</strong> — rock solid, never degrades. Space O(n) (the merge needs an extra array).</li>
<li><strong>Stability / in-place</strong>: stable, <strong>not in-place</strong>.</li>
<li><strong>Interview notes</strong>: bulletproof complexity and naturally stable — the first choice when you need &ldquo;stable + guaranteed O(n log n) worst case.&rdquo;</li>
<li><strong>LeetCode</strong>: <a href="https://leetcode.com/problems/sort-list/">148. Sort List</a> — the optimal solution for sorting a linked list is merge sort; for the array version use <a href="https://leetcode.com/problems/sort-an-array/">912. Sort an Array</a>.</li>
</ul>
<p>Two high-frequency extensions:</p>
<blockquote>
<p><strong>Concept break: linked-list sorting and external sorting</strong></p>
<p><strong>Linked-list sorting</strong>: merge sort is especially friendly to linked lists — merging only rewires pointers, no extra array needed, so it can achieve O(1) extra space (not counting the recursion stack). This is why the standard answer to &ldquo;sort a linked list in O(n log n)&rdquo; is merge sort, not quicksort.</p>
<p><strong>External sorting</strong>: when the data is too big to fit in memory (the classic interview question: &ldquo;how do you sort a 10 GB file with 1 GB of memory?&rdquo;), the answer is <strong>external merge sort</strong> — split the big file into chunks small enough to fit in memory, read each in, sort it, write it back to disk, then use a <em>k-way merge</em> to combine those sorted files into the final result. Merge&rsquo;s essence — &ldquo;combining multiple sorted sequences&rdquo; — is taken to the extreme here.</p>
</blockquote>
<h3 id="quick-sort">Quick Sort</h3>
<p>This is the section I most needed to pick back up — the partition details got fuzzy after five years untouched. Let&rsquo;s take it slow.</p>
<p><strong>One-line idea</strong>: divide and conquer. Pick a pivot, <strong>partition</strong> the array into &ldquo;less than the pivot&rdquo; and &ldquo;greater than the pivot&rdquo;, put the pivot in its final place, then recurse on both sides.</p>
<h4 id="1-lomuto-partition-the-easiest-to-memorize">1. Lomuto partition (the easiest to memorize)</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">quick_sort</span>(arr, low<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>, high<span style="color:#f92672">=</span><span style="color:#66d9ef">None</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> high <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>        high <span style="color:#f92672">=</span> len(arr) <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> low <span style="color:#f92672">&lt;</span> high:
</span></span><span style="display:flex;"><span>        p <span style="color:#f92672">=</span> partition(arr, low, high)
</span></span><span style="display:flex;"><span>        quick_sort(arr, low, p <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>)    <span style="color:#75715e"># recurse left half</span>
</span></span><span style="display:flex;"><span>        quick_sort(arr, p <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>, high)   <span style="color:#75715e"># recurse right half</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">partition</span>(arr, low, high):
</span></span><span style="display:flex;"><span>    pivot <span style="color:#f92672">=</span> arr[high]            <span style="color:#75715e"># Lomuto: always take the rightmost element as pivot</span>
</span></span><span style="display:flex;"><span>    i <span style="color:#f92672">=</span> low <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>                  <span style="color:#75715e"># i is the right boundary of the &#34;less than pivot&#34; region</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> j <span style="color:#f92672">in</span> range(low, high):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> arr[j] <span style="color:#f92672">&lt;</span> pivot:
</span></span><span style="display:flex;"><span>            i <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            arr[i], arr[j] <span style="color:#f92672">=</span> arr[j], arr[i]
</span></span><span style="display:flex;"><span>    arr[i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>], arr[high] <span style="color:#f92672">=</span> arr[high], arr[i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>]   <span style="color:#75715e"># put the pivot in place</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>Lomuto&rsquo;s advantage is that it advances a single pointer <code>i</code>, so the logic is intuitive and easy to remember. For hand-writing quicksort in an interview, this version is the default.</p>
<h4 id="2-why-it-degrades-and-how-to-fix-it">2. Why it degrades, and how to fix it</h4>
<p>Always taking the rightmost element as pivot has a fatal flaw: <strong>when the input is already sorted (or reverse-sorted), every partition splits the array into sizes 0 and n−1</strong>, the recursion depth becomes n, complexity degrades to O(n²), and it can blow the stack.</p>
<p>The fix is to <strong>stop letting the input &ldquo;predict&rdquo; the pivot</strong> — pick one at random, or take the median of the first, middle, and last elements (median-of-three):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> random
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">partition</span>(arr, low, high):
</span></span><span style="display:flex;"><span>    rand <span style="color:#f92672">=</span> random<span style="color:#f92672">.</span>randint(low, high)
</span></span><span style="display:flex;"><span>    arr[rand], arr[high] <span style="color:#f92672">=</span> arr[high], arr[rand]   <span style="color:#75715e"># random pivot, swap it to the right, reuse the logic above</span>
</span></span><span style="display:flex;"><span>    pivot <span style="color:#f92672">=</span> arr[high]
</span></span><span style="display:flex;"><span>    i <span style="color:#f92672">=</span> low <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> j <span style="color:#f92672">in</span> range(low, high):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> arr[j] <span style="color:#f92672">&lt;</span> pivot:
</span></span><span style="display:flex;"><span>            i <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            arr[i], arr[j] <span style="color:#f92672">=</span> arr[j], arr[i]
</span></span><span style="display:flex;"><span>    arr[i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>], arr[high] <span style="color:#f92672">=</span> arr[high], arr[i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>Two added lines plug the most common pitfall — degrading on sorted input. When the interviewer presses &ldquo;what about quicksort&rsquo;s worst case&rdquo;, this is the standard answer.</p>
<h4 id="3-three-way-quicksort-handling-lots-of-duplicates">3. Three-way quicksort: handling lots of duplicates</h4>
<p>If the array has <strong>many duplicate values</strong> (say, all 0s and 1s), plain quicksort still does a lot of pointless recursion. Three-way quicksort (based on the &ldquo;Dutch national flag problem&rdquo;) splits the array into <code>&lt; pivot</code>, <code>== pivot</code>, and <code>&gt; pivot</code>, and skips the whole equal-to-pivot segment:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">quick_sort_3way</span>(arr, low<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>, high<span style="color:#f92672">=</span><span style="color:#66d9ef">None</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> high <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>        high <span style="color:#f92672">=</span> len(arr) <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> low <span style="color:#f92672">&gt;=</span> high:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>    pivot <span style="color:#f92672">=</span> arr[low]
</span></span><span style="display:flex;"><span>    lt, i, gt <span style="color:#f92672">=</span> low, low, high   <span style="color:#75715e"># [low,lt)&lt;pivot  [lt,i)==pivot  (gt,high]&gt;pivot</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> i <span style="color:#f92672">&lt;=</span> gt:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> arr[i] <span style="color:#f92672">&lt;</span> pivot:
</span></span><span style="display:flex;"><span>            arr[lt], arr[i] <span style="color:#f92672">=</span> arr[i], arr[lt]
</span></span><span style="display:flex;"><span>            lt <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            i <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">elif</span> arr[i] <span style="color:#f92672">&gt;</span> pivot:
</span></span><span style="display:flex;"><span>            arr[gt], arr[i] <span style="color:#f92672">=</span> arr[i], arr[gt]
</span></span><span style="display:flex;"><span>            gt <span style="color:#f92672">-=</span> <span style="color:#ae81ff">1</span>               <span style="color:#75715e"># the swapped-in element isn&#39;t checked yet, so i stays</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            i <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    quick_sort_3way(arr, low, lt <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    quick_sort_3way(arr, gt <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>, high)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span></code></pre></div><ul>
<li><strong>Complexity</strong>: average O(n log n), worst O(n²) (almost never seen once you use a random pivot). Space O(log n), for the recursion stack.</li>
<li><strong>Stability / in-place</strong>: <strong>unstable</strong> (the long-distance swaps in partitioning scramble equal elements), <strong>in-place</strong>.</li>
<li><strong>Interview notes</strong>: default to Lomuto when hand-writing; bring up random / median-of-three when asked about the worst case; bring up three-way quicksort when asked about lots of duplicates.</li>
<li><strong>LeetCode</strong>: <a href="https://leetcode.com/problems/sort-an-array/">912. Sort an Array</a> — remember to use a random pivot on submission, or sorted / heavily-duplicated data will time out or overflow the recursion stack.</li>
</ul>
<p>One more high-frequency extension:</p>
<blockquote>
<p><strong>Concept break: Quickselect</strong></p>
<p>&ldquo;Find the k-th largest / smallest element&rdquo; is an interview regular. If you only need the k-th one, there&rsquo;s no need to fully sort: use quicksort&rsquo;s partition, and after each partition look at where the pivot landed — then <strong>recurse into only the side that contains k</strong>. Average O(n), faster than sorting first and then indexing (O(n log n)).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">quickselect</span>(arr, k):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;return the k-th smallest element, k counting from 1&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    low, high, target <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>, len(arr) <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>, k <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> low <span style="color:#f92672">&lt;=</span> high:
</span></span><span style="display:flex;"><span>        p <span style="color:#f92672">=</span> partition(arr, low, high)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> p <span style="color:#f92672">==</span> target:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> arr[p]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">elif</span> p <span style="color:#f92672">&lt;</span> target:
</span></span><span style="display:flex;"><span>            low <span style="color:#f92672">=</span> p <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>        <span style="color:#75715e"># target is on the right</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            high <span style="color:#f92672">=</span> p <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>       <span style="color:#75715e"># target is on the left</span>
</span></span></code></pre></div><p><strong>Practice</strong>: <a href="https://leetcode.com/problems/kth-largest-element-in-an-array/">215. Kth Largest Element in an Array</a> — solve it with quickselect at average O(n), a nice contrast to the heap solution.</p>
</blockquote>
<h3 id="heap-sort">Heap Sort</h3>
<p><strong>One-line idea</strong>: first build the array into a <strong>max-heap</strong> (every parent ≥ its children), so the root is the maximum; swap the root to the end, shrink the heap by one, sift the new root down, and repeat until sorted.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">heap_sort</span>(arr):
</span></span><span style="display:flex;"><span>    n <span style="color:#f92672">=</span> len(arr)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 1. build the heap: starting from the last non-leaf node, sift each one down</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(n <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>, <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>):
</span></span><span style="display:flex;"><span>        sift_down(arr, i, n)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 2. repeatedly swap the root (max) to the end, then fix the remaining heap</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> end <span style="color:#f92672">in</span> range(n <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">0</span>, <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>):
</span></span><span style="display:flex;"><span>        arr[<span style="color:#ae81ff">0</span>], arr[end] <span style="color:#f92672">=</span> arr[end], arr[<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>        sift_down(arr, <span style="color:#ae81ff">0</span>, end)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">sift_down</span>(arr, root, size):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span>        largest <span style="color:#f92672">=</span> root
</span></span><span style="display:flex;"><span>        left, right <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">*</span> root <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span> <span style="color:#f92672">*</span> root <span style="color:#f92672">+</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> left <span style="color:#f92672">&lt;</span> size <span style="color:#f92672">and</span> arr[left] <span style="color:#f92672">&gt;</span> arr[largest]:
</span></span><span style="display:flex;"><span>            largest <span style="color:#f92672">=</span> left
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> right <span style="color:#f92672">&lt;</span> size <span style="color:#f92672">and</span> arr[right] <span style="color:#f92672">&gt;</span> arr[largest]:
</span></span><span style="display:flex;"><span>            largest <span style="color:#f92672">=</span> right
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> largest <span style="color:#f92672">==</span> root:      <span style="color:#75715e"># the parent is already the largest; stop sinking</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>        arr[root], arr[largest] <span style="color:#f92672">=</span> arr[largest], arr[root]
</span></span><span style="display:flex;"><span>        root <span style="color:#f92672">=</span> largest
</span></span></code></pre></div><ul>
<li><strong>Complexity</strong>: best, average, and worst are <strong>all O(n log n)</strong>. Building the heap is O(n) (not O(n log n) — a commonly-tested counterintuitive point), then n sift-downs of O(log n) each. Space O(1).</li>
<li><strong>Stability / in-place</strong>: <strong>unstable</strong>, <strong>in-place</strong>.</li>
<li><strong>Interview notes</strong>: it&rsquo;s the <strong>only</strong> sort that both guarantees worst-case O(n log n) <em>and</em> uses only O(1) space — pick it when memory is extremely tight and you can&rsquo;t afford to degrade. Note that it&rsquo;s the same machinery as a <strong>priority queue / heap</strong>: <code>heapq</code>, Top-K problems, the heap inside Dijkstra — all variants of this <code>sift_down</code>. Maintaining a size-k min-heap to find the Top-K is a chained follow-up in this area.</li>
<li><strong>LeetCode</strong>: <a href="https://leetcode.com/problems/kth-largest-element-in-an-array/">215. Kth Largest Element in an Array</a> — the classic heap problem (maintain a size-k min-heap); it can also be solved with quickselect, a nice way to contrast the two approaches.</li>
</ul>
<h2 id="non-comparison-based-sorts">Non-comparison-based sorts</h2>
<p>Every algorithm so far relies on &ldquo;comparison&rdquo;, which is why they&rsquo;re stuck at the O(n log n) line. The next three sidestep comparison, using <strong>the element values themselves</strong> as indices to place items — which lets them hit linear time. The price is requirements on the data. This is also the area where my own memory was blankest, so I&rsquo;ll go into a bit more detail.</p>
<h3 id="counting-sort">Counting Sort</h3>
<p><strong>One-line idea</strong>: count how many times each value occurs, then use a <em>prefix sum</em> to compute each value&rsquo;s position in the result and drop it straight in. Good for <strong>integers with a small value range k</strong>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">counting_sort</span>(arr):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> arr:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>    lo, hi <span style="color:#f92672">=</span> min(arr), max(arr)
</span></span><span style="display:flex;"><span>    k <span style="color:#f92672">=</span> hi <span style="color:#f92672">-</span> lo <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    count <span style="color:#f92672">=</span> [<span style="color:#ae81ff">0</span>] <span style="color:#f92672">*</span> k
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> x <span style="color:#f92672">in</span> arr:                <span style="color:#75715e"># 1. count</span>
</span></span><span style="display:flex;"><span>        count[x <span style="color:#f92672">-</span> lo] <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">1</span>, k):        <span style="color:#75715e"># 2. prefix sum: count[i] becomes &#34;number of elements &lt;= i&#34;</span>
</span></span><span style="display:flex;"><span>        count[i] <span style="color:#f92672">+=</span> count[i <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> [<span style="color:#ae81ff">0</span>] <span style="color:#f92672">*</span> len(arr)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> x <span style="color:#f92672">in</span> reversed(arr):      <span style="color:#75715e"># 3. fill back-to-front to stay stable</span>
</span></span><span style="display:flex;"><span>        count[x <span style="color:#f92672">-</span> lo] <span style="color:#f92672">-=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        result[count[x <span style="color:#f92672">-</span> lo]] <span style="color:#f92672">=</span> x
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result
</span></span></code></pre></div><ul>
<li><strong>Complexity</strong>: O(n + k), where n is the element count and k is the value range. Space O(n + k).</li>
<li><strong>Stability / in-place</strong>: stable (the key is iterating <strong>back-to-front</strong> in step 3), not in-place.</li>
<li><strong>Interview notes</strong>: when k is far smaller than n (e.g. sorting a hundred thousand scores in 0–100), it crushes any O(n log n) sort. But once k is large (e.g. sorting arbitrary 32-bit integers), the space blows up — that&rsquo;s exactly its limit, and the problem radix sort exists to solve.</li>
<li><strong>LeetCode</strong>: <a href="https://leetcode.com/problems/sort-colors/">75. Sort Colors</a> — only three values (0, 1, 2), so counting sort (or three-way quicksort) handles it in one pass.</li>
</ul>
<h3 id="radix-sort">Radix Sort</h3>
<p><strong>One-line idea</strong>: sort digit by digit. Starting from the least significant digit (ones place), run one <strong>stable</strong> counting sort per digit, all the way up to the most significant digit. Because each pass is stable, the whole thing is sorted once you finish the highest digit.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">radix_sort</span>(arr):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> arr:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>    max_val <span style="color:#f92672">=</span> max(arr)
</span></span><span style="display:flex;"><span>    exp <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>                           <span style="color:#75715e"># current digit: 1=ones, 10=tens, 100=hundreds...</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> max_val <span style="color:#f92672">//</span> exp <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>        arr <span style="color:#f92672">=</span> counting_sort_by_digit(arr, exp)
</span></span><span style="display:flex;"><span>        exp <span style="color:#f92672">*=</span> <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">counting_sort_by_digit</span>(arr, exp):
</span></span><span style="display:flex;"><span>    count <span style="color:#f92672">=</span> [<span style="color:#ae81ff">0</span>] <span style="color:#f92672">*</span> <span style="color:#ae81ff">10</span>                  <span style="color:#75715e"># base 10, each digit is only 0-9</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> x <span style="color:#f92672">in</span> arr:
</span></span><span style="display:flex;"><span>        count[(x <span style="color:#f92672">//</span> exp) <span style="color:#f92672">%</span> <span style="color:#ae81ff">10</span>] <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">10</span>):
</span></span><span style="display:flex;"><span>        count[i] <span style="color:#f92672">+=</span> count[i <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> [<span style="color:#ae81ff">0</span>] <span style="color:#f92672">*</span> len(arr)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> x <span style="color:#f92672">in</span> reversed(arr):           <span style="color:#75715e"># back-to-front to keep this digit&#39;s sort stable (key to radix sort&#39;s correctness)</span>
</span></span><span style="display:flex;"><span>        digit <span style="color:#f92672">=</span> (x <span style="color:#f92672">//</span> exp) <span style="color:#f92672">%</span> <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>        count[digit] <span style="color:#f92672">-=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        result[count[digit]] <span style="color:#f92672">=</span> x
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result
</span></span></code></pre></div><ul>
<li><strong>Complexity</strong>: O(d·(n + k)), where d is the number of digits in the largest value and k is the base (here, base 10, so k=10). Space O(n + k).</li>
<li><strong>Stability / in-place</strong>: stable, not in-place.</li>
<li><strong>Interview notes</strong>: it solves counting sort&rsquo;s &ldquo;space blows up when the range is large&rdquo; problem — by breaking a big integer into a few small digits. The version above only handles non-negative integers; to support negatives, shift everything to be non-negative first, or handle positives and negatives separately. Common interview questions: <strong>why must you go from low digit to high digit? Why must each digit&rsquo;s sort be stable?</strong> (Because sorting a higher digit relies on stability to preserve the order already established by the lower digits.)</li>
<li><strong>LeetCode</strong>: <a href="https://leetcode.com/problems/maximum-gap/">164. Maximum Gap</a> — it demands linear time and space, and the standard solution is exactly radix sort or bucket sort.</li>
</ul>
<h3 id="bucket-sort">Bucket Sort</h3>
<p><strong>One-line idea</strong>: distribute the data evenly into a number of &ldquo;buckets&rdquo; by value, sort each bucket internally, then concatenate the buckets in order. Good for <strong>uniformly distributed</strong> data.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">bucket_sort</span>(arr):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> arr:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>    n <span style="color:#f92672">=</span> len(arr)
</span></span><span style="display:flex;"><span>    buckets <span style="color:#f92672">=</span> [[] <span style="color:#66d9ef">for</span> _ <span style="color:#f92672">in</span> range(n)]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> x <span style="color:#f92672">in</span> arr:                     <span style="color:#75715e"># assume elements are uniformly distributed in [0, 1)</span>
</span></span><span style="display:flex;"><span>        buckets[int(n <span style="color:#f92672">*</span> x)]<span style="color:#f92672">.</span>append(x)
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> bucket <span style="color:#f92672">in</span> buckets:
</span></span><span style="display:flex;"><span>        insertion_sort(bucket)        <span style="color:#75715e"># stable sort within buckets keeps the whole thing stable</span>
</span></span><span style="display:flex;"><span>        result<span style="color:#f92672">.</span>extend(bucket)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result
</span></span></code></pre></div><ul>
<li><strong>Complexity</strong>: average O(n + k) when the data is uniformly distributed; the worst case (all elements crammed into one bucket) degrades to O(n²). Space O(n + k).</li>
<li><strong>Stability / in-place</strong>: depends on the per-bucket sort — stable if you use insertion sort; not in-place.</li>
<li><strong>Interview notes</strong>: its performance rides entirely on &ldquo;is the data uniformly distributed&rdquo;, which is the biggest difference from counting and radix. Counting and radix are insensitive to the shape of the data; bucket sort is sensitive to it. Classic use case: sorting a batch of floats uniformly distributed in [0, 1).</li>
<li><strong>LeetCode</strong>: <a href="https://leetcode.com/problems/top-k-frequent-elements/">347. Top K Frequent Elements</a> — bucketing by frequency is the slickest solution to this one.</li>
</ul>
<h2 id="what-gets-used-in-the-real-world-timsort">What gets used in the real world: Timsort</h2>
<p>Everything above is a &ldquo;textbook algorithm&rdquo;. But every time you call <code>sorted()</code>, what Python actually runs underneath is <strong>Timsort</strong> — a hybrid carefully tuned for real-world data. It&rsquo;s worth a section of its own, because being able to bring it up in an interview is often a plus.</p>
<p><strong>Core idea</strong>: real data is rarely fully random — it&rsquo;s often <em>partially sorted already</em>. Timsort seizes on this:</p>
<ol>
<li>First scan the array for naturally-sorted contiguous segments, called <strong>runs</strong>;</li>
<li>Pad runs that are too short up to a minimum length (<code>minrun</code>, usually 32–64) using <strong>insertion sort</strong> — as noted earlier, insertion sort is fastest on small arrays;</li>
<li>Then <strong>merge</strong> these runs pairwise following a set of rules, with a &ldquo;galloping&rdquo; mode to speed up the merges.</li>
</ol>
<p>So Timsort = <strong>the skeleton of merge sort + the small-chunk optimization of insertion sort + special-casing for already-sorted data</strong>.</p>
<ul>
<li><strong>Complexity</strong>: worst O(n log n), but down to O(n) on nearly-sorted data. Space O(n).</li>
<li><strong>Stability</strong>: stable. This is why Python&rsquo;s <code>sorted()</code> and <code>list.sort()</code> are guaranteed stable.</li>
<li><strong>Trivia</strong>: Java&rsquo;s object sort (<code>Arrays.sort(Object[])</code>) also uses a Timsort variant; while C++&rsquo;s <code>std::sort</code> uses a different hybrid, <strong>Introsort</strong> (quicksort as the base, switching to heap sort when recursion gets too deep to avoid degrading, and insertion sort on small chunks). &ldquo;Quick + heap + insertion&rdquo; rolled into one — the same spirit as Timsort: <strong>there&rsquo;s no silver bullet; production-grade sorts are all hybrids</strong>.</li>
<li><strong>LeetCode</strong>: <a href="https://leetcode.com/problems/merge-intervals/">56. Merge Intervals</a> — sort first, then sweep and merge; in Python that <code>sorted()</code> call is running Timsort, so it&rsquo;s a good way to feel the speedup from &ldquo;real data is partially sorted&rdquo;.</li>
</ul>
<h2 id="how-to-actually-choose--how-to-answer-in-interviews">How to actually choose / how to answer in interviews</h2>
<p>Here&rsquo;s everything above boiled down to a &ldquo;which one should I use&rdquo; checklist:</p>
<ul>
<li><strong>No special requirements, just want speed</strong> → quicksort (random pivot). The default for most situations.</li>
<li><strong>Need stable, and the worst case must stay O(n log n)</strong> → merge sort.</li>
<li><strong>Memory is extremely tight (need O(1) space) and can&rsquo;t degrade</strong> → heap sort.</li>
<li><strong>Very small data (a few dozen) or nearly sorted</strong> → insertion sort.</li>
<li><strong>Sorting a linked list</strong> → merge sort.</li>
<li><strong>Integers with a small value range</strong> → counting sort.</li>
<li><strong>Integers but a huge range (e.g. fixed-length integers / strings)</strong> → radix sort.</li>
<li><strong>Data uniformly distributed over an interval</strong> → bucket sort.</li>
<li><strong>Only need the k-th largest / the median, not a full sort</strong> → quickselect.</li>
<li><strong>Data too big to fit in memory</strong> → external merge sort.</li>
</ul>
<p>A few common chained follow-ups — have the answers ready:</p>
<ul>
<li><strong>&ldquo;Which sorts are stable?&rdquo;</strong> → bubble, insertion, merge, counting, radix, bucket (when the per-bucket sort is stable), Timsort.</li>
<li><strong>&ldquo;Quicksort&rsquo;s worst case and how to avoid it?&rdquo;</strong> → sorted input + a bad pivot degrades to O(n²); use a random pivot or median-of-three.</li>
<li><strong>&ldquo;Can you beat O(n log n)?&rdquo;</strong> → comparison sorts can&rsquo;t (the decision-tree lower bound); but if the data is integers in a bounded range, non-comparison sorts get you to O(n).</li>
<li><strong>&ldquo;Is there a sort that&rsquo;s O(n log n), stable, <em>and</em> in-place?&rdquo;</strong> → not in typical implementations; merge is stable but not in-place, heap is in-place but not stable, quick is in-place but not stable. A great prompt for testing whether you understand the trade-offs.</li>
</ul>
<h2 id="a-few-common-mistakes">A few common mistakes</h2>
<p>Pitfalls I stepped in (or nearly did) while reviewing:</p>
<ul>
<li><strong>Selection sort doesn&rsquo;t speed up on sorted input</strong> — it has no early-exit, so it&rsquo;s always O(n²). Don&rsquo;t confuse it with bubble / insertion.</li>
<li><strong>Building a heap is O(n), not O(n log n)</strong>. Intuitively it looks like n elements at O(log n) each, but a careful count (most nodes are near the bottom) gives O(n). A common counterintuitive question.</li>
<li><strong>Counting / radix sort fill the result back-to-front</strong> — that step is the source of stability; do it the other way and it&rsquo;s unstable, and an unstable radix sort is simply wrong.</li>
<li><strong>Quicksort&rsquo;s space isn&rsquo;t O(1)</strong> — it partitions in place, but the recursion stack is O(log n) on average and O(n) at worst.</li>
<li><strong>Bucket sort&rsquo;s worst case is O(n²)</strong> — don&rsquo;t only remember the O(n) average; it degrades when the distribution is skewed.</li>
<li><strong>Stable ≠ in-place</strong> — these are two independent dimensions, often asked together in interviews. Don&rsquo;t conflate them.</li>
</ul>
<h2 id="wrapping-up">Wrapping up</h2>
<p>The framework for this topic is actually pretty clear:</p>
<ul>
<li><strong>Comparison sorts</strong> are stuck at O(n log n); among them quicksort is fastest but degrades, merge is stable but space-hungry, heap is in-place but unstable — <strong>there&rsquo;s no all-rounder, it&rsquo;s all trade-offs</strong>;</li>
<li><strong>Non-comparison sorts</strong> trade requirements on the data for linear time, but they make demands on that data;</li>
<li><strong>Production-grade sorts (Timsort, Introsort) are all hybrids</strong>, stitching together the strengths of several algorithms.</li>
</ul>
<p>If, like me, you&rsquo;re picking this back up after a few years, I&rsquo;d suggest running through that cheat sheet at the top: skip anything you can implement from memory and explain the complexity and stability of, and go back to the relevant section for whatever you stumble on. Next time you&rsquo;re prepping for interviews, just come back and skim it again.</p>
<p>Good luck with your interviews.</p>
]]></content:encoded></item><item><title>After Seeing MBTI and SBTI Everywhere, I Built a Programmer Personality Test</title><link>https://neilmin.com/posts/building-a-programmer-personality-test/</link><pubDate>Tue, 28 Apr 2026 11:45:00 -0700</pubDate><guid>https://neilmin.com/posts/building-a-programmer-personality-test/</guid><description>A write-up on how I got inspired by the recent wave of personality-test projects and ended up building a programmer personality test that looks serious on the outside and is full of programmer stereotypes on the inside.</description><content:encoded><![CDATA[<p>A little while ago, I kept running into personality-test projects everywhere.</p>
<p>Some of them were the familiar polished MBTI-style sites, especially things like <a href="https://www.16personalities.com/">16Personalities</a>, where the whole experience feels surprisingly complete and serious. Others were much more online and much more unserious in tone, like <a href="https://sbti.unun.dev/">SBTI</a>, which feels almost designed to be screenshotted, forwarded, and argued about. On top of that, I also saw a few more niche variants floating around, including programmer-themed ones and projects like <a href="https://github.com/liyupi/cbti-test">cbti-test</a>, which showed how far you could push a pure frontend quiz product.</p>
<p>After seeing a few of those in a row, I had a very simple thought:</p>
<p><strong>What if I built one for programmers too?</strong></p>
<p>And not just a quick joke page, but something a little more committed: a site that looks like a serious personality assessment on the outside, while the actual questions and results are clearly about the very specific ways programmers behave at work.</p>
<h2 id="what-interested-me-was-the-contrast">What interested me was the contrast</h2>
<p>If you just make a “programmer MBTI,” it is very easy for it to turn into a disposable meme page. You laugh, send it to a friend, and forget about it the next day.</p>
<p>I wanted something a little more specific than that.</p>
<p>The interesting part to me was the contrast:</p>
<ul>
<li>the interface should feel real</li>
<li>the interaction should feel real</li>
<li>the result page should feel real</li>
<li>but the content should quietly be about programmer habits, coping mechanisms, and work-brain damage</li>
</ul>
<p>Things like:</p>
<ul>
<li>do you actually write code, or do you mostly orchestrate AI into writing it for you?</li>
<li>are you the kind of person who hears “small internal tool” and immediately starts talking about architecture?</li>
<li>when something breaks, do you read logs and trace the source, or do you add five <code>console.log</code>s and hope the restart fixes it?</li>
<li>are you in this because you love tech, or because you love not getting PIP’d before vesting?</li>
</ul>
<p>Those are funny questions, but they are also concrete enough that they feel like actual behavior rather than vague personality labels.</p>
<h2 id="the-hard-part-was-not-coding-it">The hard part was not coding it</h2>
<p>This project started out pretty fuzzy.</p>
<p>At first it was just a vibe:<br>
I wanted to make a programmer personality test, ideally one that felt closer to the North American Chinese tech-worker context than to generic internet programmer humor.</p>
<p>But once you actually start building something like this, you quickly realize there are a lot of decisions hiding underneath that initial idea.</p>
<p>For example:</p>
<ul>
<li>Should it start in Chinese only, or be bilingual from the beginning?</li>
<li>Should it reuse traditional MBTI axes at all?</li>
<li>Should the result page feel like a formal report or more like a shareable poster?</li>
<li>Should the frontend use a router?</li>
<li>Should deployment live under a path inside my main blog or on its own subdomain?</li>
<li>Should the English version be a translation, or should it be rewritten naturally?</li>
</ul>
<p>None of those questions looks huge on its own, but together they decide whether the final thing feels like a toy or a real product.</p>
<p>That was probably my biggest takeaway from this build:</p>
<p><strong>the part that takes time is not implementation, it is forcing all the product decisions to become explicit.</strong></p>
<p>Sometimes that meant arguing over code. Sometimes it meant arguing over one line of copy, how aggressive a result title should sound, or whether the quiz should feel more like a real assessment or more like an internet joke.</p>
<p>Those choices sound soft, but they end up shaping the entire product.</p>
<h2 id="i-ended-up-making-a-custom-four-axis-framework-called-ship">I ended up making a custom four-axis framework called SHIP</h2>
<p>At some point I stopped trying to map everything back to classic MBTI and just made a custom framework for the app.</p>
<p>I called it <code>SHIP</code>.</p>
<p>Partly because the acronym reads well, and partly because it fits the culture: you can write code, talk architecture, and debate abstractions all day, but eventually the whole job is still about shipping.</p>
<p>The four dimensions ended up being:</p>
<h3 id="1-source-where-does-your-code-actually-come-from">1. Source: where does your code actually come from?</h3>
<ul>
<li><code>C = Copilot</code></li>
<li><code>T = Typecraft</code></li>
</ul>
<p>In other words:</p>
<ul>
<li>are you the kind of person who sees repetitive work and immediately lets Gemini, Claude, or Copilot generate the skeleton?</li>
<li>or are you still the kind of person who wants to hand-write the core logic even if AI already produced something usable?</li>
</ul>
<h3 id="2-hierarchy-how-addicted-are-you-to-architecture">2. Hierarchy: how addicted are you to architecture?</h3>
<ul>
<li><code>O = Overdesign</code></li>
<li><code>A = ASAP</code></li>
</ul>
<p>Some programmers see a basic CRUD feature and immediately start thinking about:</p>
<ul>
<li>decoupling</li>
<li>scalability</li>
<li>HA</li>
<li>backward compatibility</li>
<li>reusable platform-level services</li>
</ul>
<p>Other programmers are only thinking one thought:</p>
<p><strong>Can this get pushed to prod tonight?</strong></p>
<h3 id="3-investigation-is-your-debugging-style-logical-or-mystical">3. Investigation: is your debugging style logical or mystical?</h3>
<ul>
<li><code>L = Logic</code></li>
<li><code>P = Pray</code></li>
</ul>
<p>Some people see a production issue and go straight to logs, traces, and source code. Others do some combination of:</p>
<ul>
<li>add a few prints</li>
<li>restart first</li>
<li>write a defensive fallback script</li>
<li>if the service is back up, root cause can wait</li>
</ul>
<h3 id="4-purpose-what-is-actually-driving-you">4. Purpose: what is actually driving you?</h3>
<ul>
<li><code>G = Geek</code></li>
<li><code>W = Worker</code></li>
</ul>
<p>Some people really will spend a weekend building a side project, trying a new framework, or reading technical docs for fun.</p>
<p>Others are much more directly driven by:</p>
<ul>
<li>perf review</li>
<li>promo</li>
<li>PIP</li>
<li>H1B / PERM pressure</li>
<li>layoff anxiety</li>
<li>getting out of work on time</li>
</ul>
<p>The more I worked on it, the more I liked these axes. They are obviously satirical, but they are still grounded enough to feel recognizable.</p>
<h2 id="i-kept-the-scoring-algorithm-deliberately-simple">I kept the scoring algorithm deliberately simple</h2>
<p>On the implementation side, I had a very strong bias here:</p>
<p><strong>I did not want fake complexity.</strong></p>
<p>It would have been easy to dress the project up with some more mysterious matching system, nearest-neighbor logic, or personality vectors that sound more “scientific.” But for this product, I honestly thought that would make it worse.</p>
<p>The structure is already simple:</p>
<ul>
<li>4 dimensions</li>
<li>2 poles per dimension</li>
<li>7-point Likert answers</li>
</ul>
<p>So the scoring model stayed simple too:</p>
<ul>
<li>each question belongs to one dimension</li>
<li>each Likert choice maps to a score from <code>+3</code> to <code>-3</code></li>
<li>each dimension accumulates its own score</li>
<li>the sign of the score determines the winning pole</li>
<li>the four winning poles become a result code like <code>CAPW</code></li>
</ul>
<p>That approach had a few advantages:</p>
<ul>
<li>it is easy to explain</li>
<li>it is easy to test</li>
<li>it fits the percentage-bar UI naturally</li>
<li>it let me add bilingual support later without touching the scoring engine</li>
</ul>
<p>I ended up documenting the scoring logic in the project README in detail for exactly that reason. This kind of project is funny on the surface, but if the internals become harder to reason about than they need to be, it stops being fun to maintain very quickly.</p>
<h2 id="the-technical-architecture-stayed-intentionally-restrained">The technical architecture stayed intentionally restrained</h2>
<p>Even though the site looks like a complete product now, the actual architecture is pretty restrained.</p>
<p>I did not put it inside my blog repo. I split it out into its own repository and gave it its own subdomain:</p>
<ul>
<li>independent repo</li>
<li>Vite + React</li>
<li>static deployment</li>
<li>GitHub Pages</li>
<li>custom subdomain: <code>mbti.neilmin.com</code></li>
</ul>
<p>And I intentionally avoided a few things that would have made it look fancier without actually helping this project:</p>
<ul>
<li>no React Router</li>
<li>no full i18n framework</li>
<li>no backend</li>
<li>no database</li>
</ul>
<h3 id="why-no-router">Why no router?</h3>
<p>Because this app is still fundamentally one flow:</p>
<ul>
<li>intro</li>
<li>questions</li>
<li>result</li>
</ul>
<p>If I had introduced a full routing model just to make it look like a bigger SPA, I would have bought myself a bunch of GitHub Pages edge cases for very little benefit.</p>
<p>So the app just uses React state for screen transitions, and the only meaningful URL state it preserves is:</p>
<ul>
<li><code>?result=CODE</code></li>
</ul>
<p>That way shared result links still work, but the deployment model stays trivial.</p>
<h3 id="why-no-i18n-framework">Why no i18n framework?</h3>
<p>Later on I added English too.</p>
<p>But I still did not bring in a heavy i18n system, because this project is extremely copy-heavy and I did not want the English version to feel like a translation layer pasted over Chinese source text.</p>
<p>So the structure became:</p>
<ul>
<li>one lightweight locale state</li>
<li>one shared scoring engine</li>
<li>one shared result-code system</li>
<li>two separate content layers:
<ul>
<li>Chinese questions</li>
<li>English questions</li>
<li>Chinese personality writeups</li>
<li>English personality writeups</li>
</ul>
</li>
</ul>
<p>That let the English version sound like natural English instead of translated Chinese.</p>
<h2 id="the-most-addictive-part-turned-out-to-be-the-character-art-and-the-share-poster">The most addictive part turned out to be the character art and the share poster</h2>
<p>If I had stopped after the quiz flow and result page, the project would already have been “done enough.”</p>
<p>But personality-test sites are not really judged only by how they read in the browser. They are also judged by what gets screenshotted and forwarded.</p>
<p>That is the point where I got pulled into two extra rabbit holes.</p>
<h3 id="1-making-character-art-for-all-16-personas">1. Making character art for all 16 personas</h3>
<p>I wanted something in the general visual family of 16Personalities: clean, low-poly, geometric, readable.</p>
<p>But I still wanted the characters to feel like they belonged to this project’s own world.</p>
<p>At first I thought about generating them individually, but that would have made consistency much harder. So I went with a more scalable workflow:</p>
<ul>
<li>generate full character sheets with AI</li>
<li>cut them into separate assets with a script</li>
<li>reuse those cutouts across the homepage, result page, and poster</li>
</ul>
<p>It felt very modern in a funny way: let the model handle the creative batch generation, then let scripts do the tedious mechanical cleanup.</p>
<h3 id="2-building-a-dedicated-vertical-share-poster">2. Building a dedicated vertical share poster</h3>
<p>The result page is for reading.</p>
<p>The poster is for sending around.</p>
<p>So instead of expecting people to screenshot the result page directly, I made a dedicated mobile-first vertical poster. It includes:</p>
<ul>
<li>the result code</li>
<li>the title</li>
<li>the quote</li>
<li>dimension summaries</li>
<li>the longer description</li>
<li>the lifestyle/social profile</li>
<li>a QR code</li>
<li>the site URL</li>
</ul>
<p>And importantly, the QR code does <strong>not</strong> deep-link back to the sharer’s result. It goes to the homepage of the test itself.</p>
<p>That distinction mattered to me:</p>
<p><strong>the shared result is the content, but the QR code is the acquisition path.</strong></p>
<h2 id="in-the-end-this-was-really-just-something-i-found-interesting-enough-to-make">In the end, this was really just something I found interesting enough to make</h2>
<p>Looking back, this project does not feel like some grand product experiment to me.</p>
<p>It feels more like one of those things that started with: “this would be kind of fun,” and then I actually committed to making it real.</p>
<p>I happened to run into a few personality-test projects in a row, thought the “serious shell, unserious content” contrast was funny, and realized programmers have more than enough specific habits and stereotypes to support that kind of format. So I built one.</p>
<p>The current version already does the things I wanted it to do:</p>
<ul>
<li>it feels reasonably polished</li>
<li>the quiz itself reads well</li>
<li>the result page works</li>
<li>the share poster works</li>
</ul>
<p>But I definitely do not think it is “finished” in some permanent sense. The question wording, the persona copy, the English details, the visuals, and the interactions all still have room to improve.</p>
<p>If you want to try it, it’s live here:</p>
<p><a href="https://mbti.neilmin.com">https://mbti.neilmin.com</a></p>
<p>And if you finish it and think, “this is way too accurate,” then that probably means the joke landed at least halfway.</p>
]]></content:encoded></item><item><title>Some Personal Websites I Love</title><link>https://neilmin.com/posts/favorite-personal-websites/</link><pubDate>Tue, 14 Apr 2026 03:30:00 -0700</pubDate><guid>https://neilmin.com/posts/favorite-personal-websites/</guid><description>A small collection of personal websites I really like. Some are restrained, some are bold, but all of them have a strong sense of personality.</description><content:encoded><![CDATA[<p>I&rsquo;ve looked through quite a few personal websites lately, and the more I look, the more interesting this whole category feels.</p>
<p>They are not all chasing the same style. Some are very restrained, some go really far with interaction, but the common thread is that they all feel unmistakably personal. I wanted to save a few of my favorites here, partly as inspiration for my own site later on.</p>
<a class="favorite-site-card" href="https://gkoberger.com" target="_blank" rel="noopener noreferrer">
  <figure class="favorite-site-card__media">
    <img class="favorite-site-card__image" src="https://neilmin.com/images/favorite-personal-websites/gkoberger-homepage.png" alt="Homepage screenshot of gkoberger.com" loading="lazy">
  </figure>
  <div class="favorite-site-card__body">
    <h3 class="favorite-site-card__title">gkoberger.com</h3>
    <div class="favorite-site-card__description">What makes this one so fun is that the author built a virtual version of himself sitting at a desk. Then you can click almost everything in the scene, whether it&rsquo;s the person, the objects on the desk, or even the board behind him, and each one takes you to a different page. The first time I saw it, I immediately thought: wow, so this is another way a personal website can work.</div>
  </div>
</a>

<a class="favorite-site-card" href="https://www.alanagoyal.com/finder" target="_blank" rel="noopener noreferrer">
  <figure class="favorite-site-card__media">
    <img class="favorite-site-card__image" src="https://neilmin.com/images/favorite-personal-websites/alanagoyal-finder-homepage.png" alt="Screenshot of alanagoyal.com/finder" loading="lazy">
  </figure>
  <div class="favorite-site-card__body">
    <h3 class="favorite-site-card__title">alanagoyal.com/finder</h3>
    <div class="favorite-site-card__description">One of the coolest things about this site is that it turns almost the entire interface into something that feels like macOS. And it&rsquo;s not just for show. A lot of it is actually interactive. The whole experience feels light, playful, and very hard not to click through.</div>
  </div>
</a>

<a class="favorite-site-card" href="https://www.sharyap.com" target="_blank" rel="noopener noreferrer">
  <figure class="favorite-site-card__media">
    <img class="favorite-site-card__image" src="https://neilmin.com/images/favorite-personal-websites/sharyap-homepage.png" alt="Homepage screenshot of sharyap.com" loading="lazy">
  </figure>
  <div class="favorite-site-card__body">
    <h3 class="favorite-site-card__title">sharyap.com</h3>
    <div class="favorite-site-card__description">This one is a bit similar to the previous site. Every time you open a new page, it feels like opening a new window on a desktop. What makes it even more charming is that many of the graphics, logos, and little visual details were drawn by the author herself, which makes the whole thing feel cohesive and incredibly cute.</div>
  </div>
</a>

<a class="favorite-site-card" href="https://merodev.net" target="_blank" rel="noopener noreferrer">
  <figure class="favorite-site-card__media">
    <img class="favorite-site-card__image" src="https://neilmin.com/images/favorite-personal-websites/merodev-homepage.png" alt="Homepage screenshot of merodev.net" loading="lazy">
  </figure>
  <div class="favorite-site-card__body">
    <h3 class="favorite-site-card__title">merodev.net</h3>
    <div class="favorite-site-card__description">This is a very cool, very futuristic 3D website. The rendering quality grabs you right away. The lighting, the scene, and the overall atmosphere all feel polished in a way that makes you stop and stare for a few seconds.</div>
  </div>
</a>

<a class="favorite-site-card" href="https://bruno-simon.com" target="_blank" rel="noopener noreferrer">
  <figure class="favorite-site-card__media">
    <img class="favorite-site-card__image" src="https://neilmin.com/images/favorite-personal-websites/bruno-simon-homepage.png" alt="Homepage screenshot of bruno-simon.com" loading="lazy">
  </figure>
  <div class="favorite-site-card__body">
    <h3 class="favorite-site-card__title">bruno-simon.com</h3>
    <div class="favorite-site-card__description">This site is famous, and I first saw it years ago. Looking at it again now, it has evolved even more. The most ridiculous and unforgettable thing about it is that Bruno somehow turned his personal website into a 3D driving game. You drive around while exploring his work and personal info. Even now, I still wonder how he managed to fit all of that into one website. He also has a YouTube channel where he talks about how he built it.</div>
  </div>
</a>

<a class="favorite-site-card" href="https://logartis.info" target="_blank" rel="noopener noreferrer">
  <figure class="favorite-site-card__media">
    <img class="favorite-site-card__image" src="https://neilmin.com/images/favorite-personal-websites/logartis-homepage.png" alt="Homepage screenshot of logartis.info" loading="lazy">
  </figure>
  <div class="favorite-site-card__body">
    <h3 class="favorite-site-card__title">logartis.info</h3>
    <div class="favorite-site-card__description">This site feels a bit like watching a movie. You move through a forest-like environment, and the whole atmosphere feels incredibly complete. What I especially like is how well the weather and environmental transitions are handled. It creates a really strong sense of immersion.</div>
  </div>
</a>

<a class="favorite-site-card" href="https://wodniack.dev" target="_blank" rel="noopener noreferrer">
  <figure class="favorite-site-card__media">
    <img class="favorite-site-card__image" src="https://neilmin.com/images/favorite-personal-websites/wodniack-homepage.png" alt="Homepage screenshot of wodniack.dev" loading="lazy">
  </figure>
  <div class="favorite-site-card__body">
    <h3 class="favorite-site-card__title">wodniack.dev</h3>
    <div class="favorite-site-card__description">This is another site with a very strong personal style. A lot of the interactions and scroll-triggered effects feel unique and artistic. You can tell the author is not just using a template, but actually using the website as a way to express a distinct visual point of view.</div>
  </div>
</a>

<a class="favorite-site-card" href="https://getcoleman.com" target="_blank" rel="noopener noreferrer">
  <figure class="favorite-site-card__media">
    <img class="favorite-site-card__image" src="https://neilmin.com/images/favorite-personal-websites/getcoleman-homepage.png" alt="Homepage screenshot of getcoleman.com" loading="lazy">
  </figure>
  <div class="favorite-site-card__body">
    <h3 class="favorite-site-card__title">getcoleman.com</h3>
    <div class="favorite-site-card__description">The thing that really made me stop here is how creatively the author introduces himself. He uses progress bars to show his timeline and different stages of his life. It instantly makes you think: oh, so this is another way to do a personal introduction.</div>
  </div>
</a>

<a class="favorite-site-card" href="https://animejs.com" target="_blank" rel="noopener noreferrer">
  <figure class="favorite-site-card__media">
    <img class="favorite-site-card__image" src="https://neilmin.com/images/favorite-personal-websites/animejs-homepage.png" alt="Homepage screenshot of animejs.com" loading="lazy">
  </figure>
  <div class="favorite-site-card__body">
    <h3 class="favorite-site-card__title">animejs.com</h3>
    <div class="favorite-site-card__description">This isn&rsquo;t a personal website, but a product showcase site. Still, the animation work here is incredibly smooth. The transitions between sections while scrolling, the pacing, and the motion all feel very well tuned. It&rsquo;s one of those sites where you immediately think: this is just really well made.</div>
  </div>
</a>

<a class="favorite-site-card" href="https://www.joshwcomeau.com" target="_blank" rel="noopener noreferrer">
  <figure class="favorite-site-card__media">
    <img class="favorite-site-card__image" src="https://neilmin.com/images/favorite-personal-websites/joshwcomeau-homepage.png" alt="Homepage screenshot of joshwcomeau.com" loading="lazy">
  </figure>
  <div class="favorite-site-card__body">
    <h3 class="favorite-site-card__title">joshwcomeau.com</h3>
    <div class="favorite-site-card__description">This site feels great from top to bottom. The typography, the colors, and even the little interactive game on the homepage all make you feel that the author&rsquo;s taste is excellent, not in a flashy way, but in a way that keeps feeling better the longer you look at it. On top of that, he writes a lot of technical articles, so it&rsquo;s not just beautiful, it&rsquo;s genuinely useful too.</div>
  </div>
</a>

<a class="favorite-site-card" href="https://portfolio.ohevan.com" target="_blank" rel="noopener noreferrer">
  <figure class="favorite-site-card__media">
    <img class="favorite-site-card__image" src="https://neilmin.com/images/favorite-personal-websites/ohevan-portfolio-homepage.png" alt="Homepage screenshot of portfolio.ohevan.com" loading="lazy">
  </figure>
  <div class="favorite-site-card__body">
    <h3 class="favorite-site-card__title">portfolio.ohevan.com</h3>
    <div class="favorite-site-card__description">This site feels more like a personal showcase of someone&rsquo;s portfolio, the places they&rsquo;ve been, and the photos they&rsquo;ve taken, and those photos genuinely look cinematic. I really like this direction myself. If I ever end up with enough photos that feel worth sharing, I&rsquo;d love to seriously think about making something like this too.</div>
  </div>
</a>

<p>What I love most about these sites probably comes down to two things: either the interactions are exceptionally good, or the visual design is strong enough to make you stop and look a little longer. But more importantly, they all reminded me just how far a website can be pushed. So many things that seem impossible, or that you would never expect someone to do on a website, can actually be done. For me, this whole list was genuinely eye-opening, which is exactly why I wanted to share it.</p>
]]></content:encoded></item><item><title>How I Vibe-Coded This Blog Website Just to Publish One Post</title><link>https://neilmin.com/posts/building-my-hugo-blog-with-github-pages/</link><pubDate>Mon, 06 Apr 2026 13:30:00 -0700</pubDate><guid>https://neilmin.com/posts/building-my-hugo-blog-with-github-pages/</guid><description>How I built a bilingual Hugo blog on GitHub Pages with PaperMod, Vercount analytics, Giscus comments, a custom domain, and a few lessons from debugging a frustrating 404 deployment issue.</description><content:encoded><![CDATA[<p>This whole thing, honestly, was basically the software version of buying a dish of vinegar and ending up making an entire batch of dumplings.</p>
<p>A while ago, I ran into a bug that was both obscure and genuinely hard to fix. Stack Overflow was not giving me anything useful. Google was not giving me much either. In the end I had to grind through it myself and solve it the hard way.</p>
<p>Once I finally got it resolved, my first reaction was that I should write the whole winding story down.</p>
<p>Because if I do not write down problems like that, there is a very good chance that a few months later even I will no longer remember how I reasoned my way through them. And after burning that many brain cells on one issue, not turning it into something useful would feel like a waste.</p>
<p>But then the obvious problem showed up:</p>
<p>I had written the post. But where exactly was I supposed to publish it?</p>
<h2 id="the-real-starting-point-was-not-i-want-a-personal-website">The real starting point was not &ldquo;I want a personal website&rdquo;</h2>
<p>I did not begin with some grand plan to build a personal website.</p>
<p>My actual thought process was much simpler: I just wanted a decent place to publish that debugging write-up.</p>
<p>So I spent some time researching where it should live.</p>
<p>On the Chinese internet, platforms like CSDN, Zhihu, and Blog Garden were all options. It was not like I had nowhere to post. But they all felt too constrained, and some of those platforms have a reputation for pulling stunts that make your content feel like it does not fully belong to you anymore. A clean technical post gets wrapped in platform noise, and that was not what I wanted.</p>
<p>Platforms like Medium or Blogger feel lighter, sure, but at the end of the day you are still renting space in someone else&rsquo;s house. You can decorate it a little, but the walls are not yours, the address is not yours, and if the platform changes its rules, some part of your setup stops being fully under your control.</p>
<p>I was not doing this to monetize anything either. As an engineer, I ended up at the most predictable conclusion possible:</p>
<p>If I wanted full control, I should just build the thing myself.</p>
<p>And once that idea showed up, the rest of the stack choice became pretty straightforward. Static hosting, free infrastructure, a workflow that fits naturally with Git, and no need to think too hard about servers. It is hard to imagine something more like a proper little digital home than <a href="https://pages.github.com/">GitHub Pages</a>.</p>
<h2 id="if-i-was-going-to-build-it-i-might-as-well-build-it-properly">If I was going to build it, I might as well build it properly</h2>
<p>When it came to choosing the framework, I looked at the usual suspects first.</p>
<ul>
<li><a href="https://hexo.io/">Hexo</a> on the Node.js side</li>
<li><a href="https://jekyllrb.com/">Jekyll</a>, the old GitHub Pages classic in the Ruby world</li>
<li><a href="https://gohugo.io/">Hugo</a>, which has a reputation for being absurdly fast</li>
</ul>
<p>After comparing them a bit, I ended up choosing Hugo.</p>
<p>The reason was actually very simple: I had seen another site built with Hugo, liked how it looked overall, and for a site this small, the technical differences between these frameworks are not life-or-death anyway.</p>
<p>For the theme, I went with <a href="https://github.com/adityatelange/hugo-PaperMod">PaperMod</a>.</p>
<p>I like how restrained it is. The default look is clean, not noisy, and it keeps the content front and center. For someone like me, who mostly wants to write technical posts and keep a lightweight personal site around them, it was a really solid starting point.</p>
<p>If you want to build something similar, the basic setup is honestly pretty small:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 1. Install Hugo (macOS)</span>
</span></span><span style="display:flex;"><span>brew install hugo
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2. Create the site</span>
</span></span><span style="display:flex;"><span>hugo new site my-blog
</span></span><span style="display:flex;"><span>cd my-blog
</span></span><span style="display:flex;"><span>git init
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 3. Add PaperMod as a submodule</span>
</span></span><span style="display:flex;"><span>git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
</span></span></code></pre></div><p>There is one small detail here that is worth calling out very explicitly: <strong>PaperMod really should be added as a submodule.</strong></p>
<p>Do not just <code>git clone</code> it into <code>themes/</code> and assume you are done. That can look perfectly fine locally and still bite you later when you push to GitHub. Nested repositories and theme directories do not always behave the way people expect, and it is a very easy way to end up with a mysteriously broken site, missing theme files, or a pretty embarrassing 404 situation after deployment. I am, unfortunately, speaking from lived experience here.</p>
<h2 id="the-funniest-part-is-that-vibe-coding-actually-worked-really-well">The funniest part is that Vibe Coding actually worked really well</h2>
<p>If this had happened a few years ago, the project would still have been manageable, but it absolutely would have been the kind of thing that quietly consumed an entire weekend.</p>
<p>You would have to set up the theme, wire the Hugo config, decide how bilingual routing should work, figure out deployment, get GitHub Actions going, deal with the custom domain, tweak the styling, wire comments, wire analytics, and then spend way too much time on front-end details that individually look tiny but collectively refuse to go away.</p>
<p>Now we live in the era of <strong>Vibe Coding</strong>.</p>
<p>What really stood out to me this time is that AI was not most useful because it magically knew what I wanted. It was useful because once I knew the direction, it could help me chew through a huge amount of tedious, fragmented, annoying implementation work.</p>
<p>This site did not come together because an AI produced one perfect answer. It came together because I kept refining the target and using AI to try things with me:</p>
<ul>
<li>how to structure bilingual routes</li>
<li>how to pair English and Chinese content files</li>
<li>how to make the language switcher prefer the current page&rsquo;s translation instead of always dumping you back on the homepage</li>
<li>how fast the animated glow background should move</li>
<li>how to make it visible enough in light mode</li>
<li>what to use for page views after the first counter turned out to be flaky</li>
<li>how to wire comments so the UI language follows the page language</li>
</ul>
<p>And a lot of those details were not &ldquo;one and done&rdquo; decisions. We went back and forth on them many times.</p>
<p>The bilingual content convention, for example, ended up being very simple:</p>
<ul>
<li>English lives at <code>/</code></li>
<li>Chinese lives at <code>/zh/</code></li>
<li>default English content files stay unsuffixed</li>
<li>Chinese translations use <code>.zh.md</code></li>
</ul>
<p>So it looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>content/posts/my-post.md
</span></span><span style="display:flex;"><span>content/posts/my-post.zh.md
</span></span></code></pre></div><p>Even with vibe coding making things much easier, none of this really came out perfect in one shot. A lot of the site was not &ldquo;AI gave me the final answer.&rdquo; It was much closer to me standing next to a very fast collaborator and repeatedly saying:</p>
<p>This is not right, move it a bit left.</p>
<p>That animation is too fast, slow it down.</p>
<p>That color is ugly, try another one.</p>
<p>This interaction feels like magnets. I want it to feel like something soft being gently pushed aside.</p>
<p>That is why I have grown to like this workflow so much. I am not personally typing every line, but I am still doing the design work, the judgment, and the steering. AI feels less like a replacement and more like a teammate with a lot of execution stamina who does not get tired of being told to tweak things one more time.</p>
<h2 id="once-i-started-i-could-not-help-making-it-more-complete">Once I started, I could not help making it more complete</h2>
<p>Originally I only wanted a place to publish one technical post.</p>
<p>But once the site was half-built, the dangerous sentence showed up in my head:</p>
<p>Well, since I am already here, I might as well finish it properly.</p>
<p>And that was how things escalated.</p>
<h3 id="1-bilingual-support">1. Bilingual support</h3>
<p>I already knew I wanted the site to support both Chinese and English, so from the beginning this was not designed as a single-language blog.</p>
<p>The current structure is:</p>
<ul>
<li>English at <code>/</code></li>
<li>Chinese at <code>/zh/</code></li>
<li>posts, search, and resume pages all follow the bilingual model</li>
</ul>
<p>On paper, that sounds like just a few Hugo config settings. In reality, the annoying part is all the edges: should menus be localized, should search be language-specific, should the language switcher jump to the homepage or the translated page, should the comment widget switch its UI language too? None of those decisions is huge on its own, but together they determine whether the site feels properly bilingual or merely &ldquo;technically available in two languages.&rdquo;</p>
<h3 id="2-github-actions-deployment">2. GitHub Actions deployment</h3>
<p>For deployment, I kept it simple and just let GitHub Actions handle the job.</p>
<p>The Pages workflow is already in <code>.github/workflows/gh-pages.yml</code>, and the day-to-day workflow is basically:</p>
<ol>
<li>Change content or styling locally</li>
<li><code>git add</code></li>
<li><code>git commit</code></li>
<li><code>git push</code></li>
</ol>
<p>Then CI/CD takes care of the rest.</p>
<p>This has one dangerous side effect: it lowers the cost of polishing things so much that you start fixing everything. Sometimes even one sentence that feels slightly off is enough to trigger a whole new commit.</p>
<p>And honestly, Hugo is very pleasant in this setup. Build with <code>hugo --gc --minify</code>, let GitHub Pages host the output, let GitHub Actions publish it, and you are done. There is almost no extra ops drama in the middle, which is exactly the level of complexity I wanted for a content-first site.</p>
<h3 id="3-buying-the-domain">3. Buying the domain</h3>
<p>Using <code>neilmin.github.io</code> would have been perfectly fine.</p>
<p>But then I made the mistake of checking domain prices and discovered that a <code>.com</code> with my name on it cost something like ten or twelve dollars a year. That is a very effective way to destroy a person&rsquo;s self-control. For less than the cost of a meal, I could have a little corner of the internet with my own name on it. Hard to argue with that.</p>
<p>So I bought <code>neilmin.com</code>.</p>
<p>Functionally, a custom domain does not change much. Emotionally, it changes a lot. The moment you type that URL into the browser, the site immediately feels more real and more yours.</p>
<h2 id="after-the-basics-worked-the-little-extras-became-the-most-addictive-part">After the basics worked, the little extras became the most addictive part</h2>
<p>Once a site like this is functional, the next wave of fun is usually not &ldquo;can it work?&rdquo; but &ldquo;can I make it feel a little nicer?&rdquo;</p>
<p>That was the point where I started adding extra little toys.</p>
<h3 id="the-animated-glowing-background">The animated glowing background</h3>
<p>If you are reading this on a desktop, the homepage and search page should have a few softly moving glowing shapes in the background.</p>
<p>That effect is not an image or a video. It is just CSS plus a bit of JavaScript. It started as a mesh-gradient-like atmosphere thing, and then I kept iterating until it became much more interactive.</p>
<p>The funny part is how many rounds it took to get the feel right. The first version looked too mechanical. Another version felt weirdly magnetic and slippery. Eventually I rewrote the interaction to feel more like soft bubbles being gently pushed aside, using <code>lerp</code>-style easing and a smoother falloff curve. Now when you move the mouse through it, it should feel more like you are nudging something viscous than poking three glowing objects that want to fly away.</p>
<h3 id="page-views-from-busuanzi-to-vercount">Page views: from Busuanzi to Vercount</h3>
<p>At first I tried the usual Chinese blog counter, Busuanzi.</p>
<p>But these days its reliability feels&hellip; situational. So I eventually switched to <a href="https://www.vercount.one/">Vercount</a>.</p>
<p>What I really liked there was that it is almost a drop-in replacement for Busuanzi at the DOM level.</p>
<p>In practice that meant I barely had to touch the existing <code>busuanzi_*</code> IDs. I could swap the script source, keep the markup mostly intact, and move on. That is exactly the kind of migration I appreciate when something is already wired into templates and I do not want to rebuild the whole feature from scratch.</p>
<h3 id="giscus-comments">Giscus comments</h3>
<p>For comments, I ended up choosing <a href="https://giscus.app/">Giscus</a>.</p>
<p>I really like the model: no separate database, no extra backend to maintain, just GitHub Discussions underneath. For a small site that already treats GitHub as home base, that is a very natural fit.</p>
<p>And it matches the rest of the site nicely:</p>
<ul>
<li>no custom comment backend to operate</li>
<li>dark mode support out of the box</li>
<li>comment UI language can follow the page language</li>
</ul>
<p>So now the Chinese posts render a Chinese Giscus UI, and the English posts render an English one. Those details are small, but once they line up, the whole site feels much more coherent.</p>
<p>And honestly, this is exactly the kind of thing that makes projects like this addictive. You think you are just adding comments, and then a minute later you care about whether the comment UI language switches correctly and whether the theme tracks light/dark mode properly too.</p>
<h2 id="looking-back-this-really-was-a-full-on-dumpling-project">Looking back, this really was a full-on dumpling project</h2>
<p>At the beginning, I just wanted to publish one technical write-up.</p>
<p>By the end, I had:</p>
<ul>
<li>set up Hugo</li>
<li>wired in PaperMod</li>
<li>implemented bilingual support</li>
<li>added search</li>
<li>configured GitHub Pages deployment</li>
<li>bought a custom domain</li>
<li>added visitor stats</li>
<li>added comments</li>
<li>tuned the animated background</li>
<li>refined the interaction feel</li>
</ul>
<p>All that because I wanted somewhere to post one debugging story.</p>
<p>And honestly, I am glad it happened.</p>
<p>On one level it solved a very practical problem: I now have a place that is fully mine, where I can publish the kinds of things I actually want to write. On another level, it brought back a very familiar kind of engineering joy, the kind that says, &ldquo;Well, since I already got this far, I might as well make it a little better.&rdquo;</p>
<h2 id="final-thoughts">Final thoughts</h2>
<p>That original bug write-up is now out in the world.</p>
<p>And this site, in a way, is the side effect it dragged into existence.</p>
<p>If you happened to land on this post, feel free to scroll down and leave a comment. Log in with GitHub, say hi, and if you want, help me test whether this whole setup is actually as stable as I think it is.</p>
<p>Because for all the extra polish it has now, the origin story of this site is still extremely simple:</p>
<p>I just wanted a good place to publish one post that felt worth preserving.</p>
]]></content:encoded></item><item><title>Why PostgreSQL Kept Saying “No space left on device” with 20TB Still Free</title><link>https://neilmin.com/posts/linux-disk-bug-triage/</link><pubDate>Sun, 05 Apr 2026 12:28:16 -0700</pubDate><guid>https://neilmin.com/posts/linux-disk-bug-triage/</guid><description>A PostgreSQL backup failure that looked like a disk-capacity problem turned out to be an EXT4 directory indexing limit caused by millions of tiny Large Object files.</description><content:encoded><![CDATA[<p>A little while ago, we ran into a customer issue that turned out to be way more interesting than it looked at first glance. The customer was trying to generate a backup using <code>pg_dump</code>, and the job kept failing halfway through with this error:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>pg_dump: error: could not write to output file: No space left on device
</span></span></code></pre></div><p>When I first saw it, I honestly felt pretty relaxed. This kind of error looks like a standard problem. If the disk is full, you make the disk bigger and move on.</p>
<h2 id="the-bizarre-beginning-a-black-hole-that-wouldnt-fill">The Bizarre Beginning: A Black Hole That Wouldn&rsquo;t Fill</h2>
<h3 id="the-most-obvious-thing-to-try-add-more-disk">The most obvious thing to try: add more disk</h3>
<p>The customer&rsquo;s database was already a few terabytes in size, so we did the straightforward thing and resized the target disk to 10 TB. In a situation like this, the first thing you usually check is:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>df -h
</span></span></code></pre></div><p>At that point I was genuinely thinking, &ldquo;Alright, this should be an easy win.&rdquo; We reran <code>pg_dump</code>, and somehow it failed again.</p>
<p>At first we still did not think too much of it. Maybe 10 TB was somehow still not enough? So we kept going and expanded the disk again, this time to well over 20 TB, and ran the job one more time.</p>
<p>Same error. Same failure.</p>
<p>That was the moment it stopped making sense. <code>df -h</code> clearly showed plenty of free space, yet the system kept insisting: <code>No space left on device</code>.</p>
<p>At that point I was pretty sure this error was not saying what it appeared to be saying.</p>
<h2 id="following-the-clues-200-million-hidden-large-objects">Following the Clues: 200 Million Hidden Large Objects</h2>
<p>If this was not a simple hardware-capacity problem, then the next step was to look at the data itself. We started inspecting the contents of the database. At first, nothing jumped out: no badly bloated TOAST data, no unusually highly compressed data, nothing obviously suspicious.</p>
<p>Then we noticed something that was definitely unusual: this customer had an enormous number of Large Objects stored in the database.</p>
<h3 id="concept-break-what-are-postgresql-large-objects">Concept Break: What are PostgreSQL Large Objects?</h3>
<p>In PostgreSQL, a Large Object, or LO, is a mechanism for storing large chunks of data such as images, audio files, or documents. It exposes a file-like API with operations such as <code>open</code>, <code>read</code>, <code>write</code>, and <code>seek</code>.</p>
<p>When you run <code>pg_dump</code> using Directory Format (<code>-Fd</code>), it generates files for table schemas and data. It also generates a separate dump file for every Large Object in the database. Those files are typically named after the LO&rsquo;s OID, something like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>blob_12345.dat
</span></span></code></pre></div><p>Under normal circumstances, Large Objects are usually used to store data that is huge in size but not huge in count. This customer was doing almost the exact opposite: they had stored more than 200 million very small Large Objects.</p>
<p>And that was where things started to get interesting. <code>pg_dump</code> clearly was not optimized for the &ldquo;massive quantity of tiny LOs&rdquo; case. It just kept following its normal logic and created one file per object.</p>
<h2 id="local-reproduction-the-14-million-file-barrier">Local Reproduction: The 14 Million File Barrier</h2>
<p>Once we suspected the sheer number of LOs was the issue, we started reproducing the environment locally.</p>
<p>We were able to reproduce the same error surprisingly quickly, and the reproduction was very consistent. As we monitored the backup process, one pattern kept showing up: the failure always happened when the output directory reached roughly 14 million files.</p>
<p>My first instinct at that point was: maybe we are running out of inodes.</p>
<p>That would have been a very normal Linux explanation. Every file needs an inode, and with a huge number of tiny files, it is absolutely possible to run out of inodes long before you run out of disk blocks. So we checked:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>df -i
</span></span></code></pre></div><p>And that was not it either. We still had plenty of inodes left.</p>
<p>So then we went through the usual list of suspects: container limits, process limits such as <code>ulimit -a</code>, and other system-level resource constraints. We kept checking and checking, and nothing looked wrong.</p>
<p>The only thing that stayed stubbornly consistent was that 14-million-file threshold. Every time we hit it, the job died as if it had smashed into an invisible wall.</p>
<h2 id="the-truth-revealed-ext4-htree-and-hash-collisions">The Truth Revealed: EXT4 HTree and Hash Collisions</h2>
<p>With that &ldquo;14 million file barrier&rdquo; in mind, I dug through a lot of material online and also asked people who know Linux filesystems far better than I do. Eventually the root cause became clear: this was coming from EXT4&rsquo;s directory indexing behavior.</p>
<h3 id="deep-dive-ext4-htree-limitations-and-hash-collisions">Deep Dive: EXT4 HTree Limitations and Hash Collisions</h3>
<p>In Linux, a directory is fundamentally a file that stores filenames and pointers to their inodes. To make lookup efficient when a directory contains huge numbers of entries, EXT4 uses a hash-based tree index called HTree, which is conceptually similar to a B-Tree in databases.</p>
<p>The catch is that the traditional EXT4 HTree has a limited depth, usually two levels. If you keep dumping millions upon millions of files into one single directory, the hash space for those filenames starts getting crowded and severe hash collisions can occur. Once a hash bucket is full and the HTree has reached its depth limit and cannot split further, the filesystem refuses to create more files and returns <code>ENOSPC</code> back to the OS, which surfaces as <code>No space left on device</code>.</p>
<p>That explains perfectly why the crash always happened at around 14 million files. <code>pg_dump</code> generates files sequentially, and the filenames are derived from LO IDs, so the naming pattern is deterministic. In other words, every run walks into the exact same collision path and hits the exact same wall.</p>
<p>Honestly, that was the satisfying part of the whole investigation. Up until then it felt like wandering around in the dark, touching random things and getting nowhere. Once this clicked, all the weird symptoms suddenly lined up.</p>
<h2 id="the-fix-how-to-get-around-it">The Fix: How to Get Around It</h2>
<p>Once we understood the root cause, the solution space became much clearer. There are really three levels where you can address this.</p>
<h3 id="1-backup-strategy-split-the-dump">1. Backup strategy: split the dump</h3>
<p>If Directory Format is choking because it is trying to put every LO into one folder, then one practical fix is to separate table data from Large Objects:</p>
<ul>
<li>Dump regular table data using Directory Format (<code>-Fd</code>) while excluding blobs.</li>
<li>Dump Large Objects separately into a single large file, such as plain SQL or custom format, so you avoid generating hundreds of millions of tiny files.</li>
</ul>
<p>In practice, that means using options such as <code>--no-blobs</code> for the table dump and exporting blobs separately.</p>
<h3 id="2-filesystem-level-enable-large_dir">2. Filesystem level: enable <code>large_dir</code></h3>
<p>Modern EXT4 is actually aware of this edge case and provides a feature called <code>large_dir</code>. Once enabled, it supports a 3-level HTree and allows directories to exceed 2 GB in size. That greatly reduces the probability of hash collisions and, in practice, almost eliminates this per-directory bottleneck.</p>
<p>You can enable it like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Unmount the disk first</span>
</span></span><span style="display:flex;"><span>umount /dev/sdX
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Enable the large_dir feature</span>
</span></span><span style="display:flex;"><span>tune2fs -O large_dir /dev/sdX
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check the filesystem and remount</span>
</span></span><span style="display:flex;"><span>e2fsck -f /dev/sdX
</span></span><span style="display:flex;"><span>mount /dev/sdX /backup_dir
</span></span></code></pre></div><h3 id="3-architecture-level-directory-sharding">3. Architecture level: directory sharding</h3>
<p>More broadly, if your system genuinely needs to store tens of millions of physical files, putting all of them into one folder is simply not a good design. Even if you enable <code>large_dir</code>, basic operations such as <code>ls</code> can still become painfully slow.</p>
<p>The standard industry practice here is directory sharding based on filename hashes or IDs.</p>
<p>For example, if a file is named:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>1234567.dat
</span></span></code></pre></div><p>Instead of placing it directly in one giant folder, you split it into subdirectories:</p>
<ul>
<li>Use the first two digits <code>12</code> as the first-level directory.</li>
<li>Use the next two digits <code>34</code> as the second-level directory.</li>
</ul>
<p>The final path becomes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>/backup_dir/12/34/1234567.dat
</span></span></code></pre></div><p>That way, millions of files get distributed across thousands of subdirectories, and the number of files per directory stays low enough that filesystem bottlenecks never build up in the first place.</p>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>Looking back, this was one of those debugging sessions that starts out feeling almost too easy. You think you already know the answer. Then the obvious fix does nothing, and suddenly you are forced to question every assumption you made along the way.</p>
<p>At first, we really thought the answer was just, &ldquo;make the disk bigger.&rdquo; Then we went from 10 TB to more than 20 TB and the problem still sat there, completely unmoved. After that we started suspecting inodes, containers, process limits, and every other system-level resource we could think of. None of them fit.</p>
<p>And then the actual problem turned out to be hiding in EXT4 directory indexing and hash collisions, which is not exactly the first place your mind goes when you see a disk-space error.</p>
<p>That is probably the part I find most worth writing down. Not just that we fixed the issue, but that this kind of problem forces you to revisit system behaviors you normally take for granted. <code>No space left on device</code> sounds incredibly direct, but what it really means can be much more subtle than it looks.</p>
<p>So if you ever run into one of those situations where there is clearly still free space and yet the system refuses to write another byte, it might be worth resisting the urge to trust the error message too literally. Sometimes the real problem is hiding a layer or two deeper.</p>
]]></content:encoded></item></channel></rss>