Algorithms - Learn With Examples https://learnwithexamples.org/category/computer-science-concepts/algorithms/ Lets Learn things the Easy Way Tue, 04 Aug 2026 17:20:22 +0000 en-US hourly 1 https://wordpress.org/?v=7.0.4 https://i0.wp.com/learnwithexamples.org/wp-content/uploads/2026/07/cropped-learnwithexamples-icon.png?fit=32%2C32&ssl=1 Algorithms - Learn With Examples https://learnwithexamples.org/category/computer-science-concepts/algorithms/ 32 32 228207193 Big O Notation Explained with Real Examples https://learnwithexamples.org/big-o-notation-explained/ https://learnwithexamples.org/big-o-notation-explained/#respond Tue, 04 Aug 2026 17:20:21 +0000 https://learnwithexamples.org/?p=842 · Learn With Examples · Big O is not maths for its own sake. It is a one-line answer to the only question that matters when your data grows: does…

The post Big O Notation Explained with Real Examples appeared first on Learn With Examples.

]]>

· Learn With Examples ·

Big O is not maths for its own sake. It is a one-line answer to the only question that matters when your data grows: does this code get a little slower, or does it fall off a cliff?

Reading time13 min
LevelBeginner → Practical
Includes3 explorers

Here’s the moment Big O suddenly makes sense to people. I once watched a junior developer ship a feature that checked a list of orders for duplicates. It worked perfectly. Tests passed, code review passed, demo went beautifully. Three months later the same feature took nine minutes to load and brought a support queue to a standstill.

Nothing had changed in the code. The only thing that changed was the number of orders — from about 300 to about 40,000. His approach compared every order against every other order, so the work grew with the square of the list. Going 130 times bigger made it roughly 17,000 times slower.

Big O notation is how you spot that in advance, in about ten seconds, without running anything. It’s a shorthand for describing how the work an algorithm does grows as its input grows — and once you can read it, that nine-minute disaster becomes something you catch during code review instead of during an outage.

The one-paragraph version

What Big O actually says

Big O describes the growth rate of an algorithm’s work as the input size n increases. It deliberately ignores hardware, programming language, and constant factors, because those change by a factor of two or ten — while growth rates change by a factor of millions.

O(1) means the input size doesn’t matter at all. O(log n) means work barely grows. O(n) means it grows in step. O(n²) means doubling the input quadruples the work. That last one is where systems die.

The idea in one picture

Everything else in this article is detail attached to this one shape. Watch what happens to each line as you move right along the horizontal axis — that axis is your data growing over the life of your product.

input size n → operations → O(n log n) O(n²) O(n) O(log n) O(1)
The same five classes, drawn to scale. Notice that near the left edge they’re all bunched together — which is exactly why a slow algorithm looks fine in testing and only reveals itself in production.

That bunching on the left is the trap. With 20 test records, an O(n²) function and an O(n) function both return instantly. There is no observable difference. The difference only exists at scale, which means you cannot discover it by testing on small data — you have to reason about it. That reasoning is what Big O gives you.

The classes, with examples you’ll recognise

There are only six you meet regularly. Tap through them — each one has a plain-English meaning, a real piece of code, and an everyday analogy that makes the growth rate obvious.

Complexity explorer

tap a class

O(1)  Constant time

The work never changes, no matter how big the input gets. Looking up one item takes the same time in a list of ten or ten million.

Real example: fetching a value from a dictionary or hash map, reading array[500], pushing onto a stack, checking whether a number is even.

Analogy: opening a specific page in a book when you already know the page number. The book’s thickness is irrelevant.

O(log n)  Logarithmic time

Each step throws away half the remaining work. Doubling the input adds just one extra step — which is why this class stays fast at absurd scales.

Real example: binary search in a sorted array, finding a record in a balanced tree index, the depth of a database B-tree.

Analogy: the guessing game. “Is it higher or lower?” finds a number between 1 and a million in 20 guesses, because each guess halves the range.

Bars scaled to their own maximum — notice how the growth flattens out almost immediately.

O(n)  Linear time

Work grows in lockstep with the input. Twice the data, twice the time. Perfectly respectable, and often unavoidable — if you must look at every item, you cannot beat linear.

Real example: finding the largest number in an unsorted list, counting words in a document, validating every row of a CSV.

Analogy: reading every name on a guest list to find one person. A list twice as long takes twice as long.

O(n log n)  Linearithmic time

A linear pass repeated across a logarithmic number of levels. Slightly worse than linear, dramatically better than quadratic, and the practical ceiling for comparison-based sorting.

Real example: merge sort, quicksort on average, and every sort() in every standard library you have ever used.

Analogy: splitting a deck of cards in half repeatedly, then merging the piles back in order. You touch every card at each of about ten levels.

O(n²)  Quadratic time

Every item is compared against every other item. Double the input and the work quadruples. This is the class that quietly kills features six months after launch.

Real example: the duplicate-checking loop from the opening story, bubble sort, comparing every pair of records to find matches, a nested loop over the same list.

Analogy: every guest at a party shaking hands with every other guest. Ten guests means 45 handshakes; a hundred guests means 4,950.

O(2ⁿ)  Exponential time

Adding a single item doubles the work. Usable for tiny inputs and hopeless past roughly 40 items — at n = 60 you are past the age of the universe.

Real example: generating every possible subset, naive recursive Fibonacci, brute-forcing a password, solving the travelling salesman by trying every route.

Analogy: the grain-of-rice-on-a-chessboard story. Doubling per square sounds harmless until square 64 needs more rice than exists on Earth.

Why the difference is so violent

Abstract symbols don’t frighten anyone. Actual step counts do. Pick an input size below and look at how the same six classes behave on it.

Steps required, by input size

pick an n
ClassTypical operationSteps for n = 10Time at 1 billion ops/sec
O(1)Look up a key in a hash map1instant
O(log n)Binary search a sorted list3instant
O(n)Scan every item once10instant
O(n log n)A good sort33instant
O(n²)Compare every pair100instant
O(2ⁿ)Try every subset1,0241 microseconds
ClassTypical operationSteps for n = 100Time at 1 billion ops/sec
O(1)Look up a key in a hash map1instant
O(log n)Binary search a sorted list7instant
O(n)Scan every item once100instant
O(n log n)A good sort664instant
O(n²)Compare every pair10,00010 microseconds
O(2ⁿ)Try every subsetbeyond countinglonger than the universe has existed
ClassTypical operationSteps for n = 1,000Time at 1 billion ops/sec
O(1)Look up a key in a hash map1instant
O(log n)Binary search a sorted list10instant
O(n)Scan every item once1,0001 microseconds
O(n log n)A good sort9,96610 microseconds
O(n²)Compare every pair1.0 million1 ms
O(2ⁿ)Try every subsetbeyond countinglonger than the universe has existed
ClassTypical operationSteps for n = 1,000,000Time at 1 billion ops/sec
O(1)Look up a key in a hash map1instant
O(log n)Binary search a sorted list20instant
O(n)Scan every item once1.0 million1 ms
O(n log n)A good sort19.9 million20 ms
O(n²)Compare every pair1 trillion17 minutes
O(2ⁿ)Try every subsetbeyond countinglonger than the universe has existed

Swipe the table sideways on a narrow screen →

One billion operations per second is roughly a fast modern CPU doing simple work. The point is not the exact seconds — it is how violently the bottom two rows change as n grows.

Read across the O(n²) row as you switch sizes. At a thousand items it’s a million steps — a blink. At a million items it’s a trillion steps, and your feature is now a fifteen-minute job that times out. Nothing about the code changed. Only n did.

A faster computer buys you a constant factor. A better algorithm buys you a different curve. Only one of those scales.

The two rules that make Big O readable

Big O deliberately throws information away, and the two things it throws away trip up every beginner. Both rules exist for the same reason: at large n, only the fastest-growing part matters.

Rule 1 — drop the constants

An algorithm that does 3n + 12 operations is written O(n), not O(3n + 12). That looks like cheating, but consider: at a million items, 3n is three million and is a trillion. The constant 3 is noise beside that gap. And constants change anyway — a faster CPU, a better compiler, or a different language shifts them freely, while the shape of the curve does not.

Rule 2 — keep only the dominant term

An algorithm costing n² + 500n + 9000 is O(n²). At n = 10, the 500n part is actually bigger. At n = 10,000, the term is 100 million and the rest is five million — the square has swallowed everything. Big O describes where things end up, not where they start.

The honest caveat. Those discarded constants sometimes matter enormously in practice. An O(n log n) algorithm with a huge constant can lose to an O(n²) one on lists of thirty items — which is exactly why real sorting libraries switch to insertion sort for small chunks. Big O tells you what happens as data grows. It does not promise which is faster today, on your data, at your size.

How to find the Big O of code you’re looking at

You don’t need to count operations. Four patterns cover almost everything you’ll meet in ordinary application code.

What you seeWhat it meansResult
Statements one after anotherCosts add, then the biggest one winsO(a) + O(b) → the larger of the two
A loop over the inputBody runs n timesn × cost of the body
A loop inside a loopCosts multiplyUsually O(n²)
Input halves each stepOnly log₂n steps possibleO(log n)

Two things people get wrong constantly. First, two loops side by side are not quadratic — they’re n + n = 2n, which is linear. Nesting is what multiplies, not adjacency. Second, a loop with a fixed bound isn’t linear: looping 100 times regardless of input is constant work, because 100 doesn’t grow with n.

Five snippets — guess before you open

Read each one, decide, then open it. These are the shapes that actually appear in real code.

1. Checking a list for duplicates the obvious way
for i in range(len(orders)): for j in range(i + 1, len(orders)): if orders[i].id == orders[j].id: flag(orders[i])

O(n²) — a loop inside a loop over the same list. It runs about n²/2 times, and dropping the constant leaves n². This is the exact code from the opening story. The fix is a set: add each id to a set and check membership, turning it into O(n) with O(n) extra memory. Nine minutes becomes a fraction of a second.

2. Two loops, one after the other
for user in users: send_welcome(user) for user in users: log_signup(user)

O(n) — not O(n²). Sequential loops add: n + n = 2n, and the constant 2 gets dropped. Merging them into one loop makes the code twice as fast in wall-clock terms but leaves the complexity class unchanged. That distinction — real speedup, same Big O — is worth internalising.

3. Halving the search space
lo, hi = 0, len(sorted_items) - 1 while lo <= hi: mid = (lo + hi) // 2 if sorted_items[mid] == target: return mid if sorted_items[mid] < target: lo = mid + 1 else: hi = mid - 1

O(log n) — binary search. Each pass discards half of what’s left, so a billion items takes about 30 passes. The catch is the precondition: the list must already be sorted, and sorting costs O(n log n). Sorting once to search many times is a great trade; sorting once to search once is not.

4. A nested loop that isn’t quadratic
for order in orders: # n orders for tax in TAX_BANDS: # always 7 bands apply(order, tax)

O(n) — linear, despite the nesting. The inner loop’s length is fixed at 7 and never grows with the input, so the total is 7n, and the 7 is a constant. Nesting only multiplies complexity when both loops scale with the input. Judge by what grows, not by indentation.

5. The classic recursive trap
def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2)

O(2ⁿ) — each call spawns two more, so the call tree roughly doubles at every level. fib(30) makes about 1.6 million calls; fib(50) would run for days. Caching results (memoisation) collapses it to O(n), because each value is then computed exactly once. Same algorithm on paper, a difference of billions in practice.

Best, average and worst case

One algorithm can have three different complexities depending on what you feed it. Searching an unsorted list for a name: if it’s first, you’re done in one step; if it’s last or missing, you check all n.

Best case

The luckiest possible input. Mostly useless for planning — nobody guarantees you luck.

Average case

What typical data does. The most honest number for everyday performance work.

Worst case

The input designed to hurt you. This is what Big O usually reports, and what you should plan around.

The default assumption when someone says “this is O(n log n)” is worst case, unless they say otherwise. That’s deliberately pessimistic, and it’s the right default: your worst case will eventually arrive, and it tends to arrive at the busiest moment. Quicksort is the cautionary tale — O(n log n) on average, O(n²) when the pivots go badly, and the input that triggers it is the very ordinary case of already-sorted data.

Space complexity counts too

Big O describes memory as readily as time. An in-place sort uses O(1) extra space; merge sort needs a scratch array, so O(n). The duplicate-checking fix above trades memory for speed — building a set of every id costs O(n) space to save you from O(n²) time. That trade is almost always worth taking, and recognising when it’s available is most of practical optimisation.

Where this actually matters

Code review

Spotting a nested loop over two growing collections takes seconds and prevents the outage three months later.

Choosing data structures

Array lookup by index is O(1); searching an array is O(n); a hash map turns that search into O(1). Most real speedups are structure changes, not clever code.

Database work

An index converts an O(n) table scan into an O(log n) lookup. That’s the whole reason indexes exist.

Interviews

“What’s the complexity?” is asked in nearly every technical interview, and the follow-up is always “can you do better?”

Don’t over-apply it. If your list has 50 items and always will, an O(n²) loop is completely fine and probably clearer to read. Big O matters when n can grow — and the real skill is asking “how big can this get?” before optimising anything. Most performance work is wasted on code that was never going to be the bottleneck.

Check yourself

Five questions. Open each to check — the correct option is marked.

1. An algorithm takes 4n + 200 steps. What is its Big O?
  • O(4n + 200)
  • O(n)
  • O(200)
  • O(n²)

Drop constants and lower-order terms — only the growth rate survives, and this one grows linearly.

2. Two separate loops over the same list of n items. Total complexity?
  • O(n²)
  • O(n)
  • O(2n²)
  • O(log n)

Sequential loops add — n + n = 2n, which is O(n). Only nested loops multiply.

3. Which class describes binary search on a sorted array?
  • O(1)
  • O(log n)
  • O(n)
  • O(n log n)

Each comparison eliminates half the remaining candidates, so a billion items needs only about 30 steps.

4. Your O(n²) function handles 1,000 records in one second. Roughly how long for 10,000?
  • 10 seconds
  • About 100 seconds
  • 1 second
  • 3 seconds

Ten times the input means a hundred times the work, because the growth is quadratic. This is the calculation to do before shipping, not after.

5. You replace a nested-loop duplicate check with a hash set. What changed?
  • Time O(n²) → O(n), memory unchanged
  • Time O(n²) → O(n), memory O(1) → O(n)
  • Nothing changes
  • Time gets worse, memory improves

You bought a much better time complexity by spending memory on the set. Recognising that trade is most of practical optimisation.

Frequently asked questions

What is Big O notation in simple terms?

It’s a shorthand for how an algorithm’s work grows as its input grows. O(n) means the work grows in step with the data; O(n²) means doubling the data quadruples the work. It describes the shape of that growth, not a measurement in seconds.

Why do we ignore constants in Big O?

Because constants depend on hardware, language and compiler, and they change by small factors — while growth rates change by factors of millions. At a million items the difference between 3n and n is trivial; the difference between n and n² is a trillion steps.

Is O(1) always faster than O(n)?

Not necessarily at small sizes. A constant-time operation with heavy overhead can lose to a linear scan of ten items. Big O describes behaviour as n grows large, so it’s a statement about trends, not a guarantee about any particular input.

What’s a good Big O to aim for?

O(1) and O(log n) are excellent, O(n) is usually fine and often unavoidable, O(n log n) is the practical target for sorting. Treat O(n²) as a warning sign worth investigating, and anything exponential as unusable beyond tiny inputs.

Do I need Big O if I’m not doing interviews?

Yes, though you’ll use it informally. You don’t need the formal maths, but you do need the instinct that says “this loop is inside that loop, and both grow with the data” — that instinct is what stops a working feature becoming an outage once real data arrives.

The takeaway

Big O answers one question: as your data grows, does this code degrade gently or catastrophically? O(1) and O(log n) barely notice. O(n) and O(n log n) scale honestly. O(n²) and worse are fine on toy data and fatal on real data.

You don’t need to derive it formally. You need to look at a function and ask two questions: what grows here, and is anything nested inside anything else that also grows? That habit catches the overwhelming majority of real performance problems, long before they reach production.

Try it on something you wrote this week. Find your longest loop, ask what its input can realistically grow to, and check whether anything scaling sits inside it. That’s the whole practice — and it takes about thirty seconds once the six shapes above are familiar.

big o notationtime complexityalgorithmsspace complexityperformancecomputer science

The post Big O Notation Explained with Real Examples appeared first on Learn With Examples.

]]>
https://learnwithexamples.org/big-o-notation-explained/feed/ 0 842
Sorting Algorithms Compared: Bubble vs Merge vs Quick https://learnwithexamples.org/bubble-vs-merge-vs-quick/ https://learnwithexamples.org/bubble-vs-merge-vs-quick/#respond Tue, 04 Aug 2026 16:14:19 +0000 https://learnwithexamples.org/?p=834 Learn With Examples · Computer Science Three ways to put a list in order. One you’d never ship, one you can trust with anything, and one that beats them both…

The post Sorting Algorithms Compared: Bubble vs Merge vs Quick appeared first on Learn With Examples.

]]>

Learn With Examples · Computer Science

Three ways to put a list in order. One you’d never ship, one you can trust with anything, and one that beats them both — until the day it doesn’t. Here’s how each actually works, watched step by step.

Hand a deck of shuffled cards to three people and watch what they do. The first goes along the row swapping any two neighbours in the wrong order, over and over, until a full pass changes nothing. The second splits the deck in half, sorts each half, then merges the two sorted piles by repeatedly taking the smaller of the two top cards. The third picks one card, throws everything smaller to the left and everything bigger to the right, then repeats on each side.

Those three people just performed bubble sort, merge sort and quicksort. No pseudocode required — the ideas are that physical. What separates them is not cleverness but how many comparisons they need, and that difference explodes as the deck gets bigger.

This article walks through all three step by step, then answers the question that actually matters at work: which one do you reach for, and when does the popular choice betray you?

The 20-second version

Bubble sort compares neighbours and swaps them. Beautifully simple, quadratic time, useless beyond a few hundred items. Learn it, then never ship it.

Merge sort splits, sorts, and merges. Reliably O(n log n) in every case and stable, but needs extra memory the size of your list.

Quicksort partitions around a pivot. Usually the fastest of the three in practice and sorts in place, but a bad pivot can drag it down to quadratic.

Step through all three

Pick an algorithm, then tap through the numbered steps. Cyan marks a comparison, pink a value being moved, amber quicksort’s pivot. All three start from the same six numbers — count how many steps each one needs.

Step through all three

tap the numbers

Step 1 of 15 — 5 was bigger than 1 — swap them

Step 2 of 15 — 5 was bigger than 4 — swap them

Step 3 of 15 — 5 was bigger than 2 — swap them

Step 4 of 15 — 5 and 8 are already in order — leave them

Step 5 of 15 — 8 was bigger than 3 — swap them

Step 6 of 15 — 1 and 4 are already in order — leave them

Step 7 of 15 — 4 was bigger than 2 — swap them

Step 8 of 15 — 4 and 5 are already in order — leave them

Step 9 of 15 — 5 was bigger than 3 — swap them

Step 10 of 15 — 1 and 2 are already in order — leave them

Step 11 of 15 — 2 and 4 are already in order — leave them

Step 12 of 15 — 4 was bigger than 3 — swap them

Step 13 of 15 — 1 and 2 are already in order — leave them

Step 14 of 15 — 2 and 3 are already in order — leave them

Step 15 of 15 — A full pass with no swaps — the list is sorted

Step 1 of 22 — Merging the sorted pieces [1] and [4]

Step 2 of 22 — Smaller front card is 1 — write it into slot 2

Step 3 of 22 — One pile is empty — copy 4 across

Step 4 of 22 — Merging the sorted pieces [5] and [1, 4]

Step 5 of 22 — Smaller front card is 1 — write it into slot 1

Step 6 of 22 — Smaller front card is 4 — write it into slot 2

Step 7 of 22 — One pile is empty — copy 5 across

Step 8 of 22 — Merging the sorted pieces [8] and [3]

Step 9 of 22 — Smaller front card is 3 — write it into slot 5

Step 10 of 22 — One pile is empty — copy 8 across

Step 11 of 22 — Merging the sorted pieces [2] and [3, 8]

Step 12 of 22 — Smaller front card is 2 — write it into slot 4

Step 13 of 22 — One pile is empty — copy 3 across

Step 14 of 22 — One pile is empty — copy 8 across

Step 15 of 22 — Merging the sorted pieces [1, 4, 5] and [2, 3, 8]

Step 16 of 22 — Smaller front card is 1 — write it into slot 1

Step 17 of 22 — Smaller front card is 2 — write it into slot 2

Step 18 of 22 — Smaller front card is 3 — write it into slot 3

Step 19 of 22 — Smaller front card is 4 — write it into slot 4

Step 20 of 22 — Smaller front card is 5 — write it into slot 5

Step 21 of 22 — One pile is empty — copy 8 across

Step 22 of 22 — Every level merged — the list is sorted

Step 1 of 17 — Pivot is 3 — everything smaller goes left of it

Step 2 of 17 — 5 is not smaller than 3 — leave it

Step 3 of 17 — 1 is smaller than 3 — move it left

Step 4 of 17 — 4 is not smaller than 3 — leave it

Step 5 of 17 — 2 is smaller than 3 — move it left

Step 6 of 17 — 8 is not smaller than 3 — leave it

Step 7 of 17 — Pivot 3 drops into its final position, permanently

Step 8 of 17 — Pivot is 2 — everything smaller goes left of it

Step 9 of 17 — Pivot 2 drops into its final position, permanently

Step 10 of 17 — Pivot is 4 — everything smaller goes left of it

Step 11 of 17 — 5 is not smaller than 4 — leave it

Step 12 of 17 — 8 is not smaller than 4 — leave it

Step 13 of 17 — Pivot 4 drops into its final position, permanently

Step 14 of 17 — Pivot is 5 — everything smaller goes left of it

Step 15 of 17 — 8 is not smaller than 5 — leave it

Step 16 of 17 — Pivot 5 drops into its final position, permanently

Step 17 of 17 — Every pivot placed — the list is sorted

