<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Arpan Singh</title>
    <description>The latest articles on DEV Community by Arpan Singh (@arpan_singh_121).</description>
    <link>https://dev.to/arpan_singh_121</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4045545%2Fbe5f845d-2bba-4403-b60e-bf938d72c2da.png</url>
      <title>DEV Community: Arpan Singh</title>
      <link>https://dev.to/arpan_singh_121</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arpan_singh_121"/>
    <language>en</language>
    <item>
      <title>Big-O Notation Explained: The Regex That Took Down Cloudflare for 27 Minutes</title>
      <dc:creator>Arpan Singh</dc:creator>
      <pubDate>Fri, 24 Jul 2026 12:09:43 +0000</pubDate>
      <link>https://dev.to/arpan_singh_121/big-o-notation-explained-the-regex-that-took-down-cloudflare-for-27-minutes-1881</link>
      <guid>https://dev.to/arpan_singh_121/big-o-notation-explained-the-regex-that-took-down-cloudflare-for-27-minutes-1881</guid>
      <description>&lt;p&gt;At 13:42 UTC on July 2, 2019, Cloudflare's network stopped answering requests. Not slow. Down.&lt;/p&gt;

&lt;p&gt;For 27 minutes, a huge slice of the web returned 502 errors instead of loading.&lt;/p&gt;

&lt;p&gt;The cause wasn't a DDoS attack or a hardware failure. It was one regular expression, in one firewall rule, pushed to every server Cloudflare owns at the same time.&lt;/p&gt;

&lt;p&gt;That regex had a property called catastrophic backtracking. Feed it the wrong kind of input, and instead of matching in microseconds, the matching time roughly doubles with every extra character.&lt;/p&gt;

&lt;p&gt;Cloudflare's own &lt;a href="https://blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019" rel="noopener noreferrer"&gt;post-mortem&lt;/a&gt; lays it out plainly: a WAF rule update shipped a regex that ate CPU on every core handling HTTP and HTTPS traffic, worldwide, within minutes of going live.&lt;/p&gt;

&lt;p&gt;That single deployment is the entire reason Big-O notation is worth learning as a working developer, not just memorizing for an interview and forgetting.&lt;/p&gt;

&lt;p&gt;Every data structure and algorithm in this series has a Big-O. Get it wrong, and code that sails through testing can take your production system down exactly like it did for Cloudflare.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Big-O Actually Measures
&lt;/h2&gt;

&lt;p&gt;Big-O is not a stopwatch. It doesn't tell you an operation takes "3 milliseconds."&lt;/p&gt;

&lt;p&gt;It tells you how the &lt;em&gt;cost&lt;/em&gt; of an operation grows as the input grows. That's the whole idea.&lt;/p&gt;

&lt;p&gt;Say a function checks every item in a list once. Double the list, and the function does roughly double the work. That growth pattern, cost scaling linearly with size, is what O(n) describes.&lt;/p&gt;

&lt;p&gt;Big-O deliberately throws away two things: the exact hardware you're running on, and small, constant-size costs. It keeps only the part that matters once your input gets big: the shape of the growth curve.&lt;/p&gt;

&lt;p&gt;That's also why the Cloudflare regex was so dangerous. Its growth curve wasn't linear, or even quadratic. It was exponential, and exponential curves look almost flat for small inputs, then explode the moment the input crosses a threshold. That's exactly the kind of bug that passes every test and detonates in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dropping Constants: Reading Big-O Like an Algebra Problem
&lt;/h2&gt;

