<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Documenting software development journey ]]></title><description><![CDATA[I’ll share all my WTF moments and the key findings in this blog that, at first glance, made me realize I didn’t know that at all.]]></description><link>https://ashok-gupta.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 21 Oct 2026 18:46:33 GMT</lastBuildDate><atom:link href="https://ashok-gupta.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why O(n) Was Slower Than O(n log n) -  And It Broke Everything I Thought I Knew About Time Complexity]]></title><description><![CDATA[HashSet is O(n). Sorting is O(n log n). So HashSet should always be faster, right? Wrong. Real benchmarks show sorting consistently beating HashSet.]]></description><link>https://ashok-gupta.hashnode.dev/why-o-n-was-slower-than-o-n-log-n-and-it-broke-everything-i-thought-i-knew-about-time-complexity</link><guid isPermaLink="true">https://ashok-gupta.hashnode.dev/why-o-n-was-slower-than-o-n-log-n-and-it-broke-everything-i-thought-i-knew-about-time-complexity</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Time Complexity]]></category><category><![CDATA[optimization]]></category><category><![CDATA[technology]]></category><category><![CDATA[techblog]]></category><category><![CDATA[data structures]]></category><category><![CDATA[Java]]></category><category><![CDATA[cpu]]></category><category><![CDATA[algorithms]]></category><category><![CDATA[Computer Science]]></category><category><![CDATA[performance]]></category><category><![CDATA[Performance Optimization]]></category><dc:creator><![CDATA[Ashok kumar]]></dc:creator><pubDate>Fri, 20 Mar 2026 15:08:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/65de38338eefeee0f292bc72/f46b664b-b132-4ffa-ba2a-3f66f7e0d4bd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>for example:</p>
<pre><code class="language-plaintext">arr1 = [1, 3, 2, 1, 4, 5]
arr2 = [3, 2, 2, 1, 1]
answer = [3, 2, 1]
</code></pre>
<p>simple enough right? and if you've done even a little bit of DSA prep your brain immediately goes - <strong>HashSet.</strong></p>
<p>put all elements of one array into a set, iterate over the other, call <code>.contains()</code>, done. O(1) lookup, O(n) total. clean. elegant. textbook optimal.</p>
<p>and then there's the other approach - the sorting one. sort both arrays, use two pointers, skip duplicates, collect common elements. something like this:</p>
<pre><code class="language-java">Arrays.sort(arr1);
Arrays.sort(arr2);
int n1 = arr1.length;
int n2 = arr2.length;
int i = 0, j = 0;