Same starting list every time: [5, 1, 4, 2, 8, 3]. Count the steps each algorithm needs — that gap is the whole argument.

Bubble sort: the one everyone learns first

Bubble sort does exactly one thing: walk the list, compare each pair of neighbours, swap them if they’re out of order. Repeat until a full pass produces no swaps at all. Large values “bubble” to the end one position per pass, which is where the name comes from.

Sort [5, 1, 4, 2] by hand:

PassComparisonList after
15 vs 1 → swap[1, 5, 4, 2]
15 vs 4 → swap[1, 4, 5, 2]
15 vs 2 → swap[1, 4, 2, 5]
21 vs 4 → keep[1, 4, 2, 5]
24 vs 2 → swap[1, 2, 4, 5]
3no swaps made[1, 2, 4, 5] ✓
// bubble sort with the early-exit optimisation function bubbleSort(a) { for (let i = 0; i < a.length - 1; i++) { let swapped = false; for (let j = 0; j < a.length - 1 - i; j++) { if (a[j] > a[j + 1]) { [a[j], a[j + 1]] = [a[j + 1], a[j]]; swapped = true; } } if (!swapped) break; // already sorted, stop early } return a; }

The cost is brutal. Every pass compares nearly every pair, and you need close to n passes, so the work grows with . Ten items cost about 45 comparisons. A thousand items cost roughly half a million. Ten thousand cost fifty million. You can feel that in a browser tab.

The one situation where bubble sort isn’t embarrassing is nearly sorted data. With the early-exit check above, an already-sorted list is verified in a single pass of n-1 comparisons — that’s O(n), better than merge sort’s best case. If you have a list where one element occasionally drifts out of place, a bubble pass is a perfectly reasonable repair. That’s a narrow niche, and insertion sort usually fills it better, but it’s real.

Why teach it at all, then? Because it makes the shape of the problem visible. Once you’ve watched bubble sort waste 20 comparisons re-checking pairs it already knows are fine, the motivation behind divide-and-conquer stops being abstract. It’s the algorithm you learn in order to want a better one.

Merge sort: split, sort, stitch

Merge sort refuses to compare distant items at all. Instead it breaks the list in half, again and again, until every piece has a single element — and a single element is sorted by definition. Then it walks back up, merging pairs of sorted pieces.

The merge step is where the magic sits, and it’s the part worth understanding physically. You have two sorted piles face up. Look at the top card of each. Take the smaller one. Repeat. Because both piles are sorted, the smallest remaining card is always one of the two you’re looking at — you never search, you never backtrack.

Merging [1, 4] and [2, 3]:

Left pileRight pileCompareOutput
[1, 4][2, 3]1 vs 2 → take 1[1]
[4][2, 3]4 vs 2 → take 2[1, 2]
[4][3]4 vs 3 → take 3[1, 2, 3]
[4][ ]right empty → copy 4[1, 2, 3, 4]

Count the levels: halving a list of 1,000 takes about 10 splits to reach single items, because 2¹⁰ is 1,024. Each level does roughly n comparison work to merge everything back. Ten levels × a thousand items ≈ 10,000 operations, against bubble sort’s half a million. That’s the entire argument for O(n log n), and it’s why merge sort’s worst case, best case and average case are all the same — the splitting doesn’t care what the data looks like.

Two properties make merge sort the professional’s safe choice:

It’s stable

Equal items keep their original relative order. Sort orders by date, then by customer, and the date order survives inside each customer. Quicksort does not promise this.

It’s predictable

No input can make it slow. For latency-sensitive systems, a guaranteed ceiling beats a lower average with a nasty tail.

It works off-disk

Merging needs only the front of each pile, so you can sort a 500 GB file with 8 GB of RAM. This is how external sorting works.

It costs memory

The standard version needs a scratch array as big as the input. On memory-tight systems, that’s the dealbreaker.

Quicksort: pick a pivot, throw everything to one side

Quicksort also divides and conquers, but it partitions before recursing rather than after. Choose one element as the pivot. Rearrange the list so everything smaller sits left of it and everything larger sits right. The pivot is now in its final position, permanently. Then repeat on the left chunk and the right chunk.

Take [7, 2, 9, 4, 5] with 5 as the pivot. Walk the rest: 7 is bigger (right), 2 is smaller (left), 9 is bigger (right), 4 is smaller (left). You get [2, 4] 5 [7, 9]. One pass, and 5 is done forever. Now solve the two small pieces the same way.

// quicksort, Lomuto partition scheme function quickSort(a, lo = 0, hi = a.length - 1) { if (lo >= hi) return a; const pivot = a[hi]; let i = lo; for (let j = lo; j < hi; j++) { if (a[j] < pivot) { [a[i], a[j]] = [a[j], a[i]]; i++; } } [a[i], a[hi]] = [a[hi], a[i]]; // pivot into place quickSort(a, lo, i - 1); quickSort(a, i + 1, hi); return a; }

On average the pivot lands somewhere near the middle, the problem halves each time, and you get O(n log n) — typically with a smaller constant factor than merge sort, because quicksort swaps within the original array instead of copying into a scratch one. Fewer memory writes, better cache behaviour, faster in the real world.

The trap. Suppose you always pick the last element as pivot, and the list is already sorted. Every pivot is the largest remaining value, so one side gets everything and the other gets nothing. You’ve turned O(n log n) into O(n²) — and the input that triggers it is the most common input in the world: data that’s already in order. Real implementations dodge this by picking a random pivot, or the median of the first, middle and last elements.

Race them on the same data

Same shuffled array, three algorithms, one operation per tick. This isn’t a wall-clock benchmark — it’s an operations count, which is what the big-O notation is actually measuring.

Head-to-head race

tick to start
Bubble ~1,800 comparisons
Merge ~300 comparisons
Quick ~250 comparisons

Each bar advances at a speed proportional to the work its algorithm really does on a shuffled 60-item list. Quick and merge finish while bubble sort is still grinding through its second pass.

On random data quicksort usually finishes first, merge close behind, bubble far back. Hand all three an already-sorted list, though, and the ranking inverts completely: bubble sort exits after a single clean pass, while quicksort with a naive last-element pivot collapses into its worst case. Same three algorithms, opposite result, purely because the input changed shape.

Big-O tells you how an algorithm behaves as data grows. It doesn’t tell you which one wins on your data. Only the shape of your input decides that.

The numbers, side by side

BubbleMergeQuick
Best caseO(n)O(n log n)O(n log n)
AverageO(n²)O(n log n)O(n log n)
Worst caseO(n²)O(n log n)O(n²)
Extra memoryO(1)O(n)O(log n)
Stable?YesYesNo
In place?YesNoYes
Use it whenTeaching, or tiny nearly-sorted listsYou need guarantees, stability, or external sortingYou want raw speed on in-memory data

Those symbols get abstract fast, so put real numbers on them. Pick a list size and watch the gap between quadratic and logarithmic growth open up.

How bad does n² get?

pick a list size
45Bubble ops (n²)
33Merge / quick ops
1.4×Times more work
instantBubble time est.
5,000Bubble ops (n²)
664Merge / quick ops
7.5×Times more work
under 1 msBubble time est.
500,000Bubble ops (n²)
9,966Merge / quick ops
50×Times more work
5 msBubble time est.
5 billionBubble ops (n²)
1,660,964Merge / quick ops
3,010×Times more work
50 secondsBubble time est.