&lt;p&gt;Say you profile a function and its operation count comes out to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3n² + 5n + 2

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Big-O notation calls this function O(n²). Here's why, term by term.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drop the lower-order terms.&lt;/strong&gt; Once &lt;code&gt;n&lt;/code&gt; gets large, &lt;code&gt;5n&lt;/code&gt; and &lt;code&gt;2&lt;/code&gt; stop mattering next to &lt;code&gt;3n²&lt;/code&gt;. At n = 1,000,000: &lt;code&gt;3n²&lt;/code&gt; is 3 trillion, &lt;code&gt;5n&lt;/code&gt; is 5 million. A rounding error by comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drop the constant multiplier.&lt;/strong&gt; Whether it's &lt;code&gt;3n²&lt;/code&gt; or &lt;code&gt;30n²&lt;/code&gt;, both describe the same &lt;em&gt;shape&lt;/em&gt; of growth: quadratic. The multiplier affects real-world speed, which is a separate conversation from Big-O, but it doesn't change the growth curve.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;3n² + 5n + 2&lt;/code&gt; becomes &lt;code&gt;O(n²)&lt;/code&gt;, every time. This is the most common thing beginners get wrong — they see two nested loops and one extra pass, and try to write &lt;code&gt;O(2n² + n)&lt;/code&gt;. Nobody writes that. It's &lt;code&gt;O(n²)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Complexity Ladder
&lt;/h2&gt;

&lt;p&gt;Here's every complexity class you'll run into in this series, best to worst, each with one operation you already use.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Notation&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;A real example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;Constant&lt;/td&gt;
&lt;td&gt;Reading arr[5] from an array&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;td&gt;Logarithmic&lt;/td&gt;
&lt;td&gt;Binary search on a sorted array&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;Linear&lt;/td&gt;
&lt;td&gt;Scanning a list once for a value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;td&gt;O(n log n)&lt;/td&gt;
&lt;td&gt;Log-linear&lt;/td&gt;
&lt;td&gt;Sorting with merge sort or Python's Timsort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟠&lt;/td&gt;
&lt;td&gt;O(n²)&lt;/td&gt;
&lt;td&gt;Quadratic&lt;/td&gt;
&lt;td&gt;Comparing every pair with nested loops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;O(2ⁿ)&lt;/td&gt;
&lt;td&gt;Exponential&lt;/td&gt;
&lt;td&gt;Naive recursive Fibonacci — and Cloudflare's regex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;O(n!)&lt;/td&gt;
&lt;td&gt;Factorial&lt;/td&gt;
&lt;td&gt;Trying every ordering of n items&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Green means it scales to millions of items without blinking. Yellow still holds up at real-world scale. Orange starts hurting once &lt;code&gt;n&lt;/code&gt; hits the thousands. Red is the zone Cloudflare's regex lived in — fine for tiny inputs, catastrophic past a threshold.&lt;/p&gt;

&lt;p&gt;Each step down that table is a different universe of slowdown. At n = 20, O(n) and O(2ⁿ) are both fast. At n = 40, O(n) is still instant. O(2ⁿ) has over a trillion steps left to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Case, Average Case, Worst Case
&lt;/h2&gt;

&lt;p&gt;Big-O by itself usually means the &lt;strong&gt;worst case&lt;/strong&gt;: the input that makes an algorithm work hardest.&lt;/p&gt;

&lt;p&gt;Linear search through an unsorted list is O(n) worst case (the value isn't there, or it's last) but O(1) best case (it's the first element checked).&lt;/p&gt;

&lt;p&gt;Quicksort is the classic split-personality algorithm: O(n log n) on average, but O(n²) worst case if it keeps picking a bad pivot on already-sorted data. That's exactly why production sort implementations (Python's Timsort, V8's sort) don't run plain quicksort. They hedge against that worst case directly.&lt;/p&gt;

&lt;p&gt;When someone asks "what's the Big-O of this," they almost always mean the worst case unless they say otherwise. That's the convention this series follows too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Space Complexity: The Other Half of the Bill
&lt;/h2&gt;

&lt;p&gt;Big-O also measures memory, not just time. Same rules, different resource.&lt;/p&gt;

&lt;p&gt;An algorithm that builds a second array the same size as the input is O(n) space. One that tracks a couple of variables, regardless of input size, is O(1) space.&lt;/p&gt;

&lt;p&gt;Recursive functions carry a space cost most beginners miss: every call sits on the call stack until it returns. A recursive function that goes n levels deep uses O(n) space on the stack alone, even without allocating a single array. That'll matter once recursion shows up properly in Day 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Amortized Time: Why "Append" Isn't Always O(1)
&lt;/h2&gt;

&lt;p&gt;Both Python's &lt;code&gt;list.append()&lt;/code&gt; and JavaScript's &lt;code&gt;array.push()&lt;/code&gt; get described as O(1). That's not the whole story.&lt;/p&gt;

&lt;p&gt;Under the hood, both languages back their dynamic arrays with a fixed-size block of memory. Fill it up, and the next append has to allocate a bigger block and copy every existing element over — an O(n) operation.&lt;/p&gt;

&lt;p&gt;Both languages soften this the same way: they over-allocate, growing the backing array by a multiplying factor instead of one slot at a time. Resizes get rarer as the array grows, so the &lt;em&gt;average&lt;/em&gt; cost per append, across many appends, still comes out to O(1). That average has a name: &lt;strong&gt;amortized O(1)&lt;/strong&gt;. It's worth knowing before dynamic arrays show up properly in Day 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Read Your Own Code's Complexity
&lt;/h2&gt;

&lt;p&gt;There's a repeatable process here, simpler than it looks once you've done it a few times.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Look at the loops] --&amp;gt; B{Loops nested?}
    B --&amp;gt;|No, sequential| C["Add them: O(a) + O(b), keep the larger term"]
    B --&amp;gt;|Yes| D["Multiply them: O(a) times O(b)"]
    D --&amp;gt; E{Does each step cut&amp;lt;br/&amp;gt;the input in half?}
    E --&amp;gt;|Yes| F["O(log n) for that part"]
    E --&amp;gt;|No| G["O(n) for that part"]
    C --&amp;gt; H{Any recursive calls?}
    G --&amp;gt; H
    F --&amp;gt; H
    H --&amp;gt;|Yes| I["Count total calls made,&amp;lt;br/&amp;gt;not just the visible loop"]
    H --&amp;gt;|No| J["Done, that's your Big-O"]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walk that against a real function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;has_duplicate_pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;             &lt;span class="c1"&gt;# loop 1: n iterations
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;  &lt;span class="c1"&gt;# loop 2: nested inside loop 1
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two loops, nested. That means multiply. Loop 1 runs n times, loop 2 runs roughly n times too (shrinking a bit each pass, a detail Big-O ignores). O(n) times O(n) = &lt;strong&gt;O(n²)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Compare that to a function that looks similar but isn't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;has_negative_or_over_limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;               &lt;span class="c1"&gt;# loop 1: n iterations
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                &lt;span class="c1"&gt;# loop 2: separate, not nested
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two loops again, but sequential, not nested. Add instead of multiply: O(n) + O(n), the constant drops, and it's &lt;strong&gt;O(n)&lt;/strong&gt;. Seeing "two loops" and assuming O(n²) automatically is one of the most common mistakes in code review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Benchmarks: What This Looks Like on Real Hardware
&lt;/h2&gt;

&lt;p&gt;Theory is one thing. Here's what these growth curves look like measured for real — Python 3.12 and Node.js 22, same machine, same inputs, timed with &lt;code&gt;timeit&lt;/code&gt; and &lt;code&gt;process.hrtime.bigint()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  O(1): array index access
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Python (ns/op)&lt;/th&gt;
&lt;th&gt;Node.js (ns/op)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;43.3&lt;/td&gt;
&lt;td&gt;14.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;31.3&lt;/td&gt;
&lt;td&gt;9.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;32.1&lt;/td&gt;
&lt;td&gt;10.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;30.8&lt;/td&gt;
&lt;td&gt;27.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Flat, as expected. The time doesn't track &lt;code&gt;n&lt;/code&gt; at all. The small jump at 1,000,000 in the Node.js column isn't a Big-O violation; that's the array outgrowing CPU cache, a real hardware effect Big-O doesn't model. Big-O describes algorithmic growth, not what the cache hierarchy underneath it is doing.&lt;/p&gt;