while (i &lt; n1 &amp;&amp; j &lt; n2) {
    // skip duplicates
    while (i &gt; 0 &amp;&amp; i &lt; n1 &amp;&amp; arr1[i - 1] == arr1[i]) i++;
    while (j &gt; 0 &amp;&amp; j &lt; n2 &amp;&amp; arr2[j - 1] == arr2[j]) j++;

    if (i &gt;= n1 || j &gt;= n2) break;

    if (arr1[i] == arr2[j]) {
        ans.add(arr1[i]);
        i++;
        j++;
    } else if (arr1[i] &gt; arr2[j]) {
        j++;
    } else {
        i++;
    }
}
</code></pre>
<p>and this one? O(n log n) because of the sorting step.</p>
<p>so naturally, solution 1 wins. O(n) beats O(n log n). case closed.</p>
<p>...right?</p>
<hr />
<h2>well, not exactly</h2>
<p>so here's the thing that completely messed with my head. i was reading through a well-known programming book and found actual benchmark data comparing both approaches. real numbers, real hardware. and the results were... uncomfortable.</p>
<table>
<thead>
<tr>
<th>n</th>
<th>HashSet — O(n)</th>
<th>Sorting — O(n log n)</th>
</tr>
</thead>
<tbody><tr>
<td>10^6</td>
<td>0.3s</td>
<td>0.2s</td>
</tr>
<tr>
<td>2 × 10^6</td>
<td>0.8s</td>
<td>0.3s</td>
</tr>
<tr>
<td>3 × 10^6</td>
<td>1.3s</td>
<td>0.5s</td>
</tr>
<tr>
<td>4 × 10^6</td>
<td>1.7s</td>
<td>0.7s</td>
</tr>
<tr>
<td>5 × 10^6</td>
<td>2.3s</td>
<td>0.9s</td>
</tr>
</tbody></table>
<p>read that again.</p>
<p>the O(n log n) solution is <strong>consistently faster</strong> than the O(n) solution. and the gap doesn't close as n grows - it actually gets <strong>worse</strong> for the HashSet approach.</p>
<p>what?</p>
<p>if you're sitting there thinking "that can't be right, O(n) is literally a smaller function than O(n log n)" - yeah, that's exactly what i thought too. and that's exactly why this is worth writing about.</p>
<p>because here's the uncomfortable truth: <strong>Big-O notation tells you how an algorithm scales. it doesn't tell you how fast it actually runs.</strong></p>
<hr />
<h2>so what's actually going on?</h2>
<p>to understand this, you need to understand something about how your CPU actually works - specifically, how it reads data from memory. and trust me, once you get this, you'll never look at algorithm analysis the same way.</p>
<h3>your CPU doesn't read from RAM - it reads from cache</h3>
<p>your CPU has a multi-level memory hierarchy. think of it like this:</p>
<table>
<thead>
<tr>
<th>Level</th>
<th>Access Time</th>
<th>What It Is</th>
</tr>
</thead>
<tbody><tr>
<td>L1 Cache</td>
<td>~1 ns</td>
<td>tiny, blazing fast, right next to the CPU core</td>
</tr>
<tr>
<td>L2 Cache</td>
<td>~3-5 ns</td>
<td>slightly bigger, still very fast</td>
</tr>
<tr>
<td>L3 Cache</td>
<td>~10-20 ns</td>
<td>shared across cores, decent size</td>
</tr>
<tr>
<td>Main RAM</td>
<td>~50-100 ns</td>
<td>huge, but <strong>painfully slow</strong> compared to cache</td>
</tr>
</tbody></table>
<p>look at that gap between L1 and RAM. that's roughly a <strong>50-100x difference.</strong> reading from RAM is not just "a little slower" - it's a completely different universe of slow compared to reading from cache.</p>
<p>and here's the key mechanism that makes this matter: <strong>cache lines.</strong></p>
<p>when your CPU needs to read <code>arr[i]</code>, it doesn't just fetch that one element. it fetches an entire <strong>cache line</strong> - typically 64 bytes of contiguous memory. so if your array elements are 4-byte integers, one cache fetch pulls in the next 16 elements for free.</p>
<p>so when you access <code>arr[0]</code>, the CPU automatically pulls <code>arr[0]</code> through <code>arr[15]</code> into L1 cache. now when you access <code>arr[1]</code>, <code>arr[2]</code>, <code>arr[3]</code>... they're all already there. zero wait time. this is called <strong>spatial locality</strong> - accessing nearby memory locations in sequence.</p>
<hr />
<h2>now let's look at what happens with each approach</h2>
<h3>sorting - the cache's best friend</h3>
<p>merge sort and quicksort access array elements <strong>sequentially</strong> or in <strong>predictable patterns.</strong> the CPU's prefetcher sees this pattern and starts loading the next cache line before you even ask for it.</p>
<p>so the two-pointer traversal after sorting? pure sequential access. <code>arr1[0]</code>, <code>arr1[1]</code>, <code>arr1[2]</code>... every single access is a cache hit. the data is already sitting in L1 by the time the CPU needs it.</p>
<p>this is what we call <strong>cache-friendly</strong> access. the hardware is literally optimized for exactly this kind of pattern.</p>
<h3>HashSet - the cache's worst nightmare</h3>
<p>now think about what a HashSet actually does under the hood.</p>
<p>when you insert a value, the hash function computes a hash code and maps it to a bucket. the thing is - hash functions are <strong>designed</strong> to scatter values uniformly across memory. that's literally their job - to distribute things randomly so you get even bucket distribution.</p>
<p>but "randomly distributed across memory" is the exact opposite of what your CPU cache wants.</p>
<p>every single <code>.contains()</code> call goes something like:</p>
<ol>
<li><p>compute hash → jump to some random bucket</p>
</li>
<li><p>follow a pointer to the node → another random memory location</p>
</li>
<li><p>compare the value → maybe follow another pointer if there's a collision chain</p>
</li>
</ol>
<p>each of these jumps is likely a <strong>cache miss.</strong> the CPU has to go all the way to main RAM. 50-100 nanoseconds. per lookup.</p>
<p>and it gets worse. because HashSet in Java uses a linked structure internally (buckets with nodes/trees), you're chasing pointers all over the heap. the CPU prefetcher has absolutely no idea where you're going next. it can't help you.</p>
<p>so your "O(1) lookup" is technically O(1) in terms of comparisons - but each comparison costs 50-100x more wall-clock time because it's hitting RAM instead of cache.</p>
<hr />
<h2>the hidden constant that Big-O hides</h2>
<p>this is the thing they don't really emphasize in most DSA courses.</p>
<p>when we say an algorithm is O(n), what we actually mean is the running time is <strong>c × n</strong> for some constant c. and when we say O(n log n), it's <strong>k × n × log(n)</strong> for some constant k.</p>
<p>Big-O drops those constants because "they don't matter asymptotically." and that's mathematically true. but in the real world, on real hardware, <strong>the constants are where your program actually lives.</strong></p>
<p>for the HashSet approach:</p>
<ul>
<li><p>c includes: hash computation, random memory access (cache miss), pointer chasing, collision handling</p>
</li>
<li><p>each operation might cost ~100-200 ns due to cache misses</p>
</li>
</ul>
<p>for the sorting approach:</p>
<ul>
<li><p>k includes: comparisons and swaps on sequential data (cache hits)</p>
</li>
<li><p>each operation might cost ~1-5 ns because everything is in cache</p>
</li>
</ul>
<p>so the real comparison isn't n vs n·log(n). it's:</p>
<pre><code class="language-plaintext">HashSet:  200 × n
Sorting:  3 × n × log(n)
</code></pre>
<p>at n = 5,000,000:</p>
<pre><code class="language-plaintext">HashSet:  200 × 5,000,000             = 1,000,000,000
Sorting:  3 × 5,000,000 × 22.3       =   334,500,000
</code></pre>
<p>sorting wins by <strong>3x.</strong> which is almost exactly what the benchmark data shows.</p>
<hr />
<h2>so does this mean HashSet is bad?</h2>
<p>no. absolutely not.</p>
<p>HashSet is still the right choice in a <strong>lot</strong> of scenarios - especially when:</p>
<ul>
<li><p>your dataset fits comfortably in cache</p>
</li>
<li><p>you need frequent lookups against a relatively static set</p>
</li>
<li><p>you're doing set operations (union, intersection) on smaller collections</p>
</li>
<li><p>the alternative would require repeated sorting</p>
</li>
</ul>
<p>the point isn't "never use HashSet." the point is: <strong>don't blindly trust Big-O as a performance guarantee.</strong></p>
<p>Big-O tells you the shape of the growth curve. it does not tell you:</p>
<ul>
<li><p>the constant factor (which can be 100x different)</p>
</li>
<li><p>the cache behavior (which dominates real performance)</p>
</li>
<li><p>the memory allocation overhead</p>
</li>
<li><p>the branch prediction patterns</p>
</li>
</ul>
<hr />
<h2>the one-line takeaway</h2>
<p><strong>O(1) with a cache miss can be slower than O(log n) with a cache hit.</strong> the hardware speed gap between cache and RAM is so massive that it can completely overwhelm algorithmic advantages.</p>
<p>next time you're choosing between a hash-based approach and a sort-based approach, don't just compare the Big-O. ask yourself: <em>how is my CPU actually going to access this data?</em></p>
<p>because sometimes the "slower" algorithm is the faster program.</p>
<hr />
<p><em>if you found this interesting, i'm planning to write more about the gap between theoretical CS and what actually happens on real hardware. things like why linked lists are almost always slower than arrays in practice (same cache story), and why B-trees beat binary search trees for databases. follow along if you're into this stuff.</em></p>
]]></content:encoded></item></channel></rss>