Notice how the ratio behaves. At 100 items bubble sort does around 15 times more work — annoying, survivable. At 100,000 items it does over 3,000 times more. That is the difference between a page that renders instantly and one that hangs the browser for a minute. Complexity classes don’t matter much at small scale and matter enormously at large scale, which is exactly why beginners under-rate them.

So which one do you actually use?

Honest answer for day-to-day work: call your language’s built-in sort. It’s been tuned by people who do nothing else. But knowing what’s under it tells you when to override it.

Sorting objects by two fields

You need stability — merge sort, or a built-in that guarantees it. Sort by the secondary key first, then the primary.

Big array, memory is tight

Quicksort. In-place, cache-friendly, only recursion stack overhead. Just randomise the pivot.

Data bigger than RAM

Merge sort. It’s the only one of the three that sorts chunks on disk and merges streams.

Worst case must be bounded

Merge sort. Real-time and latency-critical systems care about the ceiling, not the average.

Fewer than ~20 items

Use insertion sort. Its overhead is so low it beats the clever algorithms at small sizes — which is why real implementations switch to it below a threshold.

Explaining sorting to someone

Bubble sort, once. Then show them the step-through above and let the operation count make the argument.

What most standard libraries actually run is a hybrid. Introsort starts as quicksort, counts its recursion depth, and switches to heapsort if the pivots are going badly — giving quicksort’s speed with a guaranteed O(n log n) ceiling. Timsort, used in Python and Java for objects, is merge sort that detects runs of already-ordered data and exploits them, which makes it startlingly fast on the semi-sorted data that real applications produce. Both are engineering answers to the trade-offs on this page.

Check yourself

Five questions. Open each one to check yourself — the correct option is marked.

1. Why is merge sort’s worst case the same as its best case?
  • It checks the data first
  • The splitting is fixed and doesn’t depend on the values
  • It uses extra memory
  • It’s stable

Merge sort always halves the list, whatever’s in it. The number of levels is fixed at log₂n, so no input can make it slow.

2. Which input makes naive quicksort hit its worst case?
  • Random data
  • Data with many duplicates only
  • Already-sorted data with a last-element pivot
  • Very short lists

Every pivot becomes the largest remaining value, so one partition gets everything. The fix is a random or median-of-three pivot.

3. What does “stable” mean for a sorting algorithm?
  • Equal items keep their original relative order
  • It never crashes
  • It always takes the same time
  • It uses no extra memory

Stability lets you sort by one field, then another, and keep the first ordering inside groups. Quicksort doesn’t guarantee it.

4. Roughly how many comparisons does bubble sort need for 1,000 items?
  • About 1,000
  • About 10,000
  • About 500,000
  • About 1,000,000

Roughly n²/2 — half a million. Merge sort handles the same list in about 10,000.

5. You must sort a 400 GB log file on a machine with 16 GB of RAM. Which approach?
  • Quicksort, it’s in place
  • Merge sort, sorting chunks then merging streams
  • Bubble sort, it uses no extra memory
  • None can do it

External merge sort only needs the front of each sorted run in memory at a time — the classic solution to sorting more data than you can hold.

Frequently asked questions

Which sorting algorithm is fastest?

For general in-memory data, quicksort is usually fastest in practice because it sorts in place with excellent cache behaviour. Merge sort is faster in the worst case, since quicksort can degrade to O(n²) with bad pivots. Bubble sort is slowest by a wide margin at any meaningful size.

Is bubble sort ever useful in real code?

Rarely, but not never. On a list that’s already nearly sorted, the early-exit version runs in O(n) and is trivial to write and verify. For anything else, insertion sort does the same job better and your language’s built-in sort beats both.

Why does quicksort beat merge sort if merge sort has a better worst case?

Constant factors. Quicksort swaps elements inside the original array; merge sort copies into a scratch array and back on every level. Fewer memory writes and better cache locality mean quicksort typically wins on wall-clock time even when both are O(n log n).

What does O(n log n) actually mean in plain terms?

The work grows a little faster than the list size, but nowhere near as fast as squaring it. Double the items and you do slightly more than double the work — whereas O(n²) means doubling the items quadruples the work. That gap is what makes large-scale sorting feasible at all.

Which sort do Python, Java and JavaScript use?

Python uses Timsort, a merge sort variant that detects existing sorted runs. Java uses Timsort for objects and a dual-pivot quicksort for primitives. Most JavaScript engines use Timsort-style stable sorts for Array.sort(). All three are hybrids built from the ideas on this page.

The takeaway

Bubble sort compares neighbours and pays for it quadratically. Merge sort splits the problem into halves whose cost adds up to n log n, every single time, at the price of extra memory. Quicksort partitions around a pivot and is usually the fastest of the three — as long as the pivot is chosen sensibly.

The deeper lesson isn’t which one wins. It’s that the same task can be organised in ways whose costs diverge by a factor of thousands, and that the winning approach depends on the shape of your data, not on the elegance of the code. Step back through the three walkthroughs above and count what each one needed on the very same six numbers — then imagine those gaps at a million items. That is the thing worth remembering.

bubble sortmerge sortquicksortbig-Oalgorithmsdata structures

The post Sorting Algorithms Compared: Bubble vs Merge vs Quick appeared first on Learn With Examples.

]]>
https://learnwithexamples.org/bubble-vs-merge-vs-quick/feed/ 0 834
How Binary Search Works — with Visual Examples https://learnwithexamples.org/how-binary-search-works-visual-examples/ https://learnwithexamples.org/how-binary-search-works-visual-examples/#respond Wed, 15 Jul 2026 16:10:39 +0000 https://learnwithexamples.org/?p=736 How Binary Search Works — with Visual Examples Algorithms · Beginner Binary search is one of the most elegant algorithms ever written. It finds any value in a sorted list…

The post How Binary Search Works — with Visual Examples appeared first on Learn With Examples.

]]>
How Binary Search Works — with Visual Examples

Algorithms · Beginner

Binary search is one of the most elegant algorithms ever written. It finds any value in a sorted list of a million items in just 20 steps. This guide shows you exactly how — with live visualizations, code in 3 languages, and a speed comparison you can feel.

📖 15 min read 🎮 3 interactive demos 💻 Python · JS · Java ❓ Quiz at the end

The Problem Binary Search Solves

Imagine you have a sorted list of 1,000,000 numbers and you need to find whether the number 742,891 is in it. The naive approach — checking every single element one by one — would take up to a million comparisons in the worst case.

Binary search solves this in at most 20 comparisons. Not 20,000. Not 2,000. Twenty. That’s the power of dividing the problem in half at every step.

Linear Search 1,000,000 steps
Binary Search 20 steps

📌 One Requirement

Binary search only works on a sorted array. This is the most important rule. We’ll revisit why at the end.

The Phone Book Intuition

You’ve already used binary search in real life. When you look up a name in a phone book (or a word in a dictionary), you don’t start from page 1 and flip forward.

You open to the middle. If the name you want comes alphabetically before the middle page, you throw away the right half and repeat on the left. If it comes after, you throw away the left half and repeat on the right. Each time, you eliminate half the remaining possibilities.

💡 Key Insight

Every comparison eliminates half of the remaining elements. That’s why 1,000,000 items only needs log₂(1,000,000) ≈ 20 steps. The algorithm grows with the logarithm of the input, not the input itself.

How It Works — Step by Step

Binary search uses three pointers on the array: low, mid, and high. Here’s the algorithm:

1

Set low = 0, high = last index

Start with the full array in scope. Low points to the first element, high points to the last.

2

Calculate mid = (low + high) / 2