&lt;h3&gt;
  
  
  O(log n): binary search
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Python bisect (ns/op)&lt;/th&gt;
&lt;th&gt;Node.js (ns/op)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;114.8&lt;/td&gt;
&lt;td&gt;35.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;121.7&lt;/td&gt;
&lt;td&gt;24.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;156.8&lt;/td&gt;
&lt;td&gt;25.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;159.0&lt;/td&gt;
&lt;td&gt;28.6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The input grew 1,000x. The time barely moved. That's the entire pitch for logarithmic algorithms in one table.&lt;/p&gt;

&lt;h3&gt;
  
  
  O(n): linear search, worst case (target never present)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Python (µs/op)&lt;/th&gt;
&lt;th&gt;Node.js (µs/op)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;6.62&lt;/td&gt;
&lt;td&gt;1.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;59.49&lt;/td&gt;
&lt;td&gt;10.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;616.47&lt;/td&gt;
&lt;td&gt;102.58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;6,060.19&lt;/td&gt;
&lt;td&gt;1,514.06&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;10x the input, roughly 10x the time, in both languages. Textbook linear.&lt;/p&gt;

&lt;h3&gt;
  
  
  O(n log n): sorting (Python's Timsort, V8's sort)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Python (ms/op)&lt;/th&gt;
&lt;th&gt;Node.js (ms/op)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;0.083&lt;/td&gt;
&lt;td&gt;0.202&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;1.262&lt;/td&gt;
&lt;td&gt;2.057&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;18.386&lt;/td&gt;
&lt;td&gt;26.654&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;260.294&lt;/td&gt;
&lt;td&gt;326.918&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Growing faster than linear, much slower than quadratic — the middle ground n log n describes. Don't read too much into Python edging out Node.js here; it's one machine, one run, and the gap is small enough that engine warm-up explains most of it, not "Python's sort is better."&lt;/p&gt;

&lt;h3&gt;
  
  
  O(n²): checking every pair for duplicates, nested loops
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Python (ms/op)&lt;/th&gt;
&lt;th&gt;Node.js (ms/op)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;3.25&lt;/td&gt;
&lt;td&gt;0.09&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;13.82&lt;/td&gt;
&lt;td&gt;0.34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;53.97&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,000&lt;/td&gt;
&lt;td&gt;219.10&lt;/td&gt;
&lt;td&gt;5.29&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Double &lt;code&gt;n&lt;/code&gt;, and the time roughly quadruples, in both languages, at every step. That's the signature of O(n²): a pattern you can watch happen in real numbers.&lt;/p&gt;

&lt;h3&gt;
  
  
  O(2ⁿ) vs O(n): naive vs. memoized Fibonacci
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Naive Python (ms)&lt;/th&gt;
&lt;th&gt;Memoized Python (µs)&lt;/th&gt;
&lt;th&gt;Naive Node.js (ms)&lt;/th&gt;
&lt;th&gt;Memoized Node.js (µs)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;0.663&lt;/td&gt;
&lt;td&gt;7.63&lt;/td&gt;
&lt;td&gt;0.091&lt;/td&gt;
&lt;td&gt;3.61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;8.613&lt;/td&gt;
&lt;td&gt;6.01&lt;/td&gt;
&lt;td&gt;0.706&lt;/td&gt;
&lt;td&gt;4.31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;78.953&lt;/td&gt;
&lt;td&gt;6.84&lt;/td&gt;
&lt;td&gt;7.700&lt;/td&gt;
&lt;td&gt;5.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;206.629&lt;/td&gt;
&lt;td&gt;7.15&lt;/td&gt;
&lt;td&gt;20.428&lt;/td&gt;
&lt;td&gt;5.11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the table that matters most in the whole post. Going from n=30 to n=32 — adding two — makes the naive version take roughly 2.6x longer, in both languages independently. That 2.6x isn't a coincidence: it's the golden ratio squared, baked into how Fibonacci's recursion tree branches.&lt;/p&gt;

&lt;p&gt;The memoized version barely moves across the entire range. One dictionary, or one &lt;code&gt;Map&lt;/code&gt;, turns an O(2ⁿ) function into an O(n) function. The benchmark shows exactly what that's worth: milliseconds versus microseconds.&lt;/p&gt;

&lt;p&gt;The benchmark scripts used to generate the numbers listed above have been uploaded to &lt;a href="https://github.com/arpansingh9888/dsa-mastery-series" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; for reference.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fib_naive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fib_naive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fib_naive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fib_memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{}):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fib_memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fib_memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where the Regex Went Wrong
&lt;/h2&gt;

&lt;p&gt;Back to Cloudflare, with the vocabulary to actually explain it now.&lt;/p&gt;

&lt;p&gt;A backtracking regex engine, faced with an ambiguous pattern, can end up trying every possible way to match it — the regex equivalent of the naive Fibonacci function above, except &lt;code&gt;n&lt;/code&gt; is the length of the input string, and there's no memoization built in by default.&lt;/p&gt;

&lt;p&gt;Small inputs, like most real traffic, never trigger the worst case. That's exactly why the rule passed testing.&lt;/p&gt;

&lt;p&gt;One request with the right shape of pathological input turned a normally instant regex into an O(2ⁿ) computation, run on every CPU core, on every server, at the same time. That's the outage in one sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Day 2 starts with arrays. There's enough ground there to split it: fundamentals and core operations first, then patterns like two-pointer and sliding window as their own follow-up. Linked lists get the same treatment once we reach them: singly and doubly linked first, then the interview-heavy patterns (reversal, cycle detection, merging) as a separate post.&lt;/p&gt;

&lt;p&gt;Every post from here on states the time and space complexity of every operation it covers. This is the post that vocabulary comes from.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/big-o-notation-explained-the-regex-that-took-down-cloudflare-for-27-minutes-f1e8y" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>performance</category>
      <category>algorithms</category>
      <category>datastructures</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Arrays Explained from Memory to Big O (With Real Benchmarks)</title>
      <dc:creator>Arpan Singh</dc:creator>
      <pubDate>Fri, 24 Jul 2026 12:08:55 +0000</pubDate>
      <link>https://dev.to/arpan_singh_121/arrays-explained-from-memory-to-big-o-with-real-benchmarks-4g8</link>
      <guid>https://dev.to/arpan_singh_121/arrays-explained-from-memory-to-big-o-with-real-benchmarks-4g8</guid>
      <description>&lt;h1&gt;
  
  
  This Loop Should Delete Three Items. It Deletes Two.
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that and guess the output. Three values are under 50: 45, 30, 15. So the obvious guess is &lt;code&gt;[88, 92, 67]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Run it, and Python prints &lt;code&gt;[30, 88, 92, 67]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;30 survives. Nothing crashed, no exception, no warning. The code just quietly kept a value it was explicitly told to remove.&lt;/p&gt;

&lt;p&gt;Swap the language and the bug follows you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same input, same wrong survivor: &lt;code&gt;[ 30, 88, 92, 67 ]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two different languages, two different removal methods, the exact same failure. That's not a coincidence. It's what an array &lt;em&gt;is&lt;/em&gt; underneath both of them, and Day 2 starts there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What An Array Actually Is
&lt;/h2&gt;

&lt;p&gt;Forget the syntax for a second. At the hardware level, an array is a promise: a single, unbroken block of memory, where every element sits the same distance apart.&lt;/p&gt;