Find the middle index. In Python/Java use integer division: (low + high) // 2 to avoid float issues.

3

Compare array[mid] with target

Three possible outcomes: equal (found it! return mid), target is smaller (search left half: high = mid − 1), target is larger (search right half: low = mid + 1).

4

Repeat until found or low > high

If low exceeds high, the target is not in the array — return −1 (or null/None depending on your language).

⚠️ Integer Overflow Tip

In languages like Java/C++, computing (low + high) / 2 can overflow for very large arrays. The safe formula is: low + (high - low) / 2.

Live Visualizer — Try It Yourself

Enter any number between 1 and 99 and watch binary search find it step by step. The yellow cell is the current midpoint being checked, grey cells are eliminated, and green means found.

  Binary Search Visualizer
Press ▶ Search to start, or Step → to go one step at a time.

Code in Python, JavaScript & Java

Python

Python
def binary_search(arr, target):
    low, high = 0, len(arr) - 1

    while low <= high:
        mid = low + (high - low) // 2  # safe from integer overflow

        if arr[mid] == target:
            return mid              # found! return the index
        elif arr[mid] < target:
            low = mid + 1           # target is in right half
        else:
            high = mid - 1          # target is in left half

    return -1                       # not found


# Example usage
numbers = [2, 7, 13, 19, 25, 34, 37, 46, 58, 72]
result = binary_search(numbers, 37)
print(f"Found at index: {result}")  # → Found at index: 6
print(binary_search(numbers, 99))   # → -1 (not found)

JavaScript

JavaScript
function binarySearch(arr, target) {
  let low = 0;
  let high = arr.length - 1;

  while (low <= high) {
    const mid = low + Math.floor((high - low) / 2);

    if (arr[mid] === target)  return mid;      // found
    if (arr[mid] < target)   low = mid + 1;   // search right
    else                       high = mid - 1;  // search left
  }

  return -1; // not found
}

// Example
const nums = [2, 7, 13, 19, 25, 34, 37, 46, 58, 72];
console.log(binarySearch(nums, 37));  // 6
console.log(binarySearch(nums, 99));  // -1

Java

Java
public class BinarySearch {

    public static int binarySearch(int[] arr, int target) {
        int low = 0;
        int high = arr.length - 1;

        while (low <= high) {
            int mid = low + (high - low) / 2; // avoids overflow

            if (arr[mid] == target)  return mid;
            if (arr[mid] <  target)  low  = mid + 1;
            else                       high = mid - 1;
        }

        return -1; // not found
    }

    public static void main(String[] args) {
        int[] nums = {2, 7, 13, 19, 25, 34, 37, 46, 58, 72};
        System.out.println(binarySearch(nums, 37)); // 6
        System.out.println(binarySearch(nums, 99)); // -1
    }
}

Time & Space Complexity

Binary search is one of the most efficient search algorithms. Its performance is measured using Big O notation.

Case Time Complexity What it means
Best Case O(1) Target is at the midpoint on the first check
Average Case O(log n) Halves the search space each iteration
Worst Case O(log n) Target not found after exhausting all halves
Space (iterative) O(1) Only stores low, mid, high — no extra memory
Space (recursive) O(log n) Call stack grows with each recursive call

📐 The log n explained

log₂(n) is the number of times you can halve n before reaching 1. For n = 1,024 → log₂(1024) = 10 steps. For n = 1,048,576 (1 million) → just 20 steps. Doubling the input only adds one more step.

Binary vs Linear Search — The Speed Race

Drag the slider to set the array size and watch how many steps each algorithm needs. The difference becomes dramatic very quickly.

  Speed Comparison Demo
1,000
Linear
500 steps
Binary
10 steps

* Linear shows average-case (n/2). Binary shows worst-case (log₂n). Both searching the same array.

The One Rule You Must Not Break

Binary search requires a sorted array. This is non-negotiable. Here’s why: when the algorithm looks at the midpoint and finds the target is smaller, it assumes everything to the right is also larger. In an unsorted array, that assumption is false — and the algorithm silently gives wrong answers.

Python — What goes wrong with unsorted input
# ❌ WRONG — unsorted array
unsorted = [37, 2, 72, 13, 25]
binary_search(unsorted, 2)   # returns -1 even though 2 is there!

# ✅ CORRECT — sort first
sorted_arr = sorted(unsorted)  # [2, 13, 25, 37, 72]
binary_search(sorted_arr, 2)  # returns 0 ✓

# Tip: Python has bisect module built-in for production use
import bisect
idx = bisect.bisect_left(sorted_arr, 2)
print(sorted_arr[idx] == 2)    # True

⛔ Common Mistake

Sorting takes O(n log n) time. If you’re only searching once, sorting + binary search is slower than linear search. Binary search pays off when you search the same sorted data many times — the sort cost is paid once, searches are O(log n) forever.

Where Binary Search Is Used in Real Life

Binary search isn’t just a textbook exercise. It runs inside software you use every day.

🗄️

Database Indexes

B-trees (the data structure behind MySQL, PostgreSQL indexes) use a generalized form of binary search to find rows in milliseconds across millions of records.

📦

Package Managers

npm, pip, and apt use binary search on sorted version lists to find compatible package versions quickly.

🎮

Game Development

Finding which tile a player is on, collision detection bounds, and sorted leaderboard lookups all use binary search variants.

🔤

Spell Checkers

Dictionaries are sorted. When you type a word, spell checkers run binary search on the dictionary to verify it in O(log n) time.

📡

Git Bisect

Git’s git bisect command uses binary search through your commit history to find which commit introduced a bug.

🌐

IP Routing

Routers use binary search on sorted prefix tables to find the right network path for packets — billions of times per second.

Knowledge Quiz

Five questions to lock in what you’ve learned.

  Binary Search Quiz
Question 1 of 5

The post How Binary Search Works — with Visual Examples appeared first on Learn With Examples.

]]>
https://learnwithexamples.org/how-binary-search-works-visual-examples/feed/ 0 736
Top 10 Algorithms Every Programmer Should Know https://learnwithexamples.org/top-10-algorithms-every-programmer-should-know/ https://learnwithexamples.org/top-10-algorithms-every-programmer-should-know/#respond Fri, 20 Jun 2025 16:09:33 +0000 https://learnwithexamples.org/?p=448 Also check: Understanding the Magic Behind Computers

The post Top 10 Algorithms Every Programmer Should Know appeared first on Learn With Examples.

]]>
Top 10 Algorithms Every Programmer Should Know

Master the Fundamentals

Algorithms are the backbone of computer science and programming. Understanding these fundamental algorithms will make you a better programmer, improve your problem-solving skills, and help you excel in technical interviews. This comprehensive guide covers the top 10 algorithms every programmer should master, complete with interactive examples and detailed explanations.

1. Binary Search Algorithm

Binary search is one of the most efficient searching algorithms for sorted arrays. It works by repeatedly dividing the search interval in half, comparing the target value with the middle element, and eliminating half of the remaining elements in each iteration.

How Binary Search Works:

  1. Start with the entire sorted array
  2. Find the middle element
  3. If the middle element equals the target, return its index
  4. If the target is less than the middle element, search the left half
  5. If the target is greater than the middle element, search the right half
  6. Repeat until the element is found or the array is exhausted