&lt;p&gt;That promise is what makes &lt;code&gt;scores[3]&lt;/code&gt; fast. The computer doesn't search for index 3. It calculates an address directly: start of the block, plus 3 times the size of one element, done. One arithmetic operation, one memory read. That's the entire reason array access is O(1): it's not clever, it's just math.&lt;/p&gt;

&lt;p&gt;Deletion breaks that promise, at least temporarily. Remove the item at index 0, and there's now a gap at the front of an otherwise unbroken block. The only way to restore the promise (a single contiguous run with no holes) is to physically slide every element after the gap back by one position.&lt;/p&gt;

&lt;p&gt;That slide is the whole story. It's an O(n) memory move hiding behind a one-line method call, and it's exactly what desynced the loop above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why The Loop Skips
&lt;/h2&gt;

&lt;p&gt;Walk &lt;code&gt;scores.remove(score)&lt;/code&gt; through what actually happens in memory, one step at a time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The loop's cursor sits at index 0. It reads &lt;code&gt;45&lt;/code&gt;, sees it's under 50, and removes it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Removing index 0 slides everything left by one slot. The value that used to be at index 1, &lt;code&gt;30&lt;/code&gt;, is now sitting at index 0.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The loop cursor doesn't know that happened. It advances to index 1, as it always does.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Index 1 now holds &lt;code&gt;88&lt;/code&gt;, the value that used to be two slots over. &lt;code&gt;30&lt;/code&gt; was never re-examined. It's still in the array when the loop ends, because the loop moved past the exact slot it landed in.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix follows directly from the same mental model. Either walk the array backward, so every shift happens &lt;em&gt;behind&lt;/em&gt; the cursor instead of into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or don't mutate the array you're iterating at all, build a new one instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version is also the one worth defaulting to. It sidesteps the shifting problem entirely instead of just working around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static Arrays vs. What You're Actually Using
&lt;/h2&gt;

&lt;p&gt;C's &lt;code&gt;int arr[10]&lt;/code&gt; is a static array: one fixed-size block, allocated once, that can never grow. Ask for an 11th slot and there's no polite failure. You're writing into memory that belongs to something else.&lt;/p&gt;

&lt;p&gt;Python's &lt;code&gt;list&lt;/code&gt; and JavaScript's &lt;code&gt;Array&lt;/code&gt; aren't that. They're &lt;strong&gt;dynamic arrays&lt;/strong&gt;: a static array under the hood, wrapped in logic that swaps in a bigger block once the old one fills up. Every &lt;code&gt;.append()&lt;/code&gt; or &lt;code&gt;.push()&lt;/code&gt; you've ever called was secretly asking "is there room?" before it ever touched memory.&lt;/p&gt;

&lt;p&gt;That wrapping is why beginners get told arrays are O(1) to append to, and it's &lt;em&gt;mostly&lt;/em&gt; true, but "mostly" is doing real work in that sentence, covered next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every Core Operation, Rated
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Complexity&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;Access by index&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;Direct address math, no searching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;Append / pop at the end&lt;/td&gt;
&lt;td&gt;O(1)*&lt;/td&gt;
&lt;td&gt;No shift needed; *occasionally O(n) — grows past capacity, shrinks past half capacity, more below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟠&lt;/td&gt;
&lt;td&gt;Search (unsorted)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;No shortcuts — check every slot until found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;Insert / delete at the start&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;Every remaining element shifts by one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;Insert / delete in the middle&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;Same shift, just for a shorter stretch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern in that table is the entire lesson: &lt;strong&gt;where&lt;/strong&gt; in the array you operate matters as much as &lt;strong&gt;what&lt;/strong&gt; operation you run. The end is cheap. The front is expensive. That's not a Python or JavaScript quirk. It's physics, the same physics that broke the loop above.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Asterisk on "O(1) Append"
&lt;/h2&gt;