function binarySearch(arr, target) { let left = 0; let right = arr.length - 1; while (left <= right) { let mid = Math.floor((left + right) / 2); if (arr[mid] === target) { return mid; } else if (arr[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return -1; // Element not found }

Interactive Binary Search Demo

Time Complexity Space Complexity Best Case Worst Case
O(log n) O(1) O(1) O(log n)

2. Quick Sort Algorithm

Quick Sort is a highly efficient divide-and-conquer sorting algorithm. It works by selecting a 'pivot' element and partitioning the other elements into two sub-arrays according to whether they are less than or greater than the pivot.

Key Insight: Quick Sort's average-case performance is excellent, making it one of the most popular sorting algorithms in practice.
function quickSort(arr, low = 0, high = arr.length - 1) { if (low < high) { let pivotIndex = partition(arr, low, high); quickSort(arr, low, pivotIndex - 1); quickSort(arr, pivotIndex + 1, high); } return arr; } function partition(arr, low, high) { let pivot = arr[high]; let i = low - 1; for (let j = low; j < high; j++) { if (arr[j] < pivot) { i++; [arr[i], arr[j]] = [arr[j], arr[i]]; } } [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]]; return i + 1; }

Interactive Quick Sort Demo

Average Case Best Case Worst Case Space Complexity
O(n log n) O(n log n) O(n²) O(log n)

3. Merge Sort Algorithm

Merge Sort is a stable, divide-and-conquer algorithm that divides the array into halves, sorts them separately, and then merges them back together. It guarantees O(n log n) time complexity in all cases.

function mergeSort(arr) { if (arr.length <= 1) { return arr; } const mid = Math.floor(arr.length / 2); const left = mergeSort(arr.slice(0, mid)); const right = mergeSort(arr.slice(mid)); return merge(left, right); } function merge(left, right) { let result = []; let leftIndex = 0; let rightIndex = 0; while (leftIndex < left.length && rightIndex < right.length) { if (left[leftIndex] < right[rightIndex]) { result.push(left[leftIndex]); leftIndex++; } else { result.push(right[rightIndex]); rightIndex++; } } return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex)); }

Interactive Merge Sort Demo

4. Depth-First Search (DFS)

DFS is a graph traversal algorithm that explores as far as possible along each branch before backtracking. It can be implemented using recursion or an explicit stack.

// Recursive DFS for adjacency list representation function dfsRecursive(graph, node, visited = new Set()) { visited.add(node); console.log(node); for (let neighbor of graph[node] || []) { if (!visited.has(neighbor)) { dfsRecursive(graph, neighbor, visited); } } } // Iterative DFS using stack function dfsIterative(graph, startNode) { const visited = new Set(); const stack = [startNode]; while (stack.length > 0) { const node = stack.pop(); if (!visited.has(node)) { visited.add(node); console.log(node); for (let neighbor of graph[node] || []) { if (!visited.has(neighbor)) { stack.push(neighbor); } } } } }

Interactive DFS Demo

Graph representation: A → [B, C], B → [D, E], C → [F], D → [], E → [F], F → []

5. Breadth-First Search (BFS)

BFS explores all vertices at the current depth before moving to vertices at the next depth level. It uses a queue data structure and is particularly useful for finding the shortest path in unweighted graphs.

function bfs(graph, startNode) { const visited = new Set(); const queue = [startNode]; const result = []; visited.add(startNode); while (queue.length > 0) { const node = queue.shift(); result.push(node); for (let neighbor of graph[node] || []) { if (!visited.has(neighbor)) { visited.add(neighbor); queue.push(neighbor); } } } return result; } // BFS for shortest path function bfsShortestPath(graph, start, target) { const queue = [[start, [start]]]; const visited = new Set([start]); while (queue.length > 0) { const [node, path] = queue.shift(); if (node === target) { return path; } for (let neighbor of graph[node] || []) { if (!visited.has(neighbor)) { visited.add(neighbor); queue.push([neighbor, [...path, neighbor]]); } } } return null; // No path found }

Interactive BFS Demo

6. Dynamic Programming - Fibonacci Sequence

Dynamic Programming is an optimization technique that solves complex problems by breaking them down into simpler subproblems. The Fibonacci sequence is a classic example where DP can dramatically improve performance.

// Naive recursive approach - O(2^n) function fibonacciNaive(n) { if (n <= 1) return n; return fibonacciNaive(n - 1) + fibonacciNaive(n - 2); } // Dynamic Programming approach - O(n) function fibonacciDP(n) { if (n <= 1) return n; const dp = [0, 1]; for (let i = 2; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; } // Space-optimized DP - O(1) space function fibonacciOptimized(n) { if (n <= 1) return n; let prev = 0, curr = 1; for (let i = 2; i <= n; i++) { const temp = curr; curr = prev + curr; prev = temp; } return curr; }

Interactive Fibonacci Demo

7. Dijkstra's Shortest Path Algorithm

Dijkstra's algorithm finds the shortest path between nodes in a weighted graph. It's widely used in networking protocols, GPS navigation, and social networking applications.

function dijkstra(graph, start) { const distances = {}; const visited = new Set(); const previous = {}; // Initialize distances for (let node in graph) { distances[node] = node === start ? 0 : Infinity; previous[node] = null; } while (visited.size < Object.keys(graph).length) { // Find unvisited node with minimum distance let minNode = null; for (let node in distances) { if (!visited.has(node) && (minNode === null || distances[node] < distances[minNode])) { minNode = node; } } if (minNode === null || distances[minNode] === Infinity) break; visited.add(minNode); // Update distances to neighbors for (let neighbor in graph[minNode]) { const distance = distances[minNode] + graph[minNode][neighbor]; if (distance < distances[neighbor]) { distances[neighbor] = distance; previous[neighbor] = minNode; } } } return { distances, previous }; }

Interactive Dijkstra's Demo

Sample graph: A→B(4), A→C(2), B→C(1), B→D(5), C→D(8), C→E(10), D→E(2)

8. Hash Table Implementation

Hash tables provide average O(1) time complexity for insertions, deletions, and lookups. They use a hash function to map keys to array indices, making them incredibly efficient for many operations.

class HashTable { constructor(size = 10) { this.size = size; this.buckets = new Array(size).fill(null).map(() => []); } hash(key) { let hash = 0; for (let i = 0; i < key.length; i++) { hash += key.charCodeAt(i); } return hash % this.size; } set(key, value) { const index = this.hash(key); const bucket = this.buckets[index]; // Check if key already exists for (let i = 0; i < bucket.length; i++) { if (bucket[i][0] === key) { bucket[i][1] = value; return; } } // Add new key-value pair bucket.push([key, value]); } get(key) { const index = this.hash(key); const bucket = this.buckets[index]; for (let [k, v] of bucket) { if (k === key) return v; } return undefined; } delete(key) { const index = this.hash(key); const bucket = this.buckets[index]; for (let i = 0; i < bucket.length; i++) { if (bucket[i][0] === key) { bucket.splice(i, 1); return true; } } return false; } }

Interactive Hash Table Demo

9. Binary Tree Traversal

Binary tree traversal algorithms are fundamental for working with tree data structures. The three main traversal methods are in-order, pre-order, and post-order traversal.

class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } } // In-order traversal (Left, Root, Right) function inorderTraversal(root, result = []) { if (root !== null) { inorderTraversal(root.left, result); result.push(root.val); inorderTraversal(root.right, result); } return result; } // Pre-order traversal (Root, Left, Right) function preorderTraversal(root, result = []) { if (root !== null) { result.push(root.val); preorderTraversal(root.left, result); preorderTraversal(root.right, result); } return result; } // Post-order traversal (Left, Right, Root) function postorderTraversal(root, result = []) { if (root !== null) { postorderTraversal(root.left, result); postorderTraversal(root.right, result); result.push(root.val); } return result; }

Interactive Binary Tree Traversal Demo

Sample tree structure:

1
2      3
4 5   6 7

10. Two Pointers Technique

The two pointers technique is a powerful algorithmic approach used to solve array and string problems efficiently. It involves using two pointers that move through the data structure to find a solution.

// Two Sum - Sorted Array function twoSumSorted(numbers, target) { let left = 0; let right = numbers.length - 1; while (left < right) { const sum = numbers[left] + numbers[right]; if (sum === target) { return [left, right]; } else if (sum < target) { left++; } else { right--; } } return [-1, -1]; } // Remove Duplicates from Sorted Array function removeDuplicates(nums) { if (nums.length === 0) return 0; let slow = 0; for (let fast = 1; fast < nums.length; fast++) { if (nums[fast] !== nums[slow]) { slow++; nums[slow] = nums[fast]; } } return slow + 1; } // Palindrome Check function isPalindrome(s) { s = s.toLowerCase().replace(/[^a-z0-9]/g, ''); let left = 0; let right = s.length - 1; while (left < right) { if (s[left] !== s[right]) { return false; } left++; right--; } return true; }

Interactive Two Pointers Demo

Conclusion

Mastering these 10 fundamental algorithms will significantly improve your programming skills and problem-solving abilities. Each algorithm has its unique strengths and use cases:

  • Binary Search: Efficient searching in sorted data
  • Quick Sort & Merge Sort: Fast and reliable sorting algorithms
  • DFS & BFS: Essential graph traversal techniques
  • Dynamic Programming: Optimization for overlapping subproblems
  • Dijkstra's Algorithm: Shortest path in weighted graphs
  • Hash Tables: Fast data retrieval and storage
  • Tree Traversal: Systematic exploration of tree structures
  • Two Pointers: Efficient array and string manipulation

Regular practice with these algorithms will help you recognize patterns in complex problems and choose the most appropriate solution approach. Remember that understanding the underlying principles is more important than memorizing the code – focus on when and why to use each algorithm.

Continue practicing these algorithms with different variations and edge cases. The interactive examples provided here are just the beginning – try implementing these algorithms in your preferred programming language and experiment with different inputs to deepen your understanding.

Also check: Understanding the Magic Behind Computers

The post Top 10 Algorithms Every Programmer Should Know appeared first on Learn With Examples.

]]>
https://learnwithexamples.org/top-10-algorithms-every-programmer-should-know/feed/ 0 448
A Beginner’s Guide to Understanding the Magic Behind Computers – Algorithms https://learnwithexamples.org/lets-learn-algorithm/ https://learnwithexamples.org/lets-learn-algorithm/#respond Sat, 27 Jan 2024 08:02:50 +0000 https://learnwithexamples.org/?p=26 Welcome to the enchanting world of algorithms, the secret sauce that powers the digital realm! If you’ve ever wondered how computers make decisions, solve problems, or perform seemingly complex tasks,…

The post A Beginner’s Guide to Understanding the Magic Behind Computers – Algorithms appeared first on Learn With Examples.

]]>

Welcome to the enchanting world of algorithms, the secret sauce that powers the digital realm! If you’ve ever wondered how computers make decisions, solve problems, or perform seemingly complex tasks, you’re in for a treat. In this whimsical journey, we’ll embark on an adventure to demystify algorithms for beginners, using simple examples that will make you the hero of your own coding story.

Chapter 1: The Quest Begins – What is an Algorithm?

Our tale begins with a simple question: What is an algorithm? Imagine you’re in a magical kitchen, trying to bake a cake. An algorithm, in its essence, is nothing more than a step-by-step recipe for achieving a specific goal. Just as a recipe guides you through the process of creating a delicious cake, an algorithm guides a computer through a series of steps to accomplish a task.

Chapter 2: The Fairy Tale of Sorting – Bubble Sort

As our journey unfolds, we encounter the fairy tale of sorting. Picture a row of enchanted books in a library, each with a unique story. Bubble Sort, our magical librarian, wants to organize them in alphabetical order. Here’s how the spell works:

  1. Start at the beginning of the row.
  2. Compare the first two books.
  3. If they are in the correct order, move to the next pair. If not, swap them.
  4. Repeat until the entire row is sorted.

In this whimsical dance, Bubble Sort keeps comparing and swapping until the books find their rightful place. While this sorting method may seem charming, it’s not the most efficient for large collections of books.

Chapter 3: The Maze of Searching – Binary Search

Now, let’s delve into the mysterious maze of searching with Binary Search. Imagine you’re in a magical forest with countless doors. Behind one of them lies the treasure you seek. Binary Search is your guide:

  1. Start at the middle door.
  2. If the treasure is behind that door, rejoice! If not, narrow your search to the left or right half, depending on whether the treasure is smaller or larger.
  3. Repeat until you find the treasure.

Binary Search cuts the possibilities in half with each attempt, making it a swift and efficient guide through the magical forest of information.

Chapter 4: The Enchanted Garden of Recursion – Factorial

As our adventure continues, we stumble upon an enchanted garden where the concept of recursion blooms like mystical flowers. Consider calculating the factorial of a number, an enchanting mathematical trick:

  1. If the number is 0 or 1, the factorial is 1.
  2. Otherwise, the factorial is the number multiplied by the factorial of the number minus 1.

This recursive dance continues until we reach the base case of 0 or 1, unraveling the magic of Factorial in the garden of numbers.

Chapter 5: The Puzzle of Greedy Algorithms – Knapsack Problem

In the heart of the algorithmic kingdom, we encounter a challenging puzzle known as the Knapsack Problem. Imagine you’re a treasure hunter, faced with a collection of treasures each with its own weight and value. Your goal is to maximize the value of the treasures you carry in your magical knapsack, but there’s a weight limit. Enter Greedy Algorithms, your trusty companions:

  1. Sort the treasures by their value-to-weight ratio.
  2. Add treasures to the knapsack in order until it’s full.

While Greedy Algorithms may not always find the absolute best solution, they offer a quick and practical approach to the Knapsack Problem.

Chapter 6: The Labyrinth of Dynamic Programming – Fibonacci Sequence

Our journey takes us through the labyrinth of Dynamic Programming, where we unravel the mystery of the Fibonacci sequence. Picture a magical staircase, and you want to know how many ways you can climb it. Dynamic Programming provides the answer:

  1. If there’s only one step, there’s only one way to climb.
  2. If there are two steps, there are two ways: climb one step twice or take two steps at once.
  3. For more steps, each step can be reached by adding the ways to reach the previous two steps.

Dynamic Programming breaks down complex problems into simpler subproblems, making the labyrinth of algorithms more manageable.


As our algorithmic adventure comes to a close, we’ve explored the enchanting world of algorithms through the lens of fairy tales and magical scenarios. From the sorting spells of Bubble Sort to the treasure hunts with Binary Search, from the recursive dances of Factorial to the strategic companionship of Greedy Algorithms and the labyrinthine wisdom of Dynamic Programming – each algorithm tells a unique story.

Remember, dear reader, algorithms are not mere lines of code; they are the enchanting tales that guide computers through the magical realm of problem-solving. Embrace the magic, let your curiosity soar, and may your coding adventures be filled with wonder and discovery!

For more learning articles keep visiting Learn with examples

The post A Beginner’s Guide to Understanding the Magic Behind Computers – Algorithms appeared first on Learn With Examples.

]]>
https://learnwithexamples.org/lets-learn-algorithm/feed/ 0 26