&lt;p&gt;Dynamic arrays grow by over-allocating: when the backing block fills up, they don't grab one more slot, they grab a noticeably bigger block and copy everything over. That copy is O(n), but it happens rarely enough that the &lt;em&gt;average&lt;/em&gt; cost per append, over many appends, still comes out to O(1). That average has a name: amortized O(1).&lt;/p&gt;

&lt;p&gt;"Rarely enough" isn't hand-waving. CPython's growth pattern is public, and it's checkable from Python itself, since &lt;code&gt;sys.getsizeof()&lt;/code&gt; reports the backing array's real allocated size, not just the list's length. Appending one item at a time and watching that number jump gives you the exact resize points, live, on CPython 3.12:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;List length at resize&lt;/th&gt;
&lt;th&gt;Backing array size (bytes)&lt;/th&gt;
&lt;th&gt;Slots allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;184&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;248&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;312&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;376&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Between each row, appends are just writing into already-reserved space: genuinely O(1), no copying at all. The resize itself, on that one unlucky append, is O(n). Spread that occasional O(n) cost across all the free appends in between, and it averages out to O(1) per call.&lt;/p&gt;

&lt;p&gt;Shrinking runs the same machinery in reverse, on a different trigger than most people guess. &lt;code&gt;pop()&lt;/code&gt; doesn't hand memory back the moment the list gets smaller: CPython only reallocates down once the length falls under half of what's currently allocated, not the instant a single slot goes unused. Pop a 33-item list down to empty and the real shrink points, traced the same way with &lt;code&gt;sys.getsizeof()&lt;/code&gt;, land at lengths 16, 11, 7, 5, 1, and 0. Compare that to the growth table above and the thresholds don't mirror each other; growing and shrinking are governed by two separate rules sharing one function, not a single reversible curve.&lt;/p&gt;

&lt;p&gt;V8 runs the same strategy for JavaScript arrays, over-allocating on growth instead of resizing one slot at a time. That's why &lt;code&gt;.push()&lt;/code&gt; gets the same amortized O(1) label &lt;code&gt;.append()&lt;/code&gt; does.&lt;/p&gt;

&lt;h2&gt;
  
  
  One More Thing V8 Tracks That CPython Doesn't
&lt;/h2&gt;

&lt;p&gt;V8 keeps something called an array's &lt;strong&gt;elements kind&lt;/strong&gt; — a running classification of what's actually in it: all small integers, mixed with floats, holding arbitrary objects, or containing gaps. &lt;a href="https://v8.dev/blog/elements-kinds" rel="noopener noreferrer"&gt;V8's own engineering blog&lt;/a&gt; lays out the mechanism: arrays built with actual gaps in them (skipped indices, or presized with &lt;code&gt;new Array(n)&lt;/code&gt; and filled out of order) get marked &lt;strong&gt;holey&lt;/strong&gt;, and every read from a holey array carries an extra check a gap-free ("packed") array skips.&lt;/p&gt;

&lt;p&gt;That transition only runs one direction: specific to general, packed to holey. It's permanent. Fill every gap in a holey array later, and V8 still won't reclassify it back to packed. The practical takeaway: build arrays in order, front to back, if you want V8 to keep every optimization available to it.&lt;/p&gt;

&lt;p&gt;How much that actually costs in practice is genuinely inconsistent. A plain summation loop over a packed vs. a holey million-element array, benchmarked repeatedly on this machine, came back centered close to even, drifting up to roughly 13% either direction depending on the run, with no consistent winner. Real, but not the dramatic one-directional gap sometimes claimed for it online. The scripts to run that comparison yourself are in the attached zip.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Benchmarks
&lt;/h2&gt;

&lt;p&gt;Same rule as Day 1: no number below is estimated. Python 3.12 and Node.js 22, timed with &lt;code&gt;time.perf_counter()&lt;/code&gt; and &lt;code&gt;process.hrtime.bigint()&lt;/code&gt;, each operation paired with its opposite (append then pop, insert then remove) so the array's size stays constant across the whole run.&lt;/p&gt;

&lt;h3&gt;
  
  
  End of the array: append / pop
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Python append (ns)&lt;/th&gt;
&lt;th&gt;Python pop (ns)&lt;/th&gt;
&lt;th&gt;Node push+pop (ns, combined)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;44.9&lt;/td&gt;
&lt;td&gt;31.0&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;31.4&lt;/td&gt;
&lt;td&gt;31.0&lt;/td&gt;
&lt;td&gt;1.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;32.1&lt;/td&gt;
&lt;td&gt;30.9&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;31.7&lt;/td&gt;
&lt;td&gt;34.6&lt;/td&gt;
&lt;td&gt;1.7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Flat across four orders of magnitude, in both languages. That's the empirical signature of O(1). The Node numbers look almost too fast to be real; that's V8's JIT compiling this exact push-then-pop pattern more aggressively the longer the process runs, not a language-superiority result. Treat the flatness as the finding, not the specific nanosecond count.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start of the array: insert / delete
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Python insert(0) (µs)&lt;/th&gt;
&lt;th&gt;Python pop(0) (µs)&lt;/th&gt;
&lt;th&gt;Node unshift (µs)&lt;/th&gt;
&lt;th&gt;Node shift (µs)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;0.26&lt;/td&gt;
&lt;td&gt;0.12&lt;/td&gt;
&lt;td&gt;0.14&lt;/td&gt;
&lt;td&gt;0.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;2.60&lt;/td&gt;
&lt;td&gt;1.84&lt;/td&gt;
&lt;td&gt;1.85&lt;/td&gt;
&lt;td&gt;0.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;24.77&lt;/td&gt;
&lt;td&gt;17.97&lt;/td&gt;
&lt;td&gt;18.65&lt;/td&gt;
&lt;td&gt;18.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;364.64&lt;/td&gt;
&lt;td&gt;342.58&lt;/td&gt;
&lt;td&gt;362.43&lt;/td&gt;
&lt;td&gt;348.51&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;10x the input keeps landing close to 10x the time, in every column. That's the same linear signature the O(n) search benchmark showed on Day 1, now showing up for a different operation entirely. (Node's &lt;code&gt;shift&lt;/code&gt; at n=10,000 reads oddly low next to its neighbors — at that size the fixed cost of the function call itself is still competing with the shift cost, so the line hasn't straightened out yet. It does by 100,000.)&lt;/p&gt;

&lt;p&gt;Both scripts behind every number in this post (the Python and Node versions, plus the resize-point tracer and the packed/holey comparison) are packaged in &lt;a href="https://github.com/arpansingh9888/dsa-mastery-series/tree/master/arrays-benchmarks" rel="noopener noreferrer"&gt;arrays-ops-benchmark.zip&lt;/a&gt;, attached to this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to the Bug
&lt;/h2&gt;

&lt;p&gt;The loop at the top of this post wasn't a trick question. It's the single most common way arrays bite working developers, in any language that has them, and it only looks mysterious until you've internalized what deletion actually costs.&lt;/p&gt;

&lt;p&gt;Once you know an array is a contiguous block, and that removing from the middle means physically sliding everything after it, the loop's behavior stops being surprising. It's not broken. It's doing exactly what shifting a block of memory always does. The bug was ever expecting the cursor to notice.&lt;/p&gt;

&lt;p&gt;Try it yourself before the next post: take the backward-iteration fix above, and instead of removing values under 50, remove every &lt;em&gt;duplicate&lt;/em&gt; value from a list, keeping just the first occurrence of each. Same shifting mechanics, one extra piece of state to track. It's a good gut-check for whether the mental model actually stuck.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/arrays-explained-from-memory-to-big-o-with-real-benchmarks-y8b8q" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>python</category>
      <category>node</category>
      <category>programmingfundamentals</category>
      <category>arrays</category>
    </item>
  </channel>
</rss>
