The post Rabin-Karp: Searching With Rolling Fingerprints appeared first on Learn With Examples.
]]>Most string searches work by comparing letters. Rabin-Karp takes a different road: it boils every chunk of text down to a single number, a fingerprint, and compares the numbers instead. The clever part is that the fingerprint of the next chunk can be worked out from the previous one in a couple of arithmetic steps, so sliding along a million-letter document never requires re-reading anything. That small trick turns out to be behind plagiarism checkers, backup tools that avoid storing duplicate data, and filters that scan for thousands of forbidden words at once. This guide builds the idea from scratch, using small numbers you can follow by hand, and backs every claim with code I actually ran.
Imagine you are a security guard checking faces against a wanted poster. You could memorise the poster and study every visitor’s eyes, nose, jaw and hairline one feature at a time. Or you could take a quick glance at each visitor and ask a much cheaper question first: does this person even roughly resemble the photo? Ninety-nine visitors fail that glance instantly. For the rare one who passes, then you do the careful feature-by-feature check.
That is Rabin-Karp in one paragraph. The glance is a hash, a number computed from the text. The careful check is the ordinary letter-by-letter comparison. The algorithm is named after its two inventors, the computer scientists Michael Rabin and Richard Karp, who published it in 1987, and its central idea has aged remarkably well.
The short version. Turn the needle into a number. Turn every window of the haystack into a number too, using a formula that lets you update the number in constant time as the window slides by one letter. Compare numbers, which is fast. Only when two numbers match do you double-check the real letters, because different texts can occasionally share a fingerprint.
Computers love numbers and are slow at comparing long strings, so let us convert. The easiest example uses digits, because a string of digits already is a number. The text “31415” becomes the number thirty-one thousand four hundred fifteen. Two strings of digits are equal exactly when their numbers are equal, and comparing two numbers is one operation no matter how many digits are inside.
That already sounds like a win, but there is a catch. With a needle 50 letters long, the number can be enormous, far too big to handle in one machine word. The fix is to keep only the remainder after dividing by some fixed number, the modulus. In my first example I use 13, which is tiny on purpose so that you can do the arithmetic in your head. The number 31415 divided by 13 leaves a remainder of 7. So the fingerprint of “31415” is 7.
Notice what we gave up. Many different strings leave the same remainder: 7 is also the fingerprint of other five-digit numbers. We will deal with that in a moment, because it is the whole reason the algorithm has a verification step. First, the clever bit.
To search a long text, you need the fingerprint of every window of five consecutive digits. Computing each one from scratch would cost five operations per window, which is no better than comparing the digits directly. The rolling hash avoids that by reusing the previous answer.
Take the window “31415” and slide one place to the right so that it becomes “14152”. Three things happened: the leading 3 fell off the left, every remaining digit shifted one place up, and a new 2 arrived at the right. In arithmetic terms:
Check it with real numbers: (31415 − 3 × 10000) × 10 + 2 = 1415 × 10 + 2 = 14152. Correct, and it took three arithmetic operations, regardless of how long the window is.
The picture shows the same update done modulo 13, which is what the program really does. The weight of the leading digit, 104, becomes 3 mod 13. Starting from the fingerprint 7, you compute (7 − 3 × 3) × 10 + 2 = −18. That is negative, so you add 13 until it is non-negative, giving 8. And 14152 divided by 13 does leave a remainder of 8. The shortcut and the long way agree.
Negative intermediate results deserve a warning. Python’s remainder operator always returns a non-negative result for a positive modulus, so the code above just works. In C, C++ and Java, the remainder of a negative number can be negative, which silently breaks the fingerprints. If you write this in one of those languages, add the modulus back before taking the remainder.
Here is a complete search on digits, with the numbers printed so nothing is hidden. The haystack is 2359023141526739921, the needle is 31415, and the modulus is 13. For every window the program shows its fingerprint and whether it equals the needle’s fingerprint of 7. It also asserts, on every step, that the rolling update gives the same answer as computing the fingerprint from scratch.
# Textbook-style walk-through with digits so you can follow the arithmetic.
text, pat, mod = "2359023141526739921", "31415", 13
m = len(pat)
target = int(pat) % mod
print(f"pattern {pat} -> {int(pat)} mod {mod} = {target}")
print()
print("shift window hash verdict")
high = 10 ** (m - 1) % mod
h = int(text[:m]) % mod
for s in range(len(text) - m + 1):
window = text[s:s + m]
assert h == int(window) % mod # rolling value equals the direct value
verdict = ""
if h == target:
verdict = "REAL match" if window == pat else "spurious hit (same hash, different text)"
print(f"{s:>5} {window} {h:>5} {verdict}")
if s < len(text) - m:
h = ((h - int(text[s]) * high) * 10 + int(text[s + m])) % mod
Output:
pattern 31415 -> 31415 mod 13 = 7
shift window hash verdict
0 23590 8
1 35902 9
2 59023 3
3 90231 11
4 02314 0
5 23141 1
6 31415 7 REAL match
7 14152 8
8 41526 4
9 15267 5
10 52673 10
11 26739 11
12 67399 7 spurious hit (same hash, different text)
13 73992 9
14 39921 11
Read down the hash column. At shift 6 the window is 31415 and the fingerprint is 7, the same as the needle: a real match. But look at shift 12. The window is 67399, which is a completely different string, and yet its fingerprint is also 7. This is called a spurious hit or false positive. If the algorithm trusted fingerprints alone, it would report a match there that does not exist.
That is why the verification step is not optional. Whenever fingerprints agree, Rabin-Karp compares the real letters. In this example it would check the window at shift 6, confirm it, check the window at shift 12, reject it, and ignore the other thirteen windows entirely. Two letter-by-letter comparisons instead of fifteen. On a longer text with a longer needle, the saving grows.
Letters are not digits, but every character has a numeric code, such as 65 for A and 97 for a. So we treat a string as a number written in base 256 (a base large enough to cover ordinary byte values), where each character is one digit. The text “ABC” becomes 65 × 2562 + 66 × 256 + 67 = 4,276,803. Everything we did with base 10 works with base 256, as long as we replace 10 by 256 in the update formula.
Here is the complete function, followed by a check of the rolling update on a small example and a tiny search:
def rabin_karp(text, pat, base=256, mod=1_000_000_007):
"""Return (match positions, hash hits that needed checking, false alarms)."""
n, m = len(text), len(pat)
if m == 0 or m > n:
return [], 0, 0
high = pow(base, m - 1, mod) # weight of the leading character
hp = ht = 0
for i in range(m): # fingerprints of pattern and first window
hp = (hp * base + ord(pat[i])) % mod
ht = (ht * base + ord(text[i])) % mod
hits, checks, false_alarms = [], 0, 0
for s in range(n - m + 1):
if ht == hp: # fingerprints agree: now compare letters
checks += 1
if text[s:s + m] == pat:
hits.append(s)
else:
false_alarms += 1
if s < n - m: # slide the window by one letter
ht = ((ht - ord(text[s]) * high) * base + ord(text[s + m])) % mod
return hits, checks, false_alarms
if __name__ == "__main__":
base, mod = 256, 1_000_000_007
h = lambda w: sum(ord(c) * base ** (len(w) - 1 - i) for i, c in enumerate(w)) % mod
old = h("ABC")
new = ((old - ord("A") * pow(base, 2, mod)) * base + ord("D")) % mod
print("hash(ABC) =", old)
print("slide: drop A, add D =", new)
print("hash(BCD) computed afresh =", h("BCD"))
print("rolling update is correct =", new == h("BCD"))
print()
print(rabin_karp("the quick brown fox jumps over the lazy dog", "brown"))
Output:
hash(ABC) = 4276803
slide: drop A, add D = 4342596
hash(BCD) computed afresh = 4342596
rolling update is correct = True
([10], 1, 0)
The top four lines confirm the rolling update: the hash of “ABC” is 4,276,803, and dropping the A and adding a D gives 4,342,596, exactly the same as computing “BCD” from scratch. The last line is the search result for “brown” in “the quick brown fox jumps over the lazy dog”: one match at position 10, one hash hit that needed checking, and zero false alarms.
The function returns three numbers: the match positions, how many fingerprint agreements triggered a letter check, and how many of those checks turned out to be false alarms. That last number is the interesting one, because it is where the algorithm can go wrong.
A fingerprint collision happens when two different strings produce the same number. They cannot be avoided entirely, since there are far more possible strings than possible remainders. What you control is how often they happen, and the main lever is the size of the modulus. If the fingerprints behave like random numbers, the chance that a wrong window collides with the needle is about one in the modulus.
I tested that on 200,000 random letters with an 8-letter needle that does not occur, using three moduli:
import random
from rk import rabin_karp
random.seed(5)
text = "".join(random.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(200000))
pat = "qzjxkvwp"
print(f"{len(text) - len(pat) + 1:,} windows, pattern of {len(pat)} letters")
print(f"{'modulus':>15}{'hash hits':>11}{'false alarms':>14}")
for mod in (101, 10007, 1_000_000_007):
hits, checks, bad = rabin_karp(text, pat, 256, mod)
print(f"{mod:>15,}{checks:>11}{bad:>14} real matches: {len(hits)}")
Output:
199,993 windows, pattern of 8 letters
modulus hash hits false alarms
101 2041 2041 real matches: 0
10,007 19 19 real matches: 0
1,000,000,007 0 0 real matches: 0
With a tiny modulus of 101, about 1 window in 100 was a false alarm: 2,041 wasted letter checks across nearly 200,000 windows. With 10,007 the number dropped to 19, and with a modulus around a billion it was zero. The pattern matches the “one in the modulus” rule almost perfectly.
So why not always use a gigantic modulus? Because arithmetic on huge numbers costs more, and in languages with fixed-width integers a product can overflow. A common compromise is a prime modulus around a billion, with care that intermediate products fit in 64 bits. Python hides this problem because its integers grow as needed, which is also why Python code like mine runs slower than the same idea in C.
Prime moduli spread remainders more evenly, which reduces patterned collisions. There is also a security angle. If a program uses a fixed, publicly known base and modulus, a hostile user can craft inputs that collide on purpose, forcing many false alarms and slowing the search to a crawl. Careful implementations choose the base, or the modulus, at random when the program starts. This is a real concern whenever the text comes from outside.
The running time is about n + m in the typical case, where n is the length of the haystack and m the length of the needle. Computing the first two fingerprints costs about m steps, and each of the n slides costs a constant amount. Verification only happens on fingerprint hits, which are mostly real matches or rare false alarms, so it adds little.
The worst case is n × m. It happens when nearly every window collides, for example with a terrible modulus or an adversarial input, so that you verify almost every position. With a well-chosen, randomised modulus that is extremely unlikely. This is the reverse of the trade-off in KMP, which guarantees linear time on every input but is not always the fastest on friendly text. Rabin-Karp is expected linear time with a small, controllable risk.
For a single short needle, honestly, Rabin-Karp is not the champion. The fingerprint arithmetic costs more per step than comparing a single letter, and skip-ahead methods or your language’s built-in search beat it. Its real strength lies elsewhere.
Suppose you have 2,000 banned words, or a database of known phrases, and one document to scan. With one-needle-at-a-time methods you repeat the entire search 2,000 times. Rabin-Karp lets you do it in a single pass. Compute the fingerprint of every needle once and put them in a set. Then slide the window over the document one time, and at each position ask a single question: is this window’s fingerprint in the set? A set lookup takes constant time, so the cost of one pass hardly depends on how many needles you have.
The one restriction is that this simple version needs all the needles to be the same length, so that one window size fits them all. For different lengths, you run one pass for each distinct length.
I measured it with 200 needles of 8 letters, five of them planted in a 200,000-letter text:
import random, time
from rk import rabin_karp
from kmp import kmp_search
random.seed(3)
letters = "abcdefghijklmnopqrstuvwxyz"
text = "".join(random.choice(letters) for _ in range(200000))
patterns = ["".join(random.choice(letters) for _ in range(8)) for _ in range(195)]
patterns += [text[i:i + 8] for i in (1000, 50000, 90000, 150000, 199000)] # five planted
M = 8; BASE = 256; MOD = (1 << 61) - 1
def multi_rabin_karp(text, patterns):
"""One pass over the text, any number of equal-length patterns."""
table = {}
for p in patterns:
hp = 0
for ch in p:
hp = (hp * BASE + ord(ch)) % MOD
table.setdefault(hp, []).append(p)
high = pow(BASE, M - 1, MOD)
ht = 0
for ch in text[:M]:
ht = (ht * BASE + ord(ch)) % MOD
found = {}
for s in range(len(text) - M + 1):
if ht in table:
for p in table[ht]:
if text[s:s + M] == p:
found.setdefault(p, []).append(s)
if s < len(text) - M:
ht = ((ht - ord(text[s]) * high) * BASE + ord(text[s + M])) % MOD
return found
if __name__ == "__main__":
def timed(fn):
t = time.perf_counter(); r = fn(); return r, (time.perf_counter() - t) * 1000
a, ta = timed(lambda: multi_rabin_karp(text, patterns))
b, tb = timed(lambda: {p: h for p in patterns for h in [kmp_search(text, p)[0]] if h})
d, td = timed(lambda: {p: True for p in patterns if text.find(p) >= 0})
print(f"{len(patterns)} patterns, {len(text):,} letters of text")
print(f"patterns found by Rabin-Karp : {len(a)}")
print(f"patterns found by KMP loop : {len(b)}")
print(f"patterns found by str.find : {len(d)}")
print()
print(f"one Rabin-Karp pass (pure Python) : {ta:8.1f} ms")
print(f"one KMP run per pattern (Python) : {tb:8.1f} ms")
print(f"str.find per pattern (C built-in) : {td:8.1f} ms")
Output:
200 patterns, 200,000 letters of text
patterns found by Rabin-Karp : 5
patterns found by KMP loop : 5
patterns found by str.find : 5
one Rabin-Karp pass (pure Python) : 144.5 ms
one KMP run per pattern (Python) : 4373.2 ms
str.find per pattern (C built-in) : 26.8 ms
All three methods found the same five planted needles. The single Rabin-Karp pass took roughly 70 to 150 milliseconds across my runs, depending on how busy the machine was. Running my Python KMP once per needle took about four seconds, roughly thirty to sixty times slower. But notice the last line: Python’s built-in find, written in C, finished the 200 searches in under 30 milliseconds, and beat my pure Python Rabin-Karp. The language matters as much as the algorithm.
The real question is what happens as the number of needles grows. So I scaled it up:
import random, time
from multi import multi_rabin_karp, text, letters
random.seed(11)
print(f"{'patterns':>9}{'Rabin-Karp one pass':>22}{'str.find per pattern':>23}")
for k in (200, 2000, 20000):
pats = ["".join(random.choice(letters) for _ in range(8)) for _ in range(k)]
t = time.perf_counter(); multi_rabin_karp(text, pats); a = (time.perf_counter() - t) * 1000
t = time.perf_counter(); [text.find(p) for p in pats]; b = (time.perf_counter() - t) * 1000
print(f"{k:>9,}{a:>19.1f} ms{b:>20.1f} ms")
Output:
patterns Rabin-Karp one pass str.find per pattern
200 119.1 ms 31.5 ms
2,000 125.6 ms 277.1 ms
20,000 107.5 ms 2757.4 ms
This is the picture that justifies the method. The per-needle approach grows in a straight line: ten times more needles took about ten times longer, reaching 2.7 seconds for 20,000. The single Rabin-Karp pass stayed roughly flat around a tenth of a second, because walking through the text dominates and each lookup costs the same. Somewhere between 200 and 2,000 needles, the crossover happens even against a C built-in, and beyond it the gap keeps widening.
Here is where the rolling hash really surprised me early in my career. Instead of searching for a known needle, you can fingerprint every window of a document and keep the whole set of fingerprints. Two documents that share long passages will share many fingerprints. Two unrelated documents will share almost none.
The recipe: lowercase the text, strip everything except letters so that punctuation and spacing tricks do not matter, then compute the rolling hash of every 12-letter window. The fingerprint set is a compact summary of the document. Comparing two summaries means intersecting two sets.
import re
BASE, MOD, K = 256, (1 << 61) - 1, 12
def fingerprints(text, k=K):
"""Set of rolling hashes of every k-letter window (letters only, lower-case)."""
t = re.sub(r"[^a-z]", "", text.lower())
high = pow(BASE, k - 1, MOD)
h = 0
for ch in t[:k]:
h = (h * BASE + ord(ch)) % MOD
out = {h}
for s in range(len(t) - k):
h = ((h - ord(t[s]) * high) * BASE + ord(t[s + k])) % MOD
out.add(h)
return out
original = ("Rolling hashes let a program fingerprint every window of a document in constant time, "
"which is why plagiarism checkers can compare thousands of essays quickly.")
edited = ("Rolling hashes let a program fingerprint every window of a document in constant time, "
"which is why copy detectors can compare thousands of essays very quickly.")
unrelated = ("The monsoon reached the hills early this year, and the apple orchards along the river "
"were heavy with blossom by the middle of April.")
base_fp = fingerprints(original)
print(f"original has {len(base_fp)} fingerprints")
for name, doc in (("light edit", edited), ("unrelated", unrelated), ("exact copy", original)):
shared = len(base_fp & fingerprints(doc))
print(f"{name:<11}: {shared:>3} shared -> {100 * shared / len(base_fp):5.1f}% of the original")
Output:
original has 122 fingerprints
light edit : 88 shared -> 72.1% of the original
unrelated : 0 shared -> 0.0% of the original
exact copy : 122 shared -> 100.0% of the original
The original sentence produced 122 fingerprints. A lightly edited copy, where I swapped two phrases, still shared 88 of them, 72 percent. An unrelated sentence about apple orchards shared none. An exact copy shared all 122. Notice that the edit did not destroy the comparison: because each fingerprint covers only 12 letters, the untouched stretches of the sentence still match even after the changed words break the windows around them.
Real systems build on exactly this. They pick the window length carefully, since too short gives false similarity from common phrases and too long misses lightly reworded copying. Some, such as the approach behind Stanford’s well-known MOSS code-similarity tool, keep only a well-chosen sample of the fingerprints so that the summary stays small. The core machinery is still the rolling hash.
| Method | Compares | Typical time | Worst case | Best for |
|---|---|---|---|---|
| Naive | Letters | Fast on plain text | n × m | Tiny inputs |
| KMP | Letters with a table | About n | n + m | Guaranteed bounds, one needle |
| Boyer-Moore family | Letters, from the end | Often below n | n × m (simple forms) | Long needle, plain text |
| Rabin-Karp | Fingerprints | About n + m | n × m (rare) | Many needles, document fingerprints |
| Built-in find | Tuned mix | Very fast | Language dependent | Everyday single searches |
You may never write Rabin-Karp for a Ctrl+F box, but its idea runs through systems you use daily. Click through five situations and notice what they share: the data is big, the comparison is repeated, and a cheap fingerprint filters out nearly everything before any expensive work.
A university tool compares a student essay against thousands of earlier documents to flag copied passages.
How it works. Each document is reduced to a set of rolling-hash fingerprints, and similarity is the overlap between sets. In my demo, a lightly edited sentence still shared 72 percent of its fingerprints with the original, while an unrelated one shared none. Because the windows are short, a few reworded phrases do not hide the untouched passages.
A backup service wants to store only the parts of a file that changed, even when someone inserts a paragraph near the top.
How it works. Fixed-size chunks would all shift after an insertion, making every chunk look new. Instead, some systems cut the file wherever a rolling fingerprint of the last few bytes hits a chosen pattern. The cut points move with the content, so an insertion changes only the nearby chunk and the rest can be recognised as already stored. This content-defined chunking idea is widely used in deduplication.
A comment system must scan every new comment against a list of several thousand blocked words and phrases.
How it works. Running one search per word would repeat the whole scan thousands of times. With Rabin-Karp you fingerprint the list once, slide a window across the comment, and check each window against a set. My test showed the single-pass cost staying near a tenth of a second from 200 to 20,000 needles, while the per-needle approach grew in a straight line.
A course instructor wants to know whether two programming assignments share suspicious stretches of code.
How it works. Tools in this family, Stanford’s MOSS being a well-known example, fingerprint runs of consecutive tokens and compare the sets, often keeping only a selected sample of fingerprints to stay compact. Renaming variables or reformatting does not defeat it, because the comparison works on normalised tokens rather than the raw text.
A program needs to find every place where a small logo appears, pixel for pixel, inside a large screenshot.
How it works. The rolling idea extends to two dimensions: fingerprint each row of the patch, then combine the row fingerprints into one, and roll it across the image horizontally and vertically. As always, a matching fingerprint is only a candidate and must be checked pixel by pixel. It finds exact copies, not similar-looking ones.
Here is the short checklist I use. The first question settles most real-world cases.
Think of a coat check. Each coat gets a small numbered ticket, and instead of describing the coat every time, staff just compare tickets, which is instant. Occasionally two coats might share a ticket number because the ticket numbers are short, so before handing a coat back, the attendant glances at it to make sure it really is yours. A rolling fingerprint is the ticket, the glance is the verification step, and the rolling update is a ticket machine that can print the next ticket from the previous one without looking at the coat again.
Try each one on paper before you tap to check.
1. Since 13 × 209 = 2717, the remainder is 1.
6. The weight is 103 mod 13 = 12, so (1 − 2 × 12) × 10 + 2 = −228, and −228 mod 13 = 6. Directly, 7182 mod 13 is also 6.
15, at shifts 0 to 14. The count is the haystack length minus the needle length, plus 1.
Different strings can share a fingerprint. In my walk-through, the window 67399 had the same hash, 7, as the needle 31415 but was not a match.
About 100, since each wrong window collides with probability near one in the modulus.
25 percent of the first document’s fingerprints appear in the second.
A set lookup takes about the same time however many needles it holds, so one pass over the text answers all of them at once.
Tap a question to reveal the answer. The correct option is marked.
Each new fingerprint is derived from the previous one in constant time, so the window length does not matter.
A collision. It is why a fingerprint match is only a candidate until the letters are compared.
With a modulus near a billion there were no false alarms, against 2,041 for a modulus of 101.
Fingerprints of all needles go into a set, and one pass over the text checks every window against it.
If most windows collide, you verify almost every position, which is as slow as the naive method.
A string matching algorithm that compares numeric fingerprints of text windows with the fingerprint of the pattern, using a rolling hash to update each fingerprint in constant time, and verifies the real letters only when fingerprints match.
A hash function that can be updated cheaply when a window slides by one position. You remove the contribution of the letter that leaves and add the one that arrives, instead of rehashing the whole window.
Expected time is about O(n + m) with a good modulus. The worst case is O(n × m) when many windows collide, which is unlikely with a large, randomly chosen modulus.
Not for a single needle. KMP guarantees linear time and avoids hashing. Rabin-Karp is better when you search for many needles at once or fingerprint documents.
Primes tend to spread remainders more evenly, reducing patterned collisions. Choosing the base or modulus at random also protects against inputs crafted to collide.
Not if you verify. A fingerprint match is only a candidate, and comparing the actual letters removes any false positive. Fingerprints never miss a true match, because equal strings always have equal fingerprints.
The post Rabin-Karp: Searching With Rolling Fingerprints appeared first on Learn With Examples.
]]>The post How Ctrl+F Finds Text Instantly: KMP and String Matching Explained appeared first on Learn With Examples.
]]>You press Ctrl+F, type a word, and every match on a long page lights up before you have lifted your finger. It feels like magic, but the problem underneath is one of the oldest in computing: find a short piece of text, the pattern, inside a much longer piece, the text. A beginner solves it in ten lines. A careful engineer asks what happens when those ten lines meet a hostile input. This guide walks through the simple method, shows exactly where it breaks, and then explains the Knuth-Morris-Pratt algorithm, the idea that makes searching reliably fast. Every number in it comes from code I ran.
Computer scientists call the long text the haystack and the thing you are looking for the needle. Searching a web page, a Word document, a source file or a chromosome all reduce to the same question: at which positions does the needle appear in the haystack?
It sounds trivial because your eyes do it so easily. But a computer has no eyes. It can only compare one character with another, one pair at a time. The whole art of string matching is about how few of those comparisons you can get away with. Fewer comparisons mean a faster search, and when the haystack holds a hundred million characters, the difference between a clever method and a careless one is the difference between a blink and a coffee break.
The short version. The simple method slides the needle along the haystack one step at a time and re-checks everything after every slip. Knuth-Morris-Pratt (KMP) remembers what the last attempt already proved, so it never looks backward in the haystack. That guarantees at most about twice as many comparisons as there are characters, no matter how nasty the input is.
Imagine you write the needle on a strip of tracing paper and lay it over the start of the haystack. If every letter lines up, you have a match. If any letter disagrees, you slide the strip one place to the right and start checking again from the strip’s first letter. This is the naive or brute-force method, and it is the one most people invent on their own.
For ordinary text it is fine. Searching for “budget” in an essay, most positions fail on the very first letter, so each slide costs one comparison. The trouble starts when the text is repetitive. Suppose the haystack is a thousand letter A’s followed by a B, and the needle is fifty A’s followed by a B. At every starting position the first fifty letters match, and only the last comparison fails. The method then slides by one and repeats almost all the same work.
Look at the picture. The first attempt matched five letters, ABABA, and then failed at the sixth. A naive search forgets everything and starts again one step to the right. But we are not ignorant: we just saw that the text contains ABABA at that spot. The last three of those letters, ABA, are also the first three letters of the needle. So the needle can slide two places and carry on from its fourth letter, without re-checking ABA at all. That one observation is the whole of KMP.
Before we get there, let me put numbers on the pain. I wrote the naive search with a counter on every character comparison, and ran it on three kinds of text:
import random
from kmp import kmp_search
def naive_search(text, pat):
hits, comps = [], 0
for s in range(len(text) - len(pat) + 1):
j = 0
while j < len(pat):
comps += 1
if text[s + j] != pat[j]:
break
j += 1
if j == len(pat):
hits.append(s)
return hits, comps
def horspool_search(text, pat):
m = len(pat)
shift = {c: m - 1 - i for i, c in enumerate(pat[:-1])} # bad-character table
hits, comps, s = [], 0, 0
while s <= len(text) - m:
j = m - 1
while j >= 0: # compare from the END of the pattern
comps += 1
if text[s + j] != pat[j]:
break
j -= 1
if j < 0:
hits.append(s)
s += shift.get(text[s + m - 1], m)
return hits, comps
if __name__ == "__main__":
essay = ("every time you press the find shortcut the page has to look through thousands of "
"characters for a short phrase. a careless method restarts at every position, while "
"a smarter one remembers what it has already seen and skips ahead. looking for a needle "
"in a haystack sounds slow, yet careful programs do it in the blink of an eye. ") * 3
random.seed(7)
dna = "".join(random.choice("ACGT") for _ in range(100000))
cases = [
("Repeated A, then B", "A" * 1000 + "B", "A" * 50 + "B"),
("English paragraph", essay, "blink of an eye"),
("Random DNA", dna, "GATTACAGATTACA"),
]
print(f"{'case':<20}{'text':>8}{'pat':>5}{'hits':>6}{'naive':>10}{'KMP':>9}{'Horspool':>10}")
for name, text, pat in cases:
a, ca = naive_search(text, pat)
b, cb = kmp_search(text, pat)
c, cc = horspool_search(text, pat)
assert a == b == c
print(f"{name:<20}{len(text):>8}{len(pat):>5}{len(a):>6}{ca:>10}{cb:>9}{cc:>10}")
Output:
case text pat hits naive KMP Horspool
Repeated A, then B 1001 51 1 48501 1951 1001
English paragraph 987 15 3 1018 990 163
Random DNA 100000 14 0 133365 124971 47163
On the repeated-A text the naive method needed 48,501 comparisons to scan a text only 1,001 characters long, roughly 48 for every character. The same job took KMP 1,951 and the Horspool method (more on that shortly) 1,001. On a plain English paragraph the three were all close, because ordinary text rarely has long near-misses. This matters: the naive method is not slow on average. It is slow on the wrong input, and a program that handles user-supplied text cannot choose its input.
In the worst case the naive method needs about n × m comparisons, where n is the haystack length and m the needle length. A 100,000-character log and a 1,000-character needle could mean a hundred million comparisons. KMP needs about n + m.
Here is a human version of the idea. You are reading a phone book looking for the name “ANANAS”. You read A, N, A, N, A, and then the next letter is not S, it is N. A naive reader would go back to the second letter of the page and start over. But you already know that the last few letters you read were A, N, A, and that is exactly how ANANAS begins. So you just carry on as if you had started a short while ago, already three letters into the name.
KMP formalises that gut feeling. Before it touches the haystack, it studies the needle alone and prepares a small cheat sheet: for each position in the needle, how much of the needle’s own beginning is also its ending? That cheat sheet is the failure table, also called the prefix function or LPS table, short for “longest proper prefix which is also a suffix”.
These words sound heavy but the idea is plain. A prefix is the beginning of a word: for ABABA the prefixes are A, AB, ABA and ABAB. A suffix is the ending: A, BA, ABA and BABA. A border is something that is both. In ABABA, the string ABA is a prefix and also a suffix, so ABA is a border of length 3. (We exclude the whole word, which is why the table says “proper”.)
Why does a border matter? Because it tells you what the needle can safely fall back to after a failure. If you have matched ABABA and then hit a mismatch, you know the last three letters of what you matched are ABA, which is the start of the needle. So you resume as though you had matched only ABA. No searching backward in the haystack is needed.
The table holds one number per letter of the needle: the border length of the needle up to and including that letter. I printed the tables for three needles:
def build_lps(pat):
"""lps[i] = length of the longest proper prefix of pat[:i+1]
that is also a suffix of it."""
lps = [0] * len(pat)
k = 0 # length of the prefix matched so far
for i in range(1, len(pat)):
while k > 0 and pat[i] != pat[k]:
k = lps[k - 1] # fall back to a shorter prefix
if pat[i] == pat[k]:
k += 1
lps[i] = k
return lps
def kmp_search(text, pat):
"""Return (list of match positions, number of character comparisons)."""
lps = build_lps(pat)
hits, comps, k = [], 0, 0
for i, ch in enumerate(text): # the text pointer only moves forward
while True:
comps += 1
if ch == pat[k]:
k += 1
break
if k == 0:
break
k = lps[k - 1] # reuse what we already matched
if k == len(pat):
hits.append(i - k + 1)
k = lps[k - 1] # allow overlapping matches
return hits, comps
if __name__ == "__main__":
for pat in ("ABABAC", "ABCDABD", "AAAB"):
lps = build_lps(pat)
print(f"pattern {pat}")
print(" char:", " ".join(pat))
print(" lps :", " ".join(map(str, lps)))
print()
hits, comps = kmp_search("ABABABAC", "ABABAC")
print("search ABABAC in ABABABAC -> start", hits, "after", comps, "comparisons")
Output:
pattern ABABAC
char: A B A B A C
lps : 0 0 1 2 3 0
pattern ABCDABD
char: A B C D A B D
lps : 0 0 0 0 1 2 0
pattern AAAB
char: A A A B
lps : 0 1 2 0
search ABABAC in ABABABAC -> start [2] after 9 comparisons
For ABABAC the table reads 0, 0, 1, 2, 3, 0. Read it this way: after matching the first five letters ABABA, the border is 3, so if the sixth letter fails, fall back to having matched 3 letters. The final C has no border, hence 0. For ABCDABD the table shows 1 and 2 under the second A and B, because the needle starts again with AB halfway through. For AAAB the table climbs 0, 1, 2 because every A is a continuation of the same run; this is precisely the shape of needle that wrecks the naive method, and also precisely where KMP shines.
Notice the first entry is always 0, since a single letter has no proper prefix. A needle with no repeated structure, such as ABCDEF, gets all zeros, and then KMP behaves almost like the naive method, because there is nothing to reuse.
With the table ready, the search runs two pointers. One walks through the haystack and only ever moves forward. The other, call it k, counts how many letters of the needle are currently matched. At each haystack letter you do one of three things:
Here is the whole algorithm, which is shorter than most people expect:
def kmp_search(text, pat):
"""Return (list of match positions, number of character comparisons)."""
lps = build_lps(pat)
hits, comps, k = [], 0, 0
for i, ch in enumerate(text): # the text pointer only moves forward
while True:
comps += 1
if ch == pat[k]:
k += 1
break
if k == 0:
break
k = lps[k - 1] # reuse what we already matched
if k == len(pat):
hits.append(i - k + 1)
k = lps[k - 1] # allow overlapping matches
return hits, comps
Trace the earlier example. The haystack is ABABABAC and the needle ABABAC. The first five letters match, so k climbs to 5. The sixth haystack letter is B but the needle wants C, a mismatch. The table entry for the fifth needle letter is 3, so k drops to 3, meaning “ABA is still matched”. The needle’s fourth letter is B, which matches the haystack’s B, so k rises to 4. Then A matches, k is 5, then C matches, k is 6, and 6 equals the needle length: a match starting at position 2. The program output said exactly that, found at start 2 after 9 comparisons, for a haystack of 8 letters.
The inner loop looks scary because of the fallback, which could in theory repeat many times. Here is why it cannot get out of hand. Each time the haystack pointer advances, k can grow by at most one. Each fallback strictly lowers k. So the total number of fallbacks over the whole search can never exceed the total number of increases, and that is at most n. Add the n forward steps and the fallbacks and you get at most about 2n comparisons. Building the table works the same way and costs about m. The total, n plus m, is called linear time, and it holds for any input.
That is why the experiment gave 1,951 comparisons for the 1,001-character repeated text. It lands just under the theoretical ceiling of 2n, which is 2,002. The algorithm is nearly as slow as it is allowed to be on that input, and still about twenty-five times better than the naive method.
This is the part most tutorials skip. Look again at the third row of the comparison output. On 100,000 random letters of DNA, using a 14-letter needle, KMP made 124,971 comparisons against naive’s 133,365. That is barely better. And the Horspool method, a cousin I have not explained yet, needed only 47,163. KMP’s selling point is the guarantee: it never degrades. It does not promise to win on friendly input.
Why did Horspool win so clearly on ordinary text? Because it uses the opposite trick. Instead of comparing the needle from the front, it compares from the back. If the haystack letter lined up with the needle’s last position is a letter that does not appear in the needle at all, say a Z when searching for “needle”, then no placement overlapping that Z can possibly match, and the whole needle can jump forward by its full length in one step. On English text with a 15-letter needle, that means looking at only about one letter in every several. The English row shows it: 163 comparisons for a 987-character text.
The family of methods built on this backward comparison is called Boyer-Moore, and Horspool is its simplified cousin. Well-known command line tools such as GNU grep are widely documented as using skip-ahead ideas of this kind for plain-text patterns, which is a large part of why searching a gigabyte of logs takes seconds. The longer your needle, the more you can skip, which feels backward until you think about it: more letters to check also means more chances to rule out a placement early.
I want to be careful here. Browsers and editors do not publish the exact algorithm they use, it varies between programs and versions, and I would not want to state one as fact. What I can describe with confidence is the shape of the job, because it is the same everywhere:
And here is the honest truth about the word “instantly”. A typical page holds maybe fifty thousand characters. Even the naive method on ordinary text does that in a tiny fraction of a second on a modern machine. My own measurement scanned 300,000 characters in about 35 milliseconds with the slowest method, and I wrote that one in Python, which is itself slow. The instant feel comes mostly from raw hardware speed and the fact that ordinary text is friendly. The cleverness of KMP matters when the text is huge, repetitive, or arrives from an untrusted source and you cannot risk a pathological case.
One subtle thing your find box must decide: do matches overlap? Search for “aa” in “aaaaa”. Do you report two hits (positions 0 and 2, taking turns) or four (positions 0, 1, 2 and 3)? Different tools answer differently, and the choice matters when you display a count. I tested three common approaches:
import re
from kmp import kmp_search
text, pat = "aaaaa", "aa"
print("str.count :", text.count(pat), "(non-overlapping)")
print("re.findall :", len(re.findall(pat, text)), "(non-overlapping)")
print("KMP, all hits :", len(kmp_search(text, pat)[0]), "at", kmp_search(text, pat)[0])
doc = "The cat sat. The Cat ran. A cat, the CAT."
print()
print("exact 'cat' :", doc.count("cat"))
print("ignore case :", doc.lower().count("cat"))
print("whole word :", len(re.findall(r"\bcat\b", doc, re.I)))
Output:
str.count : 2 (non-overlapping)
re.findall : 2 (non-overlapping)
KMP, all hits : 4 at [0, 1, 2, 3]
exact 'cat' : 2
ignore case : 4
whole word : 4
Python’s count and the regular expression finder both say 2, because they skip past each match before looking again. KMP, as I wrote it, resets to the border after a match and finds all 4 overlapping hits. Neither is wrong; they answer different questions. The second block shows the other classic gotcha: exact search for “cat” finds 2 hits in my sentence, while ignoring case finds 4. If a user types a word and sees “2 results” when they can plainly see four, that is a case-sensitivity bug, not an algorithm bug.
I timed four methods on two inputs: ordinary text, and the worst case for the naive method. These are pure Python implementations, so absolute numbers are modest, but the shape is what counts.
import random, time
from kmp import kmp_search
from compare import naive_search, horspool_search
def best(fn, *a, runs=3):
t = 1e9
for _ in range(runs):
s = time.perf_counter(); fn(*a); t = min(t, time.perf_counter() - s)
return t * 1000
random.seed(1)
text = "".join(random.choice("abcdefghijklmnopqrstuvwxyz ") for _ in range(300000))
pat = "zzqxjvkw"
print("Ordinary text, 300,000 characters, 8-letter pattern (milliseconds)")
for name, fn in (("naive", naive_search), ("KMP", kmp_search), ("Horspool", horspool_search)):
print(f" {name:<10}{best(fn, text, pat):>9.1f}")
print(f" {'str.find':<10}{best(lambda t, p: t.find(p), text, pat):>9.1f}")
bad = "a" * 60000 + "b"
badpat = "a" * 150 + "b" + "c"
print()
print("Worst case for naive, 60,000 letters (milliseconds)")
print(f" {'naive':<10}{best(naive_search, bad, badpat, runs=1):>9.1f}")
print(f" {'KMP':<10}{best(kmp_search, bad, badpat):>9.1f}")
print(f" {'str.find':<10}{best(lambda t, p: t.find(p), bad, badpat):>9.1f}")
Output (your numbers will differ a little from run to run):
Ordinary text, 300,000 characters, 8-letter pattern (milliseconds)
naive 36.9
KMP 27.8
Horspool 8.5
str.find 0.2
Worst case for naive, 60,000 letters (milliseconds)
naive 709.9
KMP 7.8
str.find 0.1
Two lessons jump out. First, on ordinary text the order was Horspool, then KMP, then naive, all within a factor of five. The built-in str.find beat all of them by a huge margin, because it is written in C, and recent versions of Python combine several strategies inside it, including a method called Two-Way for longer needles. In real programs, use the built-in. Writing KMP yourself is for learning, for languages without a good built-in, or for streaming problems where you cannot hold the whole text.
Second, on the nasty input the naive search took several hundred milliseconds while KMP took under ten, roughly ninety times faster on my run, and the gap grows as the text grows. That is the scenario that justifies the algorithm: it turns a catastrophic input into an ordinary one.
Instead of comparing letters, compute a numeric fingerprint, a hash, of the needle, and a rolling hash of each window of the haystack. Sliding the window one step updates the hash in constant time. Only when two fingerprints agree do you check the actual letters. The method is commonly described as a building block in plagiarism detection, because it extends naturally to checking many fragments at once.
What if you have ten thousand needles, such as a database of virus signatures or a list of banned words? Running KMP ten thousand times is wasteful. Aho-Corasick builds one automaton from all the needles, a generalisation of the KMP failure table, and scans the haystack once. Antivirus engines such as ClamAV are known to use it.
If the haystack is fixed and you will search it again and again, as genome databases do, you can pay once to build an index such as a suffix array, and then each query takes time depending mostly on the needle rather than the haystack. Scanning three billion DNA letters for every query would be hopeless.
| Method | Worst case | Typical case | Core idea |
|---|---|---|---|
| Naive | n × m | Fast on friendly text | Slide by one, re-check everything |
| KMP | n + m | About n | Failure table, never move back |
| Horspool | n × m | Often well below n | Compare from the end, skip ahead |
| Rabin-Karp | n × m (rare) | About n + m | Rolling hash fingerprints |
| Aho-Corasick | n + total needle length | Linear | One automaton for many needles |
You use these algorithms far more often than you press Ctrl+F. Click through five everyday situations and notice how the needs differ: sometimes you want every match, sometimes only a guarantee, sometimes thousands of needles at once.
You press Ctrl+F on a 40-page report and type “budget”. The page shows “12 results” and highlights each one.
How it works. The program normalises case, scans the visible text, stores every hit position and paints the highlights. A page is small enough that any sensible method answers in a blink, so the real design choices are the user-facing ones: ignore case, ignore accents, and whether overlapping matches count. As my overlap test showed, that decision alone can change the count from 2 to 4.
A biologist looks for a short gene fragment inside a genome. The human genome is roughly three billion letters, written with only four symbols.
How it works. A four-letter alphabet makes near-misses very common, and DNA is full of repeats, which is exactly the kind of input where the naive method suffers. KMP’s linear guarantee helps. In practice, researchers searching the same genome again and again build an index once, such as a suffix array, because scanning three billion letters for every query would still be slow.
An engineer runs grep over a day of server logs, looking for the phrase “connection refused”.
How it works. Plain-language logs with a longish phrase are the sweet spot for Boyer-Moore style skipping. Most positions can be ruled out by looking at a single letter near the end of the window, so the program leaps over large stretches of text. My Horspool test made only 163 comparisons over a 987-letter English paragraph for the same reason.
A scanner checks a file against a database of thousands of known malicious byte patterns.
How it works. Running a single-pattern search thousands of times per file would be painfully slow. Multi-pattern methods such as Aho-Corasick merge all needles into one automaton, a generalisation of the KMP failure table, and read the file once. The same idea powers profanity filters that must check a comment against a long list of banned words.
A university tool checks whether a student’s essay contains sentences lifted from thousands of earlier documents.
How it works. Rabin-Karp turns each window of text into a numeric fingerprint that can be updated in constant time as the window slides. Matching fingerprints are then confirmed letter by letter. The approach scales naturally to many fragments at once, which is why it is commonly described in connection with plagiarism detection.
When someone asks me which algorithm to use, I answer with a short sequence of questions. In practice the first question ends the conversation nine times out of ten.
find, JavaScript’s indexOf and Java’s indexOf are tuned and tested. Write KMP when you need streaming, custom matching or a guaranteed bound.lps[k - 1], not lps[k]. This is the most common bug in hand-written KMP.re.escape or a plain string search.Think of reading a street name on a long road sign while driving past. The naive method is a driver who, on spotting one wrong letter, stops, reverses to the beginning of the sign, and starts reading again. KMP is a driver who has memorised the sign’s own pattern of repeats and never reverses: after a wrong letter he glances at his memory and says, “fine, I have already read the last three letters of the right word”, and keeps driving. Horspool is a driver who reads from the far end of the sign first, and when that letter is not in the word at all, leaps ahead by the whole sign’s length.
Try each one on paper before you tap to check.
0 0 1 2. The border of ABA is A (length 1) and the border of ABAB is AB (length 2).
0 1 0 1 2 3. The word repeats itself, so the border keeps growing: AABAA ends with AA, and AABAAB ends with AAB.
0 0 0 1 2 0. AB repeats, giving 1 and 2, but the final D breaks the pattern and drops back to 0.
4, starting at positions 0, 1, 2 and 3. A non-overlapping count gives only 2.
From my run: naive 12 (three at each of four starting positions), KMP 9 and Horspool 6. All three find the single match at position 3.
Every entry is 0. The word has no border anywhere, so after a mismatch there is nothing to reuse and KMP behaves almost like the naive method.
About 2,000,000, plus a tiny amount to build the table. The naive method could need up to a thousand times more if the needle were 1,000 letters long.
Tap a question to reveal the answer. The correct option is marked.
It stores the border length, which says how much of the match can be reused after a mismatch.
After a mismatch it only changes the needle position k, using what it already learned.
Near-misses force it to repeat almost the same comparisons at every start position.
If the letter under the needle’s last position cannot appear in a match, the whole needle can jump forward.
The built-in is written in C and tuned. In my tests it took 0.2 ms where my Python KMP took 27.8 ms.
Knuth-Morris-Pratt is a string matching algorithm that finds a pattern in a text in linear time. It builds a failure table from the pattern so that, after a mismatch, it never has to move backward in the text.
O(n + m), where n is the text length and m the pattern length. Building the table costs about m steps and the search at most about 2n comparisons.
Browsers and editors do not publish their exact method and it varies between programs, so I cannot say. All of them solve the same problem: normalise the text, scan for the pattern, collect every match and highlight it.
On ordinary text most positions fail on the first letter, so each slide costs about one comparison. It only becomes slow on repetitive or adversarial input, which is when guarantees matter.
KMP compares from the front and uses a failure table to avoid re-checking letters. Boyer-Moore compares from the end and skips ahead using the letters it sees, which is often much faster on plain text but lacks KMP’s simple worst-case guarantee in its simplified forms.
Rarely. Use your language’s built-in search for everyday work. Implement KMP to learn, for streaming input, or when you need a guaranteed bound on a pattern you control.
The post How Ctrl+F Finds Text Instantly: KMP and String Matching Explained appeared first on Learn With Examples.
]]>The post Data Types Explained: Integers, Floats, Booleans and Why They Differ appeared first on Learn With Examples.
]]>Every beginner hits the same wall sooner or later. They add 0.1 and 0.2, get 0.30000000000000004, and decide the computer is broken. It is not broken. It is doing exactly what you told it to do, and what you told it depends on a choice you probably made without noticing: the data type. This guide explains integers, floats and booleans from the ground up, shows why the computer treats them so differently, and walks through the real bugs that happen when you pick the wrong one.
A computer’s memory is a very long row of tiny switches, each either off or on. That is all it holds: zeros and ones. There is no such thing as a “number” or a “letter” down there. The pattern 01000001, for example, can mean the whole number 65, or it can mean the capital letter A, or it can be one tiny piece of a colour in a photograph. The bits themselves do not say which.
The data type is the label that tells the computer how to read a pattern. It answers three questions at once: how many switches does this value use, how should those switches be interpreted, and which operations make sense on it. You can add two numbers, but adding two pieces of text sticks them together instead, and asking whether a yes/no answer is “greater than 7” makes no sense at all.
Think of a kitchen. A measuring jug, a kitchen scale and a thermometer all give you a number, but you would not use the thermometer to weigh flour. Each tool suits a kind of question. Data types are the same: tools for different kinds of facts.
The short version. An integer is a whole number, exact and used for counting. A float is a number with a decimal point, built for measuring and only approximately exact. A boolean is a yes/no value, either true or false. They differ because each is stored in a different way, and the way it is stored decides what it can do well and where it quietly goes wrong.
An integer is a number with no fractional part: −3, 0, 7, 52,314. It is the type for anything you count. The number of people in a stadium, the number of items in a shopping cart, a person’s age in years, the page you are on: all integers. You cannot have 2.5 people in the stadium, so the type simply has no room for a fraction.
The big strength of integers is that they are exact. If you add 3 and 4, you get precisely 7, every time, forever. There is no rounding and no hidden error. That reliability is why money, counters, loops and array positions all lean on integers.
Computers store integers in binary, using a fixed number of bits. With 8 bits you have 256 possible patterns. If all of them are used for non-negative numbers (an “unsigned” integer), the range is 0 to 255. That is exactly why colour values on a screen run from 0 to 255: each of red, green and blue gets one byte.
If you also want negative numbers (a “signed” integer), one bit effectively goes to the sign, and the same 256 patterns cover −128 to 127. Wider integers get far more room: 16 bits cover −32,768 to 32,767, 32 bits cover about −2.1 billion to 2.1 billion, and 64 bits cover roughly −9.2 quintillion to 9.2 quintillion.
Here is where integers can bite. A fixed-width integer has a biggest value, and adding one more does not produce an error in many languages. It wraps around, like a car odometer rolling from 99999 back to 00000.
I ran the numbers to be sure I was not just repeating folklore. This short program simulates what a signed integer of each width does when you add one to its maximum, and also shows the famous Unix time limit.
import struct
from datetime import datetime, timezone
def wrap(value, bits):
"""What a signed integer of this width does when it overflows."""
m = 1 << bits
v = value % m
return v - m if v >= m // 2 else v
for bits in (8, 16, 32):
top = 2 ** (bits - 1) - 1
print(f"{bits:>2}-bit signed: max {top:>11,} max + 1 wraps to {wrap(top + 1, bits):>14,}")
print()
last = 2 ** 31 - 1
print("32-bit Unix time limit:", datetime.fromtimestamp(last, tz=timezone.utc))
print("one second later wraps to:", datetime.fromtimestamp(wrap(last + 1, 32), tz=timezone.utc))
print()
print("Python int keeps going: 2**63 =", 2 ** 63)
print("float loses whole numbers:", float(2 ** 53) + 1 == float(2 ** 53))
print()
for code, name in (("b", "tiny int"), ("h", "short int"), ("i", "int"), ("q", "long int"), ("f", "float32"), ("d", "float64"), ("?", "boolean")):
print(f"{name:<10} {struct.calcsize(code)} byte(s)")
Output:
8-bit signed: max 127 max + 1 wraps to -128
16-bit signed: max 32,767 max + 1 wraps to -32,768
32-bit signed: max 2,147,483,647 max + 1 wraps to -2,147,483,648
32-bit Unix time limit: 2038-01-19 03:14:07+00:00
one second later wraps to: 1901-12-13 20:45:52+00:00
Python int keeps going: 2**63 = 9223372036854775808
float loses whole numbers: True
tiny int 1 byte(s)
short int 2 byte(s)
int 4 byte(s)
long int 8 byte(s)
float32 4 byte(s)
float64 8 byte(s)
boolean 1 byte(s)
Look at the date line. Many systems count time as the number of seconds since 1 January 1970, stored in a signed 32-bit integer. That counter runs out at 03:14:07 UTC on 19 January 2038. One second later it wraps to a large negative number, which a computer reads as December 1901. This is the “Year 2038 problem”, and engineers have been moving systems to 64-bit time to avoid it.
Overflow is not only a classroom curiosity. In 1996 the first launch of Europe’s Ariane 5 rocket ended in self-destruction after a value converted from a 64-bit floating-point number into a 16-bit signed integer was too large to fit. In December 2014, the view counter for the video “Gangnam Style” passed 2,147,483,647, the top of a 32-bit signed integer, and YouTube said it had to switch the counter to 64 bits. Both are reminders that the width of an integer is a decision, not a detail.
Python has an unusual comfort here: its integers grow as large as memory allows, so 2100 is simply 1,267,650,600,228,229,401,496,703,205,376. Languages such as C, Java and Go use fixed widths, and there you must choose carefully.
A float, short for floating-point number, is for values that can have a fractional part: 3.14, 36.6, 0.0001, 6.02 × 1023. Use it for things you measure rather than count: body temperature, the length of a table, the speed of a car, the voltage across a battery.
The name “floating point” describes how the number is stored. Instead of fixing the decimal point in one place, the computer keeps two pieces: the significant digits, and an exponent that says where the point floats to. It is the same idea as scientific notation. The number 6,020,000 can be written 6.02 × 106: the digits 6.02 and the exponent 6. A float stores a pair like that, but in binary.
This design gives floats a huge range. A standard 64-bit float, which most languages call a “double”, reaches from about 2.2 × 10−308 up to about 1.8 × 10308. No integer of the same size comes close. The price is exactness.
Let me show you the thing that confuses everyone, then explain it. In Python:
import math
print("7 ->", type(7).__name__)
print("7.0 ->", type(7.0).__name__)
print("True ->", type(True).__name__)
print('"7" ->', type("7").__name__)
print()
print('"5" + "5" =', repr("5" + "5"))
print('5 + 5 =', 5 + 5)
print('int("5") + int("5") =', int("5") + int("5"))
print()
print("10 / 4 =", 10 / 4)
print("10 // 4 =", 10 // 4)
print("10 % 4 =", 10 % 4)
print()
print("True + True =", True + True)
print("isinstance(True, int) =", isinstance(True, int))
print("0.1 + 0.2 =", 0.1 + 0.2)
print("0.1 + 0.2 == 0.3 =", 0.1 + 0.2 == 0.3)
print("isclose =", math.isclose(0.1 + 0.2, 0.3))
print("2 ** 100 =", 2 ** 100)
Output:
7 -> int
7.0 -> float
True -> bool
"7" -> str
"5" + "5" = '55'
5 + 5 = 10
int("5") + int("5") = 10
10 / 4 = 2.5
10 // 4 = 2
10 % 4 = 2
True + True = 2
isinstance(True, int) = True
0.1 + 0.2 = 0.30000000000000004
0.1 + 0.2 == 0.3 = False
isclose = True
2 ** 100 = 1267650600228229401496703205376
Toward the bottom you will see 0.1 + 0.2 comes out as 0.30000000000000004, and the comparison with 0.3 is false. Python is not being silly. The same result appears in JavaScript, Java, C and nearly every language, because it comes from how binary stores fractions.
Here is the analogy I use with students. Try writing one third in decimal. You get 0.3333… and it never ends. You can write as many 3s as you like, but you can never write it exactly. Now notice that one third is a perfectly ordinary number. It is only awkward because our counting system is base ten, and ten is not divisible by three.
Computers use base two. In base two, one tenth is the awkward one. It becomes 0.0001100110011… repeating forever, so it has to be cut off somewhere. What the computer actually stores for 0.1 is a very slightly different number. I printed it:
from decimal import Decimal
import struct, sys
print("0.1 is really stored as:", Decimal(0.1))
print("0.5 is stored as :", Decimal(0.5), "(a power of two, so exact)")
print("0.75 is stored as :", Decimal(0.75))
bits = format(struct.unpack(">Q", struct.pack(">d", 0.1))[0], "064b")
print("64 bits of 0.1:", bits[0], bits[1:12], bits[12:])
print("largest float :", sys.float_info.max)
print("smallest float :", sys.float_info.min)
print("digits of care :", sys.float_info.dig)
import math
print()
print("gap between neighbouring floats:")
print(" near 1 :", math.ulp(1.0))
print(" near 1e15 :", math.ulp(1e15))
print(" near 2**53 (9e15) :", math.ulp(2.0 ** 53))
Output:
0.1 is really stored as: 0.1000000000000000055511151231257827021181583404541015625
0.5 is stored as : 0.5 (a power of two, so exact)
0.75 is stored as : 0.75
64 bits of 0.1: 0 01111111011 1001100110011001100110011001100110011001100110011010
largest float : 1.7976931348623157e+308
smallest float : 2.2250738585072014e-308
digits of care : 15
gap between neighbouring floats:
near 1 : 2.220446049250313e-16
near 1e15 : 0.125
near 2**53 (9e15) : 2.0
So the stored 0.1 is 0.1000000000000000055511… Not exactly one tenth, but close enough that you normally never notice. Add two such slightly-off values and the tiny errors can show up at the sixteenth or seventeenth digit. Notice too that 0.5 and 0.75 are stored exactly, because they are built from powers of two (a half, a quarter). A float is only exact when the fraction is made of halves, quarters, eighths and so on.
There is a second quirk. A 64-bit float has only about 15 to 16 reliable decimal digits, and the gaps between neighbouring floats grow as the numbers grow. That is the output at the bottom of the block above: near 1, neighbouring floats are about 0.0000000000000002 apart. Near 9 quadrillion, they are 2 apart, so the float simply cannot represent every whole number.
The practical rule: beyond 253, which is 9,007,199,254,740,992, floats skip whole numbers. In JavaScript, where every number is a float, this is why the biggest integer you can trust is 9,007,199,254,740,991. It is also why you should never store something like a 19-digit ID in a float.
Because of those tiny errors, never ask whether two floats are exactly equal after calculation. Ask whether they are close enough. In Python, the function math.isclose does it. In other languages, take the absolute difference and compare it with a small tolerance, such as 0.000001, chosen to suit your problem. This one habit prevents a surprising number of bugs.
A boolean has exactly two possible values: true or false. It is named after the mathematician George Boole, who built a branch of logic around exactly this idea in the nineteenth century. In everyday life, a boolean is any yes/no fact: is the user logged in, is the light on, has the invoice been paid, is the stock above its limit.
Booleans are the type that makes decisions possible. Every if statement and every loop condition eventually boils down to a boolean. When you write “if age is at least 18”, the comparison produces true or false, and the program picks a path based on it.
A boolean carries one bit of information, since there are only two answers. Yet most systems store it in a whole byte, because computers address memory in bytes and it is faster to read a full byte than to dig out a single bit. The little table at the end of the overflow code printed this: a boolean takes 1 byte, the same as a tiny integer. Programmers who need to store millions of flags sometimes pack eight of them into a single byte for exactly this reason.
In Python, True behaves like 1 and False behaves like 0. The code above showed that True + True gives 2 and that a boolean passes the test for being an integer. That is occasionally handy, for example counting how many items in a list satisfy a test by adding up their true/false results. It is also a source of odd bugs, so keep it in mind.
A more important trap involves text. The string “False” is not the boolean False. In Python, any non-empty string counts as true, so bool(“False”) is True. This bites people reading settings from a file or a web form, where everything arrives as text. Convert the value deliberately, instead of trusting it.
With all three in front of you, the differences are easier to see in one place.
| Property | Integer | Float | Boolean |
|---|---|---|---|
| Holds | Whole numbers | Numbers with a fraction | True or false |
| Example | 42, −7 | 3.14, 36.6 | True, False |
| Typical size | 1, 2, 4 or 8 bytes | 4 or 8 bytes | 1 byte |
| Exact? | Yes, within its range | No, about 15 digits | Yes |
| Range | Limited by width | Enormous, but coarse | Two values |
| Main danger | Overflow | Rounding error | Confusing text with true/false |
| Best for | Counting, IDs, money in small units | Measurements, science | Conditions, flags |
Notice that each type’s strength and weakness come from the same decision. Integers are exact because they cannot hold fractions. Floats can hold fractions because they give up exactness. Booleans are simple because they hold almost nothing. There is no free lunch, just a trade-off chosen for each job.
Strictly speaking this article is about three types, but one more deserves a mention because beginners confuse it with integers all the time. A string is text: letters, digits, spaces, anything in quotation marks. The string “5” and the integer 5 look alike but behave very differently. Adding the two integers 5 + 5 gives 10. Adding the two strings “5” + “5” gives “55”, the text glued together.
This matters whenever data comes from outside your program. Form fields, CSV files and web pages deliver everything as text. If you forget to convert with int() or float(), you will get joined-up text instead of sums, or an error. The sample output earlier shows all three results side by side.
The reverse mistake is just as common. A phone number, PIN code or postal code is made of digits, but you never add or average them. Store them as strings. A PIN of 0482 stored as an integer becomes 482, and the leading zero is gone for good.
If there is one lesson from this article that will save you real trouble, it is this: do not store money in floats. It is the most common serious data-type mistake in shops, billing systems and spreadsheets. Here is a test. A shop sells 1,000 sweets at 0.10 each. I added the price one thousand times three ways:
from decimal import Decimal
# A shop sells 1,000 sweets at 0.10 each. Add the prices one by one.
total_float = 0.0
for _ in range(1000):
total_float += 0.10
print("float total :", total_float, "| equals 100.0?", total_float == 100.0)
total_dec = Decimal("0")
for _ in range(1000):
total_dec += Decimal("0.10")
print("Decimal total :", total_dec, "| equals 100?", total_dec == 100)
paise = 0
for _ in range(1000):
paise += 10 # store money as whole paise (integers)
print("integer paise :", paise, "-> rupees", paise / 100)
print()
print("19.99 * 3 =", 19.99 * 3)
print("1999 * 3 paise =", 1999 * 3, "-> 59.97")
Output:
float total : 99.9999999999986 | equals 100.0? False
Decimal total : 100.00 | equals 100? True
integer paise : 10000 -> rupees 100.0
19.99 * 3 = 59.97
1999 * 3 paise = 5997 -> 59.97
The float total is 99.9999999999986 and fails the check against 100. The Decimal type, which stores decimal digits exactly, gets 100.00. And whole units, in this case paise, get exactly 10,000, which is 100 rupees. The tiny 0.0000000000014 error would never show up on a receipt, but it can break an equality check, trip a reconciliation report or make a daily total off by a cent when multiplied by millions of transactions.
To be fair, floats do not always misbehave. At the bottom of the output, 19.99 × 3 prints a clean 59.97. That is the danger: it works nearly every time, so you trust it, and then one day it does not. Whenever correctness matters, use one of two safe approaches:
You do not need to be a programmer to meet these types. Click a tab to see five everyday situations and which type fits each one, and why.
A cricket ground counts spectators through the turnstiles. What type should the counter be?
The right type. Counting whole things is the textbook job for an integer. Every entry adds exactly 1, and the total is exact no matter how many people arrive. The only thing to check is the width: a 16-bit counter tops out at 32,767, which a big stadium would pass, so a 32-bit or 64-bit integer is the safe choice.
A shop sells an item for 19.99 rupees. How should the price be stored so the day’s total is right?
The right type. Store 1999 paise as an integer, calculate in paise and divide by 100 only for display. In my test, adding 0.10 a thousand times in a float gave 99.9999999999986, while adding 10 paise a thousand times gave exactly 10,000. A decimal type is the other safe route.
A thermometer app records 36.6 degrees Celsius. Which type fits?
The right type. A temperature is a measurement, so a float is right: it can hold a fraction, and a tiny error in the sixteenth digit is irrelevant for a thermometer that is only accurate to a tenth of a degree. When checking whether a reading equals a target, test whether it is close enough instead of testing for exact equality.
A website needs to remember whether a visitor is signed in. What type holds that fact?
The right type. A yes/no fact is a boolean. It decides whether to show the account page or the login form. The classic trap is a setting saved as text: in Python, bool(“False”) is True, because any non-empty string counts as true. Convert text to a boolean on purpose.
A form collects a phone number such as 09876543210. Should it be a number type?
The right type. No. You never add or average phone numbers, so they are labels, and labels belong in strings. Stored as an integer, the leading zero disappears and the value changes. The same advice covers PIN codes, postal codes, account numbers and ID numbers.
When I design a new database table or a form, I go through the same short list. Read the questions top to bottom and stop at the first yes.
Programs constantly change a value from one type to another, and some conversions lose information. Knowing which is useful.
If you remember only one picture, make it this. An integer is a staircase: you are on step 4 or step 5, never in between, and each step is exactly where it says it is. A float is a ramp with markers painted on it: you can stand anywhere, but the markers are only approximate, and they are spaced further apart as you climb. A boolean is a light switch: on or off, nothing else. Pick the staircase when you count, the ramp when you measure and the switch when you decide.
Try each one before you tap to check.
Integer. You count whole items, and there is no such thing as 2.5 items.
2.5 and 2. The single slash gives a float, and the double slash divides and keeps only the whole part.
One tenth has no exact binary form, so the stored 0.1 and 0.2 are both slightly off. Their sum is 0.30000000000000004.
−128. The pattern 01111111 becomes 10000000, which is read as the most negative value.
2. In Python, True counts as 1 and False counts as 0.
A string. You never do arithmetic on a PIN, and storing it as an integer would drop the leading zero and leave 482.
As the integer 1999 paise, or as a Decimal. Divide by 100 only when you display it.
Tap a question to reveal the answer. The correct option is marked.
People come in whole units, so an integer is exact and natural.
It wraps around. Binary 01111111 plus one is 10000000, which a signed integer reads as −128.
One tenth repeats forever in binary, so it must be cut off. The tiny errors show up in the result.
Any non-empty string counts as true, including the text “False”. Convert text to a boolean deliberately.
Whole units are exact. Floats are not, as 1,000 additions of 0.10 showed: 99.9999999999986.
An integer is a whole number with no fractional part and is stored exactly. A float has a decimal point and is stored as digits plus an exponent, which allows fractions and huge ranges but only approximate accuracy.
Computers store numbers in binary. Some decimals, such as 0.1, repeat forever in binary, so they are cut off and stored as a nearby value. The small error can appear after calculations.
A boolean holds true or false and drives decisions in code. Every if statement and loop condition ends up as a boolean, and flags such as is_logged_in or has_paid use the type directly.
When a calculation produces a value too large for the integer’s fixed width. In many languages it wraps around, so the biggest positive value plus one becomes the most negative value.
Both are floating-point types. Float usually means 32 bits, with about 7 reliable digits, and double means 64 bits, with about 15. Most modern code uses double unless memory is tight. Python’s float is a 64-bit double.
Most do, but names and behaviour differ. Python integers grow without a fixed limit, C and Java use fixed widths, and JavaScript has a single number type that is a 64-bit float. Always check how your language handles each type.
The post Data Types Explained: Integers, Floats, Booleans and Why They Differ appeared first on Learn With Examples.
]]>The post How a Phone’s Contact Search Finds Matches as You Type appeared first on Learn With Examples.
]]>You type the letter “m” and Mom appears. You add an “a” and the list shrinks. By the third letter, the person you wanted is already on screen and you have not finished typing. It feels like magic, but it is a handful of well-known tricks working together, and every one of them is something you can build yourself in an afternoon. This article takes the search apart piece by piece, with working code, real measurements and everyday pictures.
Search-as-you-type looks simple until you ask what the phone is really being asked to do. Every single keystroke is a new question. By the time you type the fourth letter of a name, the phone has answered four separate searches, and it has to finish each one before your finger lands on the next key. Usability researchers have used a rule of thumb for decades: if a response arrives in about a tenth of a second, people experience it as instant. Anything much slower and the interface starts to feel like it is lagging behind you.
Now picture the other side of the problem. A phone might hold a few hundred contacts, but plenty of people have thousands, once you count work contacts, old numbers and group chats. And each contact is not just a name. It has several numbers, an email, a nickname, a company. The search has to be fast, forgiving, and sensible about which result to put first.
I have built search boxes for products of every size over the years, and I always start new engineers with this exact exercise. It teaches normalisation, data structures, ranking and the limits of “just loop over everything” in one small, familiar problem.
The short version. A contact search cleans up what you type (lowercase, no accents), looks it up in an index built ahead of time instead of scanning every contact, ranks the matches so the most likely person comes first, and can forgive small typos. Apps differ in the details, and the vendors do not publish their exact internals, but these are the standard building blocks, and they are what we will build here.
Whatever the app, the path from keystroke to list looks about the same.
Let us walk through the stages in order, starting with the most naive version that works, because the way it fails shows you why each later trick exists.
Before any matching happens, both the typed text and the stored names need to be put into the same shape. People type “jose” for José, they type in lower case, and they put dots and dashes into names such as “Anne-Marie” or “O’Brien”. If you compare raw text, none of those match.
So the first stage lowercases everything, strips accents, replaces punctuation with spaces and splits the name into words. That is called normalisation, and it is the unglamorous step that makes the rest work. Skipping it is the number one reason homemade search feels “broken.”
Below is a first version of the whole search in about twenty lines. It normalises, then checks every contact. A contact matches when every word you typed is the beginning of some word in the name.
import unicodedata, re
def normalise(text):
"""Lowercase, strip accents, turn punctuation into spaces."""
text = unicodedata.normalize("NFKD", text)
text = "".join(c for c in text if not unicodedata.combining(c))
return re.sub(r"[^a-z0-9 ]+", " ", text.lower()).split() # list of words
def linear_search(contacts, query):
"""Slowest idea that works: look at every contact on every keystroke."""
q = normalise(query)
hits = []
for cid, words in contacts: # contacts = [(id, [words...]), ...]
# every typed word must be the start of some word in the name
if all(any(w.startswith(t) for w in words) for t in q):
hits.append(cid)
return hits
This already behaves like a real contact search. Type “sh” and you find both Onam Sharma (the surname starts with sh) and Shalini Rao (the first name does). Type “on sh” and only Onam Sharma remains, because each typed word must begin some word in the name. Matching at the start of any word is what makes phone searches feel natural: you can type the first or the last name, in any order.
Here is something satisfying to see. I generated 5,000 made-up contacts (random first and last names) and ran the search above while “typing” the name anit, one letter at a time. My test names lean heavily towards names beginning with A, so the early numbers are larger than a real address book would give, but the shape is what matters.
This shrinking is the whole reason search-as-you-type works so well. The first letter does the least work and returns the longest list, which the phone only needs to display the top of. Every next letter cuts the list further, and by the fourth or fifth letter you are usually looking at a handful of names.
It also hides a second, very practical trick. Every result for “ani” is guaranteed to also be a result for “an”, because anything starting with “ani” also starts with “an”. So instead of searching all contacts again, you can search only the list you already have. I counted the name checks for typing anit across 10,000 contacts: scanning everything on every keystroke took 40,000 checks, while reusing the previous results took 13,852. That is about two-thirds fewer, for a change of a few lines of code.
The catch. This only works while the user keeps typing forward. The moment someone presses backspace, the saved list is too small, and you must go back to an earlier result or start over. Good apps keep a short history of the lists for exactly that reason.
The scan-and-reuse approach is fine for a thousand contacts. But it still touches every candidate on the first keystroke, and the cost grows with the size of the list. Real search systems avoid scanning entirely. They do the hard work once, ahead of time, and build an index, which is a structure designed to answer one specific question quickly. For “which words start with these letters?” the classic answer is a trie.
A trie (said “try”) is a tree where each step down spells one more letter. All words that share a start share the same path. To find everything starting with “ani”, you walk three steps down and you are standing on the answer: every contact reachable from that spot begins with those letters. You never look at the other thousands of contacts at all.
The cost of a lookup depends on the length of what you typed, not on how many contacts you own. A person with 100 contacts and a person with 100,000 both pay the same few steps for “ani”. That is the property that makes it scale.
Here is a compact version. In this one, every node remembers the set of contacts passing through it, so reading the answer is instant:
class Trie:
"""Every node remembers which contacts pass through it."""
def __init__(self):
self.root = {}
def add(self, word, cid):
node = self.root
for ch in word:
node = node.setdefault(ch, {})
node.setdefault("#", set()).add(cid) # '#' holds contact ids
def prefix(self, text):
node = self.root
for ch in text:
node = node.get(ch)
if node is None:
return set()
return node.get("#", set())
def build(contacts):
t = Trie()
for cid, words in contacts:
for w in words:
t.add(w, cid)
return t
def trie_search(t, query_words):
sets = [t.prefix(w) for w in query_words]
return set.intersection(*sets) if sets else set()
The trade-off is memory. The trie stores extra information so lookups are fast, which is the oldest bargain in computing: spend space and preparation time to buy speed.
If a trie sounds like more machinery than you want, there is a leaner way to get the same effect. Keep every word in one alphabetically sorted list. All words starting with “ani” sit together in one block, because they sort together. Use binary search to find where that block begins and ends, which takes only about seventeen steps even for a hundred thousand words. If you read our article on heaps and sorted arrays, this is the same “sorted array” idea put to a different job. It uses less memory than a trie and is easier to write, and it is a perfectly good choice.
I built all three versions (scan, sorted list with binary search, and trie), checked that they return identical results on 500 random queries, and then timed them. The test is typing the four letters anit, taking the best of five runs, with different address book sizes. These figures come from Python on a cloud machine, so a real phone running compiled code will be quicker in absolute terms. What matters is the ratio.
| Contacts | Scan everything | Sorted list + binary search | Trie | Trie vs scan |
|---|---|---|---|---|
| 1,000 | 3.13 ms | 0.025 ms | 0.011 ms | 275x faster |
| 10,000 | 32.87 ms | 0.28 ms | 0.089 ms | 368x faster |
| 100,000 | 345.59 ms | 8.18 ms | 1.31 ms | 263x faster |
Look at the first row of the table. With 1,000 contacts, every method finishes in a few milliseconds or less, and nobody would notice a difference. That is the honest takeaway: for a normal-sized address book, even the naive scan is fast enough. The index only earns its keep as the data grows. At 100,000 entries the scan needed about a third of a second to answer four keystrokes, which is long enough to feel sluggish, while the trie needed about a millisecond.
Name search matches the start of words. Number search needs different rules, because people remember numbers differently. They recall the last four digits, or the number without the country code, or they type it with spaces and dashes that were never stored.
The standard solution is to normalise numbers the same way we normalised names. Strip everything that is not a digit, and maybe drop a leading country code such as +91, so that “+91 98765-43210”, “098765 43210” and “9876543210” all become the same string. Then, instead of matching only at the beginning, you let the typed digits match anywhere in the number, or at the end for the “last four digits” habit.
Because digits have only ten possible characters, a similar trie or sorted index works well. For suffix searching, you can index the reversed number so “last four digits” becomes an ordinary prefix lookup.
Finding matches is only half the job. If typing “sha” returns thirty people in alphabetical order, the one you wanted may be on the third screen. So the list has to be ranked.
The idea is to give every match a score from several signals, then sort by score. The signals I always start with are: how well the text matches (the whole name starts with it, a word starts with it, or it appears in the middle), whether you marked the person as a favourite, and how often or how recently you contact them.
from norm import normalise
def score(contact, query):
"""Higher is better. contact = dict(name, favorite, calls)."""
words = normalise(contact["name"])
q = normalise(query)
s = 0
if " ".join(words).startswith(" ".join(q)):
s += 100 # the whole name starts with what you typed
elif all(any(w.startswith(t) for w in words) for t in q):
s += 60 # every typed word starts some word in the name
elif all(t in " ".join(words) for t in q):
s += 20 # typed text appears inside a word
else:
return 0
s += 30 if contact["favorite"] else 0
s += min(contact["calls"], 50) * 0.5 # frequent contacts float up
return s
contacts = [
{"name": "Shalini Rao", "favorite": False, "calls": 2},
{"name": "Onam Sharma", "favorite": False, "calls": 40},
{"name": "Sharad Verma", "favorite": True, "calls": 12},
{"name": "Ashwin Shah", "favorite": False, "calls": 5},
{"name": "Harsh Gupta", "favorite": False, "calls": 60},
]
query = "sha"
ranked = sorted(((score(c, query), c["name"]) for c in contacts), reverse=True)
for s, name in ranked:
if s: print(f"{s:6.1f} {name}")
Running it for the typed text “sha” gives:
136.0 Sharad Verma
101.0 Shalini Rao
80.0 Onam Sharma
62.5 Ashwin Shah
Notice the order. Sharad Verma is first because his name starts exactly with “sha” and he is a favourite. Shalini Rao is next on pure match quality. Onam Sharma follows, as only the surname starts with “sha”, even though he is called often. Ashwin Shah comes last of the matches, and Harsh Gupta does not appear at all because “sha” never occurs in his name. The weights (100, 60, 20, 30 and the call bonus) are just numbers I chose to demonstrate. In a real product, teams tune them with usage data, and that tuning is the difference between a search that is merely correct and one that feels uncannily helpful.
Thumbs are clumsy and names have many spellings. Is it Anita or Anitha? Sunita or Suneeta? Strict prefix matching treats any typo as a complete failure. Fuzzy matching asks a gentler question: how many small edits would it take to turn what you typed into this name?
The classic measure is edit distance, also called Levenshtein distance, which counts single-letter insertions, deletions and replacements. Here it is in a few lines:
def edit_distance(a, b):
"""Minimum single-letter edits (insert, delete, replace) to turn a into b."""
prev = list(range(len(b) + 1))
for i, ca in enumerate(a, 1):
cur = [i]
for j, cb in enumerate(b, 1):
cur.append(min(prev[j] + 1, cur[j-1] + 1, prev[j-1] + (ca != cb)))
prev = cur
return prev[-1]
names = ["Anita", "Amit", "Anil", "Ankit", "Anjali"]
typed = "Anitha" # another common spelling of "Anita"
for name in sorted(names, key=lambda n: edit_distance(typed.lower(), n.lower())):
print(f"{name:<7} distance {edit_distance(typed.lower(), name.lower())}")
The output:
Anita distance 1
Amit distance 3
Anil distance 3
Ankit distance 3
Anjali distance 4
“Anitha” is one deletion away from “Anita”, so Anita comes first, and everything else is at least three edits away. A search that falls back to fuzzy matching only when the exact search finds nothing gives you the best of both: exact results are fast and clean, and typos still find someone.
One small warning from experience: in plain edit distance, swapping two neighbouring letters (typing “Aniat” for “Anita”) costs two edits, not one, even though it is the most common typing mistake. A variant called Damerau-Levenshtein counts a swap as one. Also, comparing your text against every contact is slow, so real systems use fuzzy matching sparingly, usually on a shortlist, or use clever indexes built for it.
Many phone dialers let you type digits on the number pad and show contacts whose names could be spelled with those keys. It is the old keypad letter layout (2 for ABC, 3 for DEF and so on) doing a modern job.
KEYS = {"2":"abc","3":"def","4":"ghi","5":"jkl","6":"mno","7":"pqrs","8":"tuv","9":"wxyz"}
LETTER = {ch: d for d, chars in KEYS.items() for ch in chars}
def to_digits(name):
return "".join(LETTER[c] for c in name.lower() if c in LETTER)
for name in ["Anita", "Amit", "Anil", "Ankit"]:
print(f"{name:<6} -> {to_digits(name)}")
typed = "26" # you pressed 2 then 6 on the dialer
matches = [n for n in ["Anita", "Amit", "Anil", "Ankit", "Rahul"] if to_digits(n).startswith(typed)]
print("typed", typed, "matches", matches)
Anita -> 26482
Amit -> 2648
Anil -> 2645
Ankit -> 26548
typed 26 matches ['Anita', 'Amit', 'Anil', 'Ankit']
Pressing 2 then 6 matches every name whose first letter lives on the 2 key and whose second letter lives on the 6 key, which is why Anita, Amit, Anil and Ankit all appear. Add a 4 and Ankit drops out, because its third letter, K, sits on the 5 key. Add an 8 and Anil drops out too, leaving Amit and Anita. Working a few of these by hand is a good way to check that you really understand the mapping.
This pattern appears far beyond contacts. Click a tab to see it in other places.
You open the phone app and type three letters of a friend’s name.
How it searches. Everything lives on the device, so the lookup is local and needs no network. The list matches at the start of any word in the name, can also match digits in a number, and ranks favourites and frequently called people first. The exact internals vary between phone makers and are not published, but this is the standard design.
You start typing a colleague’s name in the To field and a small list drops down.
How it searches. The suggestions are usually drawn from people you have written to and from your address book, then ranked so recent and frequent contacts come first. Matching works on both name and email address, so typing the start of either works. The list is kept short, because a dropdown of fifty names would be slower to scan than to type.
You type into a shop’s search bar and suggestions appear below it.
How it searches. Here the index lives on a server, so every request costs real time. Pages wait a moment after you stop typing (debouncing) and cancel old requests so the suggestions always match the latest text. The suggestions often come from a prefix index of popular searches, which is the same idea as our trie.
You type “gandhi mar” and the app guesses a road you have never spelled in full.
How it searches. Place search has to cope with millions of names, so it relies on prebuilt indexes, not scans. It typically ranks results by closeness to you or to the map you are viewing, as well as text match, and tolerates misspellings. It shows why ranking signals beyond the text itself matter once the list is large.
You type get in a code editor and it offers getUser, getUserName and getUsers.
How it searches. Editors index the names in your project and match what you type against them as you go. Many also match on initials, so typing “gUN” can find getUserName. Ranking uses context, such as which names are valid at the cursor, and how recently you used them. It is search-as-you-type applied to code.
Everything so far searched a local list, where each lookup takes microseconds. Things change when the search has to ask a server, such as a company directory with a million staff or a search box on a website. Now each keystroke could trigger a network request taking 100 to 500 milliseconds, and two new problems appear.
Phone contact lists are local, so they rarely need debouncing. A server-backed search box almost always does. Knowing which situation you are in is half the design.
| Your situation | Good choice | Why |
|---|---|---|
| A few hundred or a few thousand items | Simple scan, plus reuse of previous results | Fast enough, minimal code, easy to debug. |
| Tens of thousands of items, prefix search | Sorted list with binary search | Lean on memory and quick to write. |
| Very large lists, instant response needed | Trie or similar index | Lookup cost depends on typed length, not list size. |
| Users make typos | Exact search first, fuzzy fallback | Keeps the common case fast and still forgives mistakes. |
| Results must feel “smart” | Ranking by match quality plus usage | Puts the likely person first. |
| Data lives on a server | Debounce and cancel old requests | Saves bandwidth and avoids wrong-order results. |
Tap a question to reveal the answer. The correct option is marked.
Anything that starts with “ani” also starts with “an”, so the longer search can run on the shorter search’s results. It stops working when the user presses backspace.
You walk one step per typed letter, so the cost follows the typed length, not the size of the list.
Finding matches is only half the job. Scoring by match quality, favourites and how often you contact someone puts the likely person first.
Waiting a short moment after the last keystroke cuts the number of requests sharply, and cancelling old requests keeps stale replies off the screen.
Edit distance counts the small changes between two strings. “Anitha” is one deletion away from “Anita”, so it ranks first.
It does the heavy work ahead of time. Names are cleaned up and stored in an index, so each keystroke is a quick lookup instead of a scan of every contact. Results are then ranked so the likely person comes first.
A tree where each step down spells one more letter. Words that start the same share the same path, so finding everything that begins with a few letters means walking down those letters and reading what lies below.
Good contact search matches the start of any word in the name, not just the first word. That lets you type either the first or the last name.
Usually not. With a few thousand items, a simple scan is fast enough. In my test, even 10,000 contacts took only a few milliseconds per keystroke for the naive version in Python. Build an index when the data is large or the response must be instant.
Many try an exact prefix search first and, only if that finds little, fall back to fuzzy matching such as edit distance. That keeps common searches fast while still forgiving typos.
Debouncing waits for a pause in typing before sending a request, which reduces how many requests you make. Caching remembers earlier answers so repeating a search needs no new work. Real search boxes often use both.
The post How a Phone’s Contact Search Finds Matches as You Type appeared first on Learn With Examples.
]]>The post Top 20 Networking Interview Questions with Plain-English Answers appeared first on Learn With Examples.
]]>I have sat on both sides of the interview table for a long time, and I can tell you something that surprises most candidates: interviewers rarely want a textbook definition. They want to hear whether you actually understand what happens when two computers talk. This guide gives you the twenty questions that come up again and again, each answered the way you would say it out loud, with a real-world picture, a “what the interviewer wants to hear” note, and a few small programs whose output I ran so the numbers are real.
Read the questions in order the first time, because later answers lean on earlier ones. Subnets make more sense after IP addresses, and the “what happens when you type a URL” question ties together half the list. After that, use the headings to jump to whatever you are weakest on.
The one rule for answering. Give the one-sentence definition first, then a quick real-world picture, then one detail that proves you have actually used it. Three short layers beat a long speech. If the interviewer wants more, they will ask, and that is when the conversation gets good.
Questions 1 to 5 cover the foundations. Questions 6 to 11 cover the protocols that make the internet work. Questions 12 to 15 cover devices and routing, and 16 to 20 cover security and troubleshooting.
A network is two or more devices connected so they can exchange data. Everything else is about scale. A LAN (local area network) covers a small area such as a home, an office floor or a school lab. A MAN (metropolitan area network) spans a city or a campus. A WAN (wide area network) links networks across cities or countries, and the internet is the largest WAN there is.
The OSI model is a seven-layer teaching framework for how data travels. Each layer does one job and talks only to the layers directly above and below it. The TCP/IP model is the four-layer version the real internet is built on. Interviewers love asking for both because the layers give you a map for troubleshooting.
A handy memory trick for the OSI layers from the top is: All People Seem To Need Data Processing (Application, Presentation, Session, Transport, Network, Data Link, Physical). In practice, people use the layer numbers as shorthand. When an engineer says “layer 2 problem,” they mean something about switches and MAC addresses. “Layer 3” means IP and routing, and “layer 4” means TCP or UDP ports.
An IP address is the logical address of a device on a network, used to route data to it. IPv4 addresses are 32 bits long, written as four numbers such as 192.168.1.10, giving about 4.29 billion possible addresses (232 is exactly 4,294,967,296). That turned out not to be enough for a world of phones and sensors. IPv6 addresses are 128 bits, which gives roughly 3.4 × 1038 addresses, an almost unimaginable number.
Private addresses are reserved for use inside networks and are not routed on the public internet. The three ranges are 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16. Public addresses are unique across the internet. Because many homes reuse the same private ranges, NAT (Question 13) lets them all share one public address.
A subnet mask tells a device which part of an IP address identifies the network and which part identifies the host. The mask 255.255.255.0 means the first three numbers name the network and the last one names the device. Engineers also write it as CIDR notation: 192.168.10.0/24, where 24 is the count of network bits.
Subnetting means cutting one large network into smaller ones, usually for tidiness, security or to reduce broadcast traffic. The useful formula: a network with a prefix length of n has 2(32 − n) addresses, and two of those are reserved (the network address and the broadcast address), so the usable host count is 2(32 − n) − 2. A /24 gives 254 hosts. A /26 gives 62.
Here is a short Python program that does the arithmetic with the standard ipaddress module. It splits a /24 into four /26 subnets.
import ipaddress
net = ipaddress.ip_network("192.168.10.0/24")
print(net, "has", net.num_addresses, "addresses,", net.num_addresses - 2, "usable hosts")
print("mask:", net.netmask)
print()
print("Split into four /26 subnets:")
for s in net.subnets(new_prefix=26):
hosts = list(s.hosts())
print(f"{str(s):<18} hosts {hosts[0]} - {hosts[-1]} broadcast {s.broadcast_address}")
Output:
192.168.10.0/24 has 256 addresses, 254 usable hosts
mask: 255.255.255.0
Split into four /26 subnets:
192.168.10.0/26 hosts 192.168.10.1 - 192.168.10.62 broadcast 192.168.10.63
192.168.10.64/26 hosts 192.168.10.65 - 192.168.10.126 broadcast 192.168.10.127
192.168.10.128/26 hosts 192.168.10.129 - 192.168.10.190 broadcast 192.168.10.191
192.168.10.192/26 hosts 192.168.10.193 - 192.168.10.254 broadcast 192.168.10.255
Read each line as a mini network: the first usable host, the last usable host, and the broadcast address at the end. Interviewers who ask this question often give you a block like 192.168.10.0/24 and ask you to carve out four equal subnets. The answer is the table above, and the trick is that each /26 holds 64 addresses, so the blocks start at 0, 64, 128 and 192.
A MAC address is a 48-bit hardware identifier burned into a network card, written like 3C:52:82:AB:12:EF. It is used for delivery within one local network. An IP address is assigned by software or a DHCP server, can change, and is used to route data between networks.
One more detail worth saying: as a packet crosses several routers, the IP addresses at both ends stay the same, but the MAC addresses are rewritten at every hop. That single sentence tells an interviewer you understand how layers 2 and 3 cooperate.
TCP is connection-oriented. It sets up a connection first, numbers the data, retransmits what is lost and delivers it in order. UDP is connectionless. It just sends packets with no handshake, no retransmission and no ordering guarantee, so it is faster and lighter.
| Feature | TCP | UDP |
|---|---|---|
| Connection | Handshake first | None |
| Delivery | Guaranteed and ordered | Best effort |
| Speed | Slower, more overhead | Faster, less overhead |
| Used for | Web pages, email, file transfer, SSH | DNS lookups, voice and video calls, games, streaming |
Before TCP sends any data, the two sides agree to talk. The client sends a SYN (“I want to connect, my starting sequence number is X”). The server answers with SYN-ACK (“Got it, here is mine, Y”). The client finishes with an ACK (“Got yours too”). Now both sides know the other is alive and reachable, and they have agreed on sequence numbers for tracking data.
A bonus line that impresses: a SYN flood attack sends huge numbers of SYNs and never completes the handshake, leaving the server holding half-open connections. Defences include SYN cookies and rate limits.
DNS, the Domain Name System, is the internet’s phone book. People remember names like example.com, but computers need IP addresses, and DNS translates one into the other.
When your device needs an address, it asks a recursive resolver, usually run by your ISP or a public DNS service. If the resolver does not already have the answer cached, it asks a root server, which points it to the server for the top-level domain (.com, .org, .in). That server points to the domain’s authoritative server, which gives the real answer. The resolver returns it to you and remembers it for the record’s time to live (TTL).
Know the common record types: A (name to IPv4), AAAA (name to IPv6), CNAME (an alias to another name), MX (mail servers) and NS (which servers are authoritative). Plain DNS uses UDP port 53, and TCP port 53 is used for large responses.
DHCP, the Dynamic Host Configuration Protocol, automatically gives a device an IP address, subnet mask, default gateway and DNS servers when it joins a network. The exchange is nicknamed DORA: the client broadcasts a Discover, the server sends an Offer, the client sends a Request for that offer, and the server sends an Acknowledge. The address is a lease, not a permanent grant, and the client renews it before it expires.
On a local network, devices deliver frames by MAC address, but programs only know IP addresses. ARP, the Address Resolution Protocol, bridges the gap. When your PC wants to reach 192.168.1.1 and does not know its MAC, it broadcasts “Who has 192.168.1.1?” to everyone. The owner replies with its MAC address. Your PC stores the answer in its ARP cache for a while, so it does not need to ask again.
The classic security follow-up is ARP spoofing: an attacker sends false ARP replies so that traffic meant for the gateway flows through their machine. Managed switches can defend against it with features such as dynamic ARP inspection.
Here is the version I recommend you tell, in order:
GET / with headers.Mention that on your local network, the first packet needs ARP to find the gateway’s MAC address, and that routers along the way forward the packet by IP. That shows you can connect every layer in one story.
| Device | Works at | How it forwards | Intelligence |
|---|---|---|---|
| Hub | Layer 1 | Repeats every signal out of every port | None, and now rare |
| Switch | Layer 2 | Learns MAC addresses and sends frames only to the right port | Medium |
| Router | Layer 3 | Looks at IP addresses and connects different networks | High |
NAT, Network Address Translation, lets many devices with private addresses share one public IP address. The router rewrites the source address and port of outgoing packets and keeps a table. When replies come back, it looks up the table and forwards them to the right internal device. The common form, where ports tell the devices apart, is often called PAT or NAT overload.
Know the side effects. NAT helped the world survive IPv4 exhaustion, and it hides internal addresses from the outside. But it is not a firewall, and it breaks anything that needs inbound connections, which is why we use port forwarding to reach a server inside a home network.
A default gateway is the router a device uses for any destination outside its own subnet. Your laptop does not know how to reach the whole internet, so it hands everything non-local to the gateway.
A router keeps a routing table, a list of destination networks and where to send traffic for each. When a packet arrives, the router finds every entry that matches the destination and picks the most specific one, meaning the longest prefix. That is the longest prefix match. The entry 0.0.0.0/0 is the default route and matches everything, but because it is the least specific, it only wins when nothing better matches.
Here is a tiny router in Python with four routes:
import ipaddress
routes = {
"0.0.0.0/0": "ISP (default route)",
"10.0.0.0/8": "Head office core",
"10.1.0.0/16": "Branch office",
"10.1.2.0/24": "Branch lab network",
}
def lookup(dest):
ip = ipaddress.ip_address(dest)
best = None
for prefix, hop in routes.items():
net = ipaddress.ip_network(prefix)
if ip in net and (best is None or net.prefixlen > best[0].prefixlen):
best = (net, hop)
return best
for dest in ["10.1.2.50", "10.1.9.9", "10.7.7.7", "8.8.8.8"]:
net, hop = lookup(dest)
print(f"{dest:<10} matches {str(net):<12} -> {hop}")
Output:
10.1.2.50 matches 10.1.2.0/24 -> Branch lab network
10.1.9.9 matches 10.1.0.0/16 -> Branch office
10.7.7.7 matches 10.0.0.0/8 -> Head office core
8.8.8.8 matches 0.0.0.0/0 -> ISP (default route)
Look at the first line. The address 10.1.2.50 matches the /8, the /16 and the /24, and the /24 wins because it is the most specific. The last line matches only the default route, which sends it to the ISP.
One more thing interviewers like: routes can be static (written by an administrator) or dynamic (learned from other routers using protocols such as OSPF inside an organisation and BGP between internet providers). Static is simple and predictable. Dynamic adapts when links fail.
A VLAN, virtual LAN, splits one physical switch into several logical networks. Devices in different VLANs cannot talk directly even if they are plugged into the same switch, and they need a router (or a layer 3 switch) to communicate. Frames carry a VLAN tag following the 802.1Q standard when they travel between switches.
The reasons to use them are security (keep guest Wi-Fi away from the finance team), performance (smaller broadcast domains) and tidiness (group devices by function, not by cable location).
A firewall filters network traffic by rules, allowing or blocking it based on things like source, destination, port and protocol. A stateless firewall checks each packet alone against the rules. A stateful firewall tracks connections, so it knows that an incoming packet is a reply to something you started, and it allows it automatically. Modern “next-generation” firewalls also look at applications and can detect certain threats.
The principle to quote is default deny: block everything, then allow only what you need. That way, a mistake leaves you too closed instead of too open.
HTTP sends web traffic in plain text on port 80, so anyone on the path can read or alter it. HTTPS is the same protocol carried inside TLS encryption, usually on port 443. TLS gives you three things: confidentiality (nobody can read it), integrity (nobody can change it unnoticed) and authentication (a certificate proves you reached the real site).
If asked how the handshake works, a safe outline is: the client says hello and lists its supported options, the server answers with its certificate, the client checks the certificate against trusted authorities, and both sides agree on session keys, which then encrypt the data. Modern TLS also uses temporary keys, so recorded traffic stays unreadable even if a server key leaks later.
A VPN creates an encrypted tunnel between your device and a VPN server, and typically sends all your traffic through it. Companies use VPNs so remote staff can reach internal systems as if they were in the office. A proxy is a server that makes requests on your behalf, usually for particular applications, and it may not encrypt anything. A VPN protects the whole path to the VPN server. A proxy mainly changes where the request appears to come from or adds filtering and caching.
Be careful with claims. A VPN hides your traffic from the local network and your ISP, but the VPN provider can see it instead. It does not make you anonymous.
Ping sends a small ICMP echo request and waits for a reply. It tells you whether a host is reachable and how long the round trip takes. Traceroute (called tracert on Windows) lists the routers on the path by sending packets with increasing TTL values, so each router in turn reports back when the TTL runs out. It shows you where along the path things slow down or stop.
My own habit is a four-step ladder, working outward until something fails:
Also check the basics early: is the address a 169.254 one (DHCP failed), is the cable plugged in, and did anything change recently? In my experience, most “mysterious” outages are one of those three.
Bandwidth is the maximum amount of data a link can carry per second, like the number of lanes on a highway. Latency is how long a single piece of data takes to get there, like the time for one car to complete the trip. Throughput is the amount that actually gets through in practice, which is usually lower than bandwidth because of congestion, errors and protocol overhead.
It is worth adding that a bigger plan does not fix a latency problem. When someone says “my connection is fast but pages feel slow,” look at latency, DNS and packet loss before blaming bandwidth.
The twenty questions above are common, but interviewers adjust the depth to the job. Click a tab to see what each role is really probing for.
You are the first person someone calls when their laptop cannot reach anything.
What they probe. Expect Questions 3, 9, 10 and 19 in some form. They want a calm method: check the cable or Wi-Fi, look for a 169.254 address, ping the gateway, then a public IP, then a name. Say each step out loud and what a failure at that step would mean.
You design and maintain the switches, routers and links that a company depends on.
What they probe. Expect deep dives into Questions 4, 12, 14 and 15: carve a block into subnets on a whiteboard, explain how a routing table picks a path, how a VLAN trunk works, and what happens when a link fails. Quote real protocols and mention labs you have built.
You write services that talk to databases, APIs and browsers across a network.
What they probe. The URL question (Q11) is almost guaranteed. Be ready for follow-ups on TCP versus UDP, what a timeout or a connection reset means, how DNS caching can cause stale answers after a deploy, and how HTTPS certificates fail. Latency and connection reuse come up often.
You build and run infrastructure in a cloud, where networks are made from settings, not cables.
What they probe. Subnetting and CIDR (Q4) matter a lot, because you will plan address ranges for virtual private clouds. Expect NAT (Q13), firewalls and security groups (Q16), DNS records (Q8), and a scenario such as why a private server cannot reach the internet. The answer often involves a missing route or NAT gateway.
You watch traffic for things that should not be there and harden what is exposed.
What they probe. Expect Questions 7, 10, 16, 17 and 18 with an attacker’s twist: SYN floods, ARP spoofing, why HTTP is unsafe, what a VPN does and does not hide, and how stateful firewalls track connections. Strong answers always state both the attack and the defence.
If you have two weeks, here is the order I recommend. Spend the first few days on layers, IP, subnets and TCP versus UDP, and practise subnetting by hand until the powers of two come automatically. In the middle days, trace one full web request (Question 11) on paper, then capture a real one using a packet analyser such as Wireshark and watch the DNS query, handshake and TLS hello appear. In the last days, build a small lab. The free Packet Tracer simulator from Cisco Networking Academy is excellent for this, and you can also run a few Linux virtual machines. Break something on purpose, then fix it using the ping ladder.
Nothing replaces hands-on practice. The people who stand out are the ones who say “when I set this up at home, I noticed…” because that cannot be memorised.
Tap a question to reveal the answer. The correct option is marked.
A /26 has 2^(32-26) = 64 addresses. Two are reserved for the network and broadcast addresses, leaving 62.
169.254.x.x is the link-local fallback used when no DHCP server answers.
All three match, but the longest prefix (/16) is the most specific, so it wins.
A switch is a layer 2 device that learns MAC addresses and sends each frame only to the right port.
The client sends SYN, the server replies SYN-ACK, and the client finishes with ACK.
It depends on the role. For a general IT or developer job, expect a handful of fundamentals such as TCP versus UDP, DNS and the URL question. For a network engineer role, expect a long technical session with subnetting, routing and troubleshooting scenarios.
It helps for network engineering and support roles because it shows structured knowledge and gives you lab practice. It is rarely required for developer roles, where clear understanding of the basics matters more.
Use a free simulator such as Cisco Packet Tracer, run a few virtual machines on your own PC, and capture real traffic with Wireshark. Breaking and fixing a small lab teaches more than reading.
Say what you do know, reason from the layers, and say how you would find out. Interviewers value honest thinking much more than a confident wrong answer.
Learn the common ones: 22 SSH, 25 SMTP, 53 DNS, 80 HTTP, 443 HTTPS, 3306 MySQL and 3389 RDP. You do not need hundreds, but you do need those, and the reasoning behind them.
The internet runs on TCP/IP, but engineers use OSI layer numbers every day as shorthand for where a problem lives. That makes it worth knowing even though it is a reference model, not a protocol stack.
The post Top 20 Networking Interview Questions with Plain-English Answers appeared first on Learn With Examples.
]]>The post What Is a Port Number? Why 80, 443 and 22 Matter appeared first on Learn With Examples.
]]>An IP address gets data to the right computer. But that computer is running dozens of programs at once: a browser, a mail app, a database, a chat client, an update service. Something has to decide which program receives which piece of data. That something is the port number, and once you understand it, firewalls, port forwarding, “address already in use” errors and half of the security advice you have ever read start making sense.
Think about what happens when you open a website while a video call is running, a music app is streaming and your email is syncing in the background. All of that traffic arrives at your laptop through one network connection, addressed to one IP address. Yet the website data ends up in the browser, the call audio in the call app, and the emails in the mail client. They never get mixed up.
The IP address cannot do that job, because it only identifies the machine. You need a second number that identifies the program or service on that machine. That number is the port.
The one-paragraph version. A port is a 16-bit number, from 0 to 65535, that tells the receiving computer which program or service should get the incoming data. An IP address is the building, and the port is the apartment number. Web servers conventionally listen on 80 (HTTP) and 443 (HTTPS), and remote-login servers on 22 (SSH). A connection is identified by the pair of IP addresses plus the pair of port numbers, which is how thousands of conversations share one network card without colliding.
Imagine you send a parcel to “42 Mall Road.” The courier finds the building, which is what an IP address does. But 42 Mall Road is an apartment block with a hundred flats. Without a flat number the parcel sits in the lobby. Write “Flat 443” on it and the right resident receives it.
The analogy also explains why some flat numbers are famous. Everybody knows the front desk is Flat 1, so visitors do not have to ask. Ports work the same way. If every web server on Earth listens on 443 for secure traffic, your browser can reach any of them without being told the number.
Ports belong to the transport layer of networking, and the two protocols that use them are TCP and UDP.
TCP port 53 and UDP port 53 are different doors that happen to share a number. Each protocol has its own full set of 65,536 ports, which is why you will see written forms like 443/tcp and 443/udp. In fact, modern HTTP/3 uses UDP port 443, while ordinary HTTPS uses TCP port 443.
The port number is stored in 16 bits in the TCP and UDP headers. Sixteen bits give 216, or 65,536 possible values, counting from zero. That is the whole reason for the limit. Port 0 is reserved and is not used for ordinary traffic, so in practice you work with 1 to 65535.
The internet’s numbering authority, IANA, divides the 65,536 numbers into three zones.
| Range | Name | Count | What lives here |
|---|---|---|---|
| 0 – 1023 | Well-known (system) ports | 1,024 | Core services: web, SSH, email, DNS. Assigned by IANA. |
| 1024 – 49151 | Registered ports | 48,128 | Applications and databases, such as MySQL on 3306 or PostgreSQL on 5432. |
| 49152 – 65535 | Dynamic (ephemeral) ports | 16,384 | Temporary numbers your device picks for outgoing connections. |
One caution: operating systems do not all follow the IANA suggestion for temporary ports. Modern Windows uses 49152 to 65535 by default. Linux commonly uses 32768 to 60999, and you can read the exact range in /proc/sys/net/ipv4/ip_local_port_range. That is a tuning detail worth knowing when you debug connection limits.
| Port | Service | What it does |
|---|---|---|
| 22 (TCP) | SSH | Encrypted remote login and file copying. |
| 25 (TCP) | SMTP | Mail transfer between mail servers. |
| 53 (UDP/TCP) | DNS | Turns names like example.com into IP addresses. |
| 80 (TCP) | HTTP | Unencrypted web traffic. |
| 443 (TCP, UDP) | HTTPS | Encrypted web traffic. UDP carries HTTP/3. |
| 587 (TCP) | SMTP submission | Where your mail app sends outgoing mail. |
| 993 (TCP) | IMAPS | Encrypted mailbox access. |
| 3306 (TCP) | MySQL | Default MySQL and MariaDB database port. |
| 3389 (TCP) | RDP | Windows Remote Desktop. |
| 5432 (TCP) | PostgreSQL | Default PostgreSQL database port. |
| 6379 (TCP) | Redis | Default Redis cache and data store port. |
These are conventions, not laws. Nothing in physics forces a web server to listen on 80. It is simply the agreed default, and the agreement is what lets browsers and servers find each other without negotiation.
When you type http://example.com, your browser quietly adds :80. This is plain, unencrypted HTTP. Anyone on the path, such as the coffee shop Wi-Fi owner, can read or modify what travels over it. Today port 80 mostly survives to redirect visitors to the secure version.
When you type https://example.com, the browser adds :443. The same HTTP conversation happens, but wrapped in TLS encryption. This is the port that matters most for any site you run. Search engines, browsers and users all expect it. If you want to run a site that ranks and earns trust, port 443 must be open, and the certificate behind it must be valid.
SSH is how people manage servers from afar. It is also, unfortunately, one of the most attacked doors on the internet. If you have ever put a server online and checked its logs an hour later, you have seen strangers hammering port 22 with guessed passwords. We will come back to what to do about that.
You can use any port in a URL by writing it after a colon. https://example.com:8443 tells the browser to use 8443 instead of 443. Developers do this constantly, and the reason is a ritual every programmer knows: local test servers run on ports like 3000, 5000, 8000 or 8080, so that they do not clash with the real web ports.
Here is the part that clears up the most confusion. When your browser connects to a web server, it uses a port on both sides. The server side is the well-known one (443). Your side is a temporary number the operating system picks from the dynamic range.
A TCP connection is uniquely identified by four values: your IP address, your port, the server’s IP address and the server’s port. Change any one of them and it is a different connection. That is how you can open ten tabs to the same website, all talking to the same server on the same port 443, without the answers colliding. Each tab uses a different source port on your side.
I like proving things in code rather than asking you to believe them. The script below starts a small server, connects to it three times, and prints which ports each side used. Then it tries to start a second server on the same port.
import socket, threading
# 1. A server listens on one port. We let the OS pick a free one (port 0).
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 0))
server.listen()
host, port = server.getsockname()
print("server listening on port", port)
# 2. Three clients connect to that single server port.
def serve():
for _ in range(3):
conn, addr = server.accept()
conn.close()
threading.Thread(target=serve, daemon=True).start()
for n in range(3):
c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
c.connect((host, port))
print(f"client {n+1}: my port {c.getsockname()[1]} -> server port {c.getpeername()[1]}")
c.close()
# 3. A second server cannot grab the same port.
dup = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
dup.bind((host, port))
except OSError as e:
print("second bind failed:", e.strerror)
This is the output from one run on my machine. Your numbers will differ, because the operating system picks them:
server listening on port 54875
client 1: my port 47676 -> server port 54875
client 2: my port 47684 -> server port 54875
client 3: my port 47700 -> server port 54875
second bind failed: Address already in use
Three things are worth noticing. First, the server uses one port, 54875 in this run, for all three clients. Second, each client received a different temporary port of its own. Third, the attempt to bind a second server to the same address and port failed with “Address already in use.” Only one program can listen on a given port at a time, which explains an error that confuses beginners every single week.
The error you will meet one day. “Address already in use” or “port 3000 is already allocated” almost always means a previous copy of your program is still running, or another application owns that port. Find the owner with the commands later in this article, stop it, and try again.
Click a tab to see ports at work in different places. No reload, nothing to install.
You type a web address and press enter. You never type a port, yet one is used on every request.
What happens with ports. With no port in the address, the browser assumes 80 for http and 443 for https. A site on port 80 usually just redirects you to 443. Newer browsers may also try HTTP/3, which uses UDP port 443, and fall back to TCP if that fails. If you ever see a site at :8443 or :8080, the owner chose a non-default port.
A developer manages a rented server from home. They connect with a secure shell, and their password-guessing visitors do the same.
What happens with ports. SSH listens on 22 by default, so every scanner on the internet knocks there. Sensible practice is key-based login, no root login, and a firewall rule that allows 22 only from known addresses. To use another port, you write ssh -p 2222 user@host. It lowers log noise but is not a security measure.
Your phone’s mail app needs to send a message and fetch new ones.
What happens with ports. Mail uses several ports because it has several jobs. Your app submits outgoing mail on 587 (or 465 with immediate TLS). Mail servers pass messages between each other on 25. For reading, IMAP over TLS uses 993 and POP3 over TLS uses 995. Many hosting providers block outgoing port 25 from ordinary customers to cut spam, so a mail setup that works on a laptop may fail on a cloud server.
A web app stores its data in a database on the same server, or on a neighbouring one.
What happens with ports. Databases listen on their own registered ports, and the app connects to them. The golden rule is to bind them to a private address and block them in the firewall, so only the application server can reach them. Unprotected database ports on the public internet are found and attacked within hours, which is why every serious guide says to keep them closed.
You host a game server on your PC so friends can join from their homes.
What happens with ports. A common Java Minecraft server listens on 25565. Your friends connect to your public IP on that port, but your router drops the unsolicited traffic until you forward public port 25565 to the PC’s private address. Then allow the same port in the PC’s own firewall. Providers that put many homes behind shared addresses can block this entirely, in which case a hosted server or a relay service is the workaround.
Your home has one public IP address from your internet provider, yet a dozen devices share it. How do replies find the right device? The router performs NAT, network address translation, and ports are the trick that makes it work.
Suppose your phone (192.168.1.20) opens a connection to a web server from its local port 51000. The router rewrites the outgoing packet so it appears to come from the router’s own public address, using its own chosen port, say 40001. It remembers the pairing in a table. When the reply returns to port 40001, it looks it up and forwards the data to the phone at 192.168.1.20 port 51000. Your laptop doing the same thing would get a different public port.
| Device (private) | Private port | Router (public) port | Destination |
|---|---|---|---|
| 192.168.1.20 phone | 51000 | 40001 | example.com:443 |
| 192.168.1.31 laptop | 51000 | 40002 | example.com:443 |
| 192.168.1.44 TV | 49500 | 40003 | video.example.net:443 |
The numbers in the table are illustrative, since real routers pick their own. Notice that the phone and laptop both used private port 51000 and nothing broke. The router separates them by assigning different public ports.
NAT works naturally for connections that start inside your home. Connections that start outside have a problem: the router has no table entry, so it drops them. If you want a friend to join a game server running on your PC, you create a port forwarding rule: “anything arriving on public port 25565 goes to 192.168.1.50, port 25565.” That is exactly what the home-router tab above shows.
A firewall is largely a list of rules about ports. A typical server firewall says: allow incoming 443, allow incoming 22 only from my office, and drop everything else. Because a program can only receive connections if something is listening, closing a port you do not need means that if the software behind it has a flaw, the flaw is unreachable.
When you scan a machine for open ports, there are usually three results:
The practical rule I teach every junior: every open port is a promise to answer strangers. Keep that list as short as you can.
| Goal | Command | Notes |
|---|---|---|
| List listening ports (Linux) | ss -tulpn | Shows protocol, port and the program. Needs sudo to see other users’ programs. |
| List listening ports (Windows) | netstat -ano | The last column is the process ID. Match it in Task Manager. |
| Who owns port 3000? (macOS, Linux) | lsof -i :3000 | Prints the program and its process ID. |
| Test a remote port (Windows PowerShell) | Test-NetConnection host -Port 443 | Reports whether the TCP connection succeeded. |
| Test a remote port (Linux, macOS) | nc -zv host 443 | Needs netcat installed. |
Tool options vary slightly between systems and versions, so treat these as reliable starting points. Only scan machines you own or have permission to test. Scanning other people’s networks can break rules or laws.
Because each outgoing connection needs a temporary port, a single client talking to a single server address and port has a ceiling. With Windows’ default dynamic range of 49152 to 65535, that is 16,384 simultaneous connections. With Linux’s common default of 32768 to 60999, it is 28,232. After that, new connections fail until old ones close.
Most users never see this. Busy servers and load balancers do, and the same arithmetic hits NAT gateways that funnel thousands of devices through one public address. When people say a service “ran out of ephemeral ports,” this is what they mean. The usual fixes are reusing connections (keep-alive and connection pooling), widening the range, or adding more addresses.
Moving SSH from 22 to something obscure reduces noise in your logs because the lazy scanners that probe only port 22 stop finding you. It does not stop a determined scanner, which checks all ports in minutes. Real protection comes from key-based login instead of passwords, disabling root login, keeping software updated, and restricting who may connect at all. Treat a port change as tidiness, not security.
A closed port still reveals that the machine exists. Only a firewall that silently drops traffic hides that, and even then the machine can leak information in other ways.
Everybody knows the number. MySQL on 3306, PostgreSQL on 5432, Redis on 6379 and MongoDB on 27017 are scanned constantly. A database should normally listen only on a private interface, with the firewall blocking public access.
Tap a question to reveal the answer. The correct option is marked.
Sixteen bits give 65,536 values, 0 through 65535.
The https scheme defaults to 443. Plain http defaults to 80.
A connection is identified by both IP addresses and both ports. Each tab uses its own temporary source port.
Only one program can listen on a given address and port. Find the owner with lsof, ss or netstat.
A full port scan finds any port in minutes. Real protection is key-based login, updates and firewall rules.
A number that tells a computer which program should receive incoming data. The IP address finds the machine, and the port finds the program on it.
Port 80 is the default for plain HTTP, which is unencrypted. Port 443 is the default for HTTPS, which wraps the same web traffic in TLS encryption. Modern sites use 443 and redirect 80 to it.
No. It is just a number in the TCP or UDP header that the operating system uses to route data to the right program. It is not a hardware socket, even though a network cable plug is also sometimes called a port.
Yes, within limits. Avoid the well-known range below 1024 for personal projects, since on many systems those need extra privileges, and check that the port is not already used. Ports from about 1024 up are the usual choice for custom apps.
An open port is a program accepting connections from the network. It is only as risky as the program behind it and who can reach it. Keep open only what you need, patch the software, and restrict access with a firewall.
They are two separate sets of 65,536 numbers. TCP 53 and UDP 53 are different doors. TCP is reliable and ordered, UDP is faster with no guarantees, and some services, like DNS, use both.
The post What Is a Port Number? Why 80, 443 and 22 Matter appeared first on Learn With Examples.
]]>The post What Is DHCP? How Your Device Gets an IP Address Automatically appeared first on Learn With Examples.
]]>You walk into a café, tap the Wi-Fi name, and within a second the web works. Nobody typed an address, picked a gateway or told your phone where to find a DNS server. Behind that second sits a short, polite conversation between your phone and a small piece of software called a DHCP server. This article walks through that conversation, step by step, with real examples and a simulation I ran to show what happens when it goes wrong.
Every device on a network needs an IP address, which is the number other devices use to send it data. If you set one by hand, you have to pick a number nobody else is using, enter the right subnet mask, the right gateway, and at least one DNS server. Get one digit wrong and the device either cannot talk to anyone or, worse, collides with another device and knocks it offline.
I have done this by hand on more machines than I care to count, back when a small office meant a spreadsheet of addresses taped inside the server cabinet. It works for ten machines. It collapses at a hundred, and it is hopeless for phones that join and leave a network several times a day.
The fix is DHCP, the Dynamic Host Configuration Protocol. A DHCP server owns a range of addresses and a set of settings. Any device that joins the network asks for them, receives a loan of one address, and uses it until the loan runs out. The key word is loan, and it explains almost everything else in this article.
The one-paragraph version. DHCP automatically gives a device an IP address and the other settings it needs (subnet mask, default gateway, DNS servers) when it joins a network. The exchange has four messages, nicknamed DORA: Discover, Offer, Request, Acknowledge. The address is leased for a limited time, and the device renews it before it expires. At home, your router is the DHCP server.
Picture a hotel with fifty rooms. You arrive with a suitcase and no room number. You do not walk the corridors trying doors. You go to the front desk, and the clerk checks which rooms are free, assigns you one, and hands over a key card that works until checkout. If you stay longer, you stop by the desk and extend it. If you leave without telling anyone, the key card simply stops working at checkout time and the room goes back into the pool.
Map it across: the hotel is the network, the rooms are IP addresses, the guest is your device, and the clerk is the DHCP server. The checkout time is the lease. That last detail matters more than it looks, and we will see why when the café runs out of rooms.
An IP address is only the headline. A device usually receives a small bundle of settings, delivered as numbered “options” inside the DHCP messages.
| Setting | Example | What it is for |
|---|---|---|
| IP address | 192.168.1.134 | Your device’s identity on this network. |
| Subnet mask | 255.255.255.0 | Tells the device which addresses are on the same local network. |
| Default gateway | 192.168.1.1 | The router that carries traffic out to the internet. |
| DNS servers | 192.168.1.1 | Where to look up names like example.com. At home, this is often the router itself, which forwards on. |
| Lease time | 24 hours | How long the device may keep this address before renewing. |
The ordinary home values above are only an example. Many routers use 192.168.0.x or 10.0.0.x instead, and lease times vary by manufacturer and by network. There are many more options (a domain name, time servers, boot file locations for network booting), but these five are the ones you will see in daily life.
Here is the puzzle that makes DHCP clever. Your phone has just joined the Wi-Fi. It has no IP address, no idea what the network looks like, and no idea where the DHCP server is. How do you send a message to someone you cannot address?
The answer is broadcast. The phone sends its first message from the address 0.0.0.0 (“I am nobody yet”) to 255.255.255.255 (“everybody on this local network”). It does not know who the server is, so it shouts to the whole room. Every device on the local network hears it, but only a DHCP server answers. DHCP runs over UDP, with clients using port 68 and servers using port 67.
Network engineers use the nickname DORA for the four steps. Here is the whole exchange.
The client broadcasts: “Is there a DHCP server out there? I need an address.” The message carries the client’s hardware (MAC) address so the server knows who is asking.
A DHCP server picks a free address from its pool and replies: “I can lend you 192.168.1.134, with this mask, this gateway, these DNS servers, for 24 hours.” Notice it is an offer, not a grant. The address is held aside for the client, but the deal is not done.
The client says: “Yes, I would like that address from that server.” This message is still broadcast, and that is deliberate. If two DHCP servers both made offers, the broadcast tells every server which offer the client accepted, so the others can release the address they had reserved.
The server confirms: “It is yours.” Only now does the client configure its network interface with the address. The conversation takes a fraction of a second on a healthy network.
If you remember one thing: Discover and Request come from the client, Offer and Acknowledge come from the server. A common exam and interview trap is mixing up Offer (a proposal) with Acknowledge (the final confirmation).
When the server says “24 hours,” the clock starts. The client does not wait until the last second. It follows two timers defined in the DHCP standard (RFC 2131):
On a normal network, renewal succeeds at the 12-hour mark and the lease clock resets. Your laptop can keep the same address for months without you noticing. The lease exists for the opposite case: devices that vanish. A phone that walks out of range never says goodbye, so the server needs a way to reclaim its address. That is what expiry does.
Reading about leases is one thing. Seeing one is better. Here is a small address pool in Python that hands out addresses, renews them and reclaims expired ones. It is not a real DHCP server (there are no network packets), but it implements the same bookkeeping that real servers do.
import ipaddress
from collections import deque
class DhcpPool:
"""A tiny DHCP address pool: hand out, renew and reclaim leases."""
def __init__(self, first, last, lease_seconds):
a = int(ipaddress.IPv4Address(first))
b = int(ipaddress.IPv4Address(last))
self.free = deque(str(ipaddress.IPv4Address(i)) for i in range(a, b + 1))
self.lease = lease_seconds
self.leases = {} # mac -> (ip, expires_at)
def reclaim(self, now):
for mac, (ip, exp) in list(self.leases.items()):
if exp <= now:
del self.leases[mac]
self.free.append(ip) # reused last, like many servers
def request(self, mac, now):
self.reclaim(now)
if mac in self.leases: # renewal: same address, fresh clock
ip = self.leases[mac][0]
elif self.free:
ip = self.free.popleft()
else:
return None # pool exhausted
self.leases[mac] = (ip, now + self.lease)
return ip
def timers(lease_seconds):
"""RFC 2131 defaults: renew at 50 percent, rebind at 87.5 percent."""
return lease_seconds * 0.5, lease_seconds * 0.875
# demo
pool = DhcpPool("192.168.1.100", "192.168.1.102", lease_seconds=3600)
print("phone ->", pool.request("aa:aa", 0))
print("laptop ->", pool.request("bb:bb", 10))
print("tv ->", pool.request("cc:cc", 20))
print("tablet ->", pool.request("dd:dd", 30), "(pool is empty)")
print("phone renews at 1800s ->", pool.request("aa:aa", 1800))
print("tablet at 3700s ->", pool.request("dd:dd", 3700), "(laptop and tv expired)")
t1, t2 = timers(86400)
print("24h lease: renew at %.0fh, rebind at %.1fh" % (t1/3600, t2/3600))
Running it with a pool of only three addresses and a one-hour lease produces this:
phone -> 192.168.1.100
laptop -> 192.168.1.101
tv -> 192.168.1.102
tablet -> None (pool is empty)
phone renews at 1800s -> 192.168.1.100
tablet at 3700s -> 192.168.1.101 (laptop and tv expired)
24h lease: renew at 12h, rebind at 21.0h
Read it line by line. The first three devices get consecutive addresses. The tablet asks while the pool is empty and gets nothing, which is exactly what a real device experiences when a network runs out of addresses. The phone renews at the half-hour mark and keeps the same address. By 3,700 seconds, the laptop and TV leases have expired without anyone releasing them, so the tablet finally gets the laptop’s old address.
Lease length sounds like a boring setting until you see it break something. So I simulated a café with Wi-Fi. About one new customer device joins every minute, stays for roughly half an hour on average, and leaves without telling the network. The DHCP pool has 100 addresses. I ran ten hours of café time, repeated twenty times with different random seeds, and varied only the lease length. Devices renew at the 50 percent mark while they are present, just like real ones.
| Lease time | Peak addresses in use | Devices that could not get an address |
|---|---|---|
| 10 min | 54 of 100 | 0.0% |
| 30 min | 71 of 100 | 0.0% |
| 1 hour | 95 of 100 | 0.2% |
| 4 hours | 100 of 100 | 51.3% |
| 24 hours | 100 of 100 | 83.3% |
The shape of the result is the lesson. With a 10 or 30-minute lease, nobody was turned away. At one hour, a handful of devices failed during the busiest moments. At four hours, about half of all arriving devices found no free address, and at 24 hours more than four out of five did. The crowd was identical in every run. The only difference was how long departed devices kept their addresses reserved.
The figures come from a simplified model: random arrivals, a random stay time and no early release. Real cafés have more variety, and the exact percentages will differ. The pattern will not. A short lease is the right tool for a transient crowd, and a long lease is the right tool for a stable one. Home and office networks, where the same devices come back every day, can use long leases comfortably. Hotels, airports, conferences and cafés should use short ones.
Click a tab to see DHCP in different places. Nothing to install and no page reload.
You buy a Wi-Fi router, plug it in and connect your phone, TV, laptop and a smart speaker. You never touch an address setting.
How DHCP fits. The router runs a DHCP server by default, usually with a pool such as 192.168.1.100 to 192.168.1.199 (the exact range differs by brand). Long leases suit a home, because the same dozen devices come back every day. Look in the router’s admin page for a “connected devices” or “DHCP clients” list and you will see the leases.
Hundreds of strangers pass through, most staying under an hour. Phones leave without any goodbye.
How DHCP fits. This is the café from our simulation. Public networks use short leases and wide pools so addresses of departed devices return quickly. Many also use much larger private ranges than a home network, because a /24 holds only 254 addresses.
A company separates staff, guests, phones and printers into different networks for security and tidiness.
How DHCP fits. Each network gets its own pool, called a scope, on one central DHCP server. Routers forward the broadcasts through a relay agent. Managers also add reservations for fixed gear and exclusion ranges so the pool never touches addresses set by hand.
Your home router does not come with an internet-facing address built in. It has to get one from your internet provider.
How DHCP fits. On many cable and fibre connections, your router is itself a DHCP client on the provider’s side. It broadcasts a request, and the provider’s server leases it a public address. That is why a home address can change after a long outage. Other providers use different methods, such as PPPoE, so this varies by ISP.
Your family prints from five devices. The printer is found by address, and every few weeks one laptop loses it.
How DHCP fits. A reservation tells the DHCP server: whenever this MAC address asks, always give it this address. The printer still uses DHCP, so nothing is typed on the device itself, but its address never moves. It is the tidiest way to get a stable address without hand-configuring anything.
DHCP is not all-or-nothing. Most real networks mix three approaches.
| Method | How the address is set | Best for | Watch out for |
|---|---|---|---|
| Dynamic DHCP | Server picks any free address from the pool. | Phones, laptops, guests, most devices. | The address can change after a long absence. |
| DHCP reservation | Server always gives the same address to a specific MAC address. | Printers, NAS boxes, cameras, servers. | Changing the network card changes the MAC, which breaks the match. |
| Static (manual) | You type the address on the device itself. | Routers, core infrastructure, bootstrapping. | You must avoid the DHCP pool range or you risk conflicts. |
My own rule after fifteen-odd years: configure a reservation on the DHCP server rather than a static address on the device whenever you can. All the addressing then lives in one place, and you can see and change it from the server. Static addresses scattered across devices are how networks accumulate mysteries.
If a device sends Discover and nobody answers, it does not give up silently. Modern operating systems fall back to a link-local address in the range 169.254.0.0 to 169.254.255.255 (defined in RFC 3927, and called APIPA on Windows). The device picks one at random in that range and checks that nobody else is using it.
That fallback only lets the device talk to other link-local devices on the same local network. There is no gateway, so there is no internet. If you ever open your network settings and see an address starting with 169.254, read it as a message: “I asked for an address, and nobody gave me one.”
Quick diagnosis. An address starting with 169.254 means DHCP did not respond. Check that the router is on, that you are on the right network, and that the DHCP service is enabled. Do not waste time hunting for DNS or website problems yet. You do not even have a valid address.
Seeing the lease on your own machine makes this concrete. The commands vary a little by operating system.
| Goal | Windows | Linux or macOS |
|---|---|---|
| See your current address and DHCP server | ipconfig /all | ip addr (Linux), ipconfig getpacket en0 (macOS) |
| Give up the current lease | ipconfig /release | Toggle the network interface off, or use your network manager’s reconnect option. |
| Ask for a new lease | ipconfig /renew | Toggle the interface back on, or reconnect. |
On Windows, ipconfig /all prints a “DHCP Server” line plus “Lease Obtained” and “Lease Expires” times. That is the lease we have been discussing, sitting right there on your screen. The macOS interface name (en0 here) can differ between machines, and Linux tools differ between distributions, so treat these as starting points.
There is an obvious catch with broadcasts. Routers do not forward them, so a DHCP broadcast stays inside its own local network. In a company with separate networks for staff, guests and printers, would you need a DHCP server in every one?
No. The trick is a DHCP relay agent, usually a setting on the router (often called “IP helper”). It listens for broadcasts on one network, wraps them in a unicast message and forwards them to a central DHCP server. The relay stamps in the address of the network the request came from, so the server knows which pool to use. One central server can then serve dozens of separate networks, each with its own address range.
Everything above is DHCP for IPv4. IPv6 networks have another automatic option called SLAAC (stateless address autoconfiguration), where a device builds its own address from information advertised by the router. DHCPv6 also exists, and networks use it when they want central control over addresses or need to pass out extra settings. The two are not rivals so much as tools, and many networks use them together. The details deserve their own article.
DHCP was designed for a friendlier era. A client believes the first offer it likes, with no authentication. That creates two classic problems.
Someone plugs in a router, or a misconfigured device starts running a DHCP service. Clients that hear its offer first accept it, and now they receive a wrong gateway or a malicious DNS server. At best, people lose internet access and nobody can explain why. At worst, traffic is redirected through an attacker. The defence on managed switches is DHCP snooping, which only allows DHCP server messages from ports marked as trusted.
An attacker, or a buggy device, sends a flood of requests with fake MAC addresses until the pool is empty and real users get nothing. Switch features that limit the number of MAC addresses per port, and short leases on guest networks, reduce the damage.
Tap a question to reveal the answer. The correct option is marked.
Remember DORA: Discover and Request come from the client, Offer and Acknowledge from the server.
With no address and no knowledge of the server, the client shouts to everyone on the local network and lets the server answer.
T1 is 50 percent of the lease, so renewal begins at 4 hours. T2, the broadcast rebind, comes at 87.5 percent, or 7 hours.
169.254.x.x is the link-local fallback used when DHCP does not respond. It allows only local-network chatter.
A reservation keeps the printer on DHCP but always gives it the same address, with no conflict risk and no manual typing on the device.
Dynamic Host Configuration Protocol. It automatically configures devices with an IP address and related network settings.
No. DHCP gives a device its address and tells it which DNS server to use. DNS is the separate service that turns names like example.com into addresses.
Often yes in practice, because a returning device usually gets its old address back if it is still free. But a lease is not a guarantee. For a fixed address, use a DHCP reservation.
In almost every case, it is your router. Some networks use a separate server or a dedicated device, and a home setup can have only one active DHCP server safely.
Clients take whichever offer reaches them first, so devices get inconsistent settings, such as different gateways, and failures look random. Disable DHCP on one of the devices.
Use DHCP for most devices. For devices that need a fixed address, prefer a DHCP reservation. Use true static addressing mainly for the router itself and a few core pieces of infrastructure, keeping those addresses outside the DHCP pool.
The post What Is DHCP? How Your Device Gets an IP Address Automatically appeared first on Learn With Examples.
]]>The post Heap vs Sorted Array for Priority Handling appeared first on Learn With Examples.
]]>An emergency room does not treat patients in the order they walk in. A nurse looks at each person and the sickest one goes first. Software has the same problem everywhere: tasks, packets, alerts and routes all arrive with different levels of urgency, and something has to hand you the most urgent one, fast. There are two classic ways to build that. One keeps everything sorted. The other keeps everything “sorted enough.” This article shows you, with real numbers, when each one wins.
I have been writing and reviewing code for a little over fifteen years, and the same question comes back every couple of years in a new costume. A job scheduler needs the next job to run. A game server needs the next event to fire. A routing service needs the next closest intersection. Different industries, same shape: items arrive in any order, each carries a priority, and at any moment somebody asks for the single most important item.
The abstract name for this is a priority queue. It is a queue where the order of leaving is decided by priority, not by arrival. You need exactly three operations, and almost nothing else.
The one-paragraph version. A sorted array keeps every item in order, so the most urgent one is always at the end and you can grab it instantly, but every insertion has to shove other items aside, which costs O(n). A binary heap keeps only a loose order: the most urgent item is at the top, and each insertion or removal fixes things with about log n small swaps. For small or read-heavy data the sorted array is perfectly fine. For large collections with constant inserting and removing, the heap wins by a wide margin.
That is the whole contract. How you store the items behind the scenes is your business, and the storage choice decides how fast each of those three runs. In this article, “most urgent” is just whichever end of the ordering you care about. Python’s standard library treats the smallest number as most urgent, so I will use that convention for the heap. For the sorted array I will keep the list in ascending order and call the highest number most urgent, which keeps removal cheap. The two structures can be flipped either way, and the cost is the same.
Before we compare structures, it helps to see how often this pattern appears. Here are the ones I run into most.
We will return to each of these in the interactive panel later. For now, hold on to the hospital, because it makes the data-structure question concrete.
Start with the two ideas everybody tries first.
A normal queue serves first come, first served. That is fair at a bakery and a disaster at a hospital. The chest-pain patient who arrived last would wait behind everyone with a sprained ankle. A plain queue ignores priority entirely, so it cannot be the answer.
Throw items into a list in any order. Insertion is instant, you just append. But to find the most urgent item you have to scan the whole list, which costs O(n). With ten items that is nothing. With a million items and a million requests, you are doing about a trillion comparisons. The unsorted list moves all the cost to the extraction side.
So the real design question is where to spend your effort: at insertion time, at extraction time, or a little at each. That is exactly the difference between our two contenders.
The idea is simple enough to explain to anyone. Keep the array sorted at all times. If the most urgent item is at one end, extraction is trivial. If you keep the array ascending and treat the largest as most urgent, the best item sits at the last position, and removing the last element of an array costs O(1). Peeking costs O(1) too.
Insertion is where you pay. To add a new item, you first find where it belongs. Because the array is sorted, you can use binary search and find the spot in O(log n) comparisons. That part is cheap. The expensive part is that everything after that spot has to slide one position to the right to make room. On average that is half the array.
Here is the whole thing in Python. It uses the bisect module, which does the binary search and the insertion for you.
from bisect import insort
class SortedPQ:
"""Highest number = most urgent. Kept in ascending order,
so the most urgent item is always at the end of the list."""
def __init__(self):
self.a = []
def push(self, x):
insort(self.a, x) # binary search + shift: O(n)
def pop(self):
return self.a.pop() # remove from the end: O(1)
def peek(self):
return self.a[-1]
That is nearly all of it. The simplicity is a real virtue, and I do not want to understate it. A sorted array is cache-friendly, easy to debug, easy to print, and the memory move that does the shifting is one of the most heavily optimised operations a computer knows. We will come back to that, because it changes the verdict for small data.
A heap makes a different bargain. It refuses to keep everything sorted. It only promises one thing: every parent is at least as urgent as its children. That single rule guarantees the most urgent item is at the top, and it is much cheaper to maintain than a full ordering.
The clever part is how it is stored. A heap is conceptually a tree, but it lives in a plain array with no pointers at all. The tree is kept complete, filled level by level from left to right, so the position of every relative is just arithmetic:
i is at (i - 1) // 2.i are at 2i + 1 and 2i + 2.Here is what happens if you insert 5, 3, 8, 1, 9 and 2 into a min-heap, one at a time. The array and the tree below are the same data seen two ways.
Notice that the array [1, 3, 2, 5, 9, 8] is not sorted. The 3 sits before the 2. A heap does not care. It only cares that the 1 at the top is the smallest and that each parent beats its children.
To add an item, append it at the end of the array, which is the next free slot in the tree. Then compare it with its parent. If it is more urgent, swap them. Repeat until it is in a good place or reaches the root. The tree has about log₂(n) levels, so you do at most that many swaps. A million items means roughly twenty levels, not a million moves.
The top item is the one you want. Take it out, which leaves a hole at the root. Fill the hole by moving the last array element to the root, then let it sink: compare it with its two children, swap with the more urgent child, and repeat. Again at most about log₂(n) swaps.
The full implementation is short enough to read in one sitting. I wrote it by hand so you can see the mechanics, though in real projects you would reach for a library.
class MinHeap:
def __init__(self):
self.a = [] # the heap lives in a plain list
def push(self, x):
a = self.a
a.append(x) # 1. put it at the end
i = len(a) - 1
while i > 0: # 2. sift up while smaller than parent
parent = (i - 1) // 2
if a[i] >= a[parent]:
break
a[i], a[parent] = a[parent], a[i]
i = parent
def pop(self):
a = self.a
top = a[0]
last = a.pop() # 1. remove the last item
if a:
a[0] = last # 2. move it to the root
i, n = 0, len(a)
while True: # 3. sift down
left, right, small = 2 * i + 1, 2 * i + 2, i
if left < n and a[left] < a[small]:
small = left
if right < n and a[right] < a[small]:
small = right
if small == i:
break
a[i], a[small] = a[small], a[i]
i = small
return top
def peek(self):
return self.a[0]
Python ships the same idea in the heapq module, which operates on an ordinary list. Here it is running a small emergency-room queue, where a lower number means a sicker patient.
import heapq
from itertools import count
queue, tie = [], count() # tie-breaker keeps arrival order
def add(priority, name):
heapq.heappush(queue, (priority, next(tie), name))
add(3, "Ana")
add(5, "Ben")
add(2, "Chi")
add(2, "Dev")
add(1, "Eli")
while queue:
priority, _, name = heapq.heappop(queue)
print(priority, name)
The output is, in order: Eli (1), Chi (2), Dev (2), Ana (3), Ben (5). Chi and Dev share priority 2 and come out in the order they arrived, thanks to the little counter in the tuple. That detail matters in production, and I will say more about it in the pitfalls section.
Here is how the common ways of building a priority queue compare. N is the number of items stored.
| Structure | Insert | Peek | Extract | Notes |
|---|---|---|---|---|
| Unsorted array | O(1) | O(n) | O(n) | Cheap to fill, slow to query. |
| Sorted array | O(n) | O(1) | O(1) | Binary search finds the slot in O(log n), shifting costs O(n). |
| Sorted linked list | O(n) | O(1) | O(1) | No shifting, but finding the slot is a linear walk. |
| Binary heap | O(log n) | O(1) | O(log n) | Building a heap from n items at once takes only O(n). |
Read the table by asking which column your program hammers. If you insert once and then extract a million times, the sorted array looks great. If you interleave inserts and extracts all day, the heap’s O(log n) on both sides is the safer bet.
Big-O is a promise about growth, not a stopwatch. So I measured. In the experiment below, I generated n random priorities, inserted all of them, and then extracted everything. Instead of timing, I first counted the elementary moves, because counts are the same on every machine.
| n items | Sorted array: elements shifted | Heap: swaps while inserting | Heap: swaps while extracting |
|---|---|---|---|
| 1,000 | 254,969 | 1,183 | 7,343 |
| 10,000 | 24,856,194 | 12,840 | 106,757 |
| 100,000 | 2,509,367,779 | 127,339 | 1,400,573 |
At a hundred thousand items, the sorted array shuffled about 2.5 billion elements while the heap made around 1.5 million swaps. That is a gap of roughly 1,600 times. Notice how the sorted array’s cost grows about a hundredfold every time n grows tenfold. That is the quadratic growth you would expect from n inserts that each cost O(n). The heap’s cost grows only a little faster than n itself.
Counts are tidy, but you pay in seconds. I timed the same insert-everything-then-extract-everything test using the sorted array above against Python’s heapq. Timings depend on the machine, so treat the shape as the lesson and not the exact digits.
| n items | Sorted array | heapq | Verdict |
|---|---|---|---|
| 1,000 | 0.31 ms | 0.20 ms | Heap 1.6x faster |
| 10,000 | 11.92 ms | 2.75 ms | Heap 4.3x faster |
| 100,000 | 1.031 s | 40.42 ms | Heap 25.5x faster |
The gap opens exactly as theory says. At 100,000 items the sorted array needed about a second and the heap about 0.04 seconds. But look at the small end. At a thousand items both finish in well under a millisecond, and that points to something that surprises many people.
The reason is that shifting elements in a Python list is not a Python loop. It is a single memory-move operation written in C, and modern processors copy contiguous memory astonishingly fast. A heap, on the other hand, does a lot of small comparisons and swaps with a fair amount of overhead per step. When n is small, a very fast O(n) move can beat a slower O(log n) procedure.
To test this properly I ran a second experiment that looks more like a real service: a queue that stays at a steady size while 50,000 insert-then-extract pairs flow through it.
| Queue size | Sorted array | heapq | Verdict |
|---|---|---|---|
| 100 | 7.79 ms | 8.95 ms | About equal |
| 1,000 | 9.76 ms | 11.98 ms | Sorted array 1.2x faster |
| 10,000 | 52.10 ms | 24.04 ms | Heap 2.2x faster |
| 100,000 | 0.787 s | 27.92 ms | Heap 28.2x faster |
This is the part I most want you to take away. For queues holding a hundred or a thousand items, the sorted array was actually a little faster in my run. By ten thousand items the heap was already about twice as fast, and at a hundred thousand it was nearly thirty times faster. The crossover point depends on your language and hardware, but it is real, and it is usually somewhere in the low thousands.
The veteran’s lesson. Complexity tells you what happens as data grows. It does not tell you where the crossover is. When your queue is small and will stay small, the simplest structure that works is often the right call. When it can grow without a ceiling, choose the one that scales.
Click a tab below to see how priority handling looks in different places. No page reload, nothing to install.
Patients arrive all night. A nurse assigns each a severity score, and when a bed opens, the sickest person goes in next.
Why a heap fits. Interleaved arrivals and calls for the next patient are exactly a heap workload. The tie-breaker matters: two patients with equal severity should be seen in arrival order, which is why the counter sits inside the tuple.
A background system receives jobs: send email, resize image, rebuild index. Each has a priority or a due time, and a worker asks for the next job to run.
Why a heap fits. Queues like this can grow long during a traffic spike, and a heap keeps each insert and extract at O(log n) even then. Many job and timer queues are built on heaps for that reason, though the exact structure varies by system.
A map app finds the fastest route. It keeps a frontier of places it could visit next and always expands the closest one first.
Why a heap fits. Dijkstra’s algorithm calls extract-min once per node and pushes whenever it finds a shorter path. With a binary heap the total is about O((V + E) log V). Since a heap has no cheap decrease-key, implementations push a duplicate entry and skip the stale one later, the lazy deletion trick from the pitfalls section.
You scan a stream of millions of game scores and want only the top ten.
Why a heap fits. Keep a min-heap of size 10. For each new score, if it beats the smallest of the ten, replace it. The heap never grows past K, so each step costs only O(log K). A sorted array of size 10 would also be fine here, because K is tiny, which is the small-n rule in action.
Twenty servers each write a log already sorted by time. You want one combined, time-ordered log.
Why a heap fits. Keep a heap holding the current first line of each file. Pop the earliest, write it, and push that file’s next line. The heap never exceeds k items, and Python’s heapq.merge does exactly this lazily, so you can merge files far bigger than memory.
| Your situation | Better pick | Why |
|---|---|---|
| Small queue, a few hundred items | Sorted array | Simple, and the fast memory move beats heap overhead at this size. |
| Fill once, then mostly read | Sorted array | You pay for sorting once and then read in O(1) forever. |
| You need a full sorted view, ranks or range queries | Sorted array | A heap cannot answer “what is the 5th item?” or list everything in order without extracting it all. |
| You need both the best and the worst item | Sorted array | Both ends are directly reachable. A single heap only exposes one end. |
| Constant mixed inserts and extracts, large n | Heap | O(log n) on both operations keeps latency predictable. |
| Queue can grow to hundreds of thousands | Heap | The sorted array’s shifting cost becomes the bottleneck. |
| Dijkstra, schedulers, event loops | Heap | The textbook workload for a heap: many inserts, many extract-min calls. |
| Top-K from a huge stream | Heap | A heap of size K discards the rest cheaply. |
There is a trick worth knowing. If you already have all n items, you do not need to insert them one by one, which would cost O(n log n). Python’s heapq.heapify(list) rearranges the list in place into a valid heap in O(n) time. It works by sifting down every non-leaf node, starting from the bottom, and the maths happens to add up to linear time because most nodes sit near the bottom where there is almost nothing to sift.
The practical rule: when you have the data up front, heapify. When it arrives over time, push one at a time. And if you are going to sort everything anyway, a plain sort is simpler still.
If you want the largest item first, negate the priorities when you push and negate them again when you pop. Forgetting this is a classic source of “why is my queue upside down?” bugs. Java’s PriorityQueue is also a min-heap by default, while C++’s std::priority_queue is a max-heap by default. The defaults differ across languages, so check before you trust.
A heap is not stable. Two items with the same priority can come out in either order. If fairness matters, and in a job queue it nearly always does, add a tie-breaker such as an incrementing counter, as we did in the tuple earlier. Also be aware that if your items are not comparable (two dictionaries, say), Python raises an error when priorities tie. The counter in the middle of the tuple prevents that comparison from ever reaching the payload.
Say a task becomes more urgent after it is queued. Finding an arbitrary item inside a heap takes O(n), because the heap only organises the top. The common workaround, used in Dijkstra implementations everywhere, is lazy deletion: push a new entry with the better priority, and when you pop a stale entry later, notice it is outdated and skip it.
Printing the underlying list gives you something that looks almost sorted, which tempts people to iterate over it directly. Do not. The only guaranteed position is index 0. To get everything in order you must pop repeatedly, or just sort a copy.
Neither structure is safe for concurrent writers. Wrap access in a lock, or use a purpose-built concurrent queue such as Python’s queue.PriorityQueue, which is a heap plus locking.
You will rarely write a heap by hand outside of an interview or a learning exercise. Python gives you heapq for lists and queue.PriorityQueue for threads. Java gives you PriorityQueue. C++ gives you std::priority_queue. JavaScript has no built-in priority queue, so people either write a small heap or pull in a package. All of these are heaps underneath. For a sorted array in Python, bisect is the standard tool, and the third-party sortedcontainers package offers a sorted list with much better insertion behaviour for large sizes.
bisect or use a heap instead.Tap a question to reveal the answer. The correct option is marked.
Children of index i are at 2i + 1 and 2i + 2. For i = 2 that gives 5 and 6.
Binary search only finds the slot. Opening a gap there requires moving, on average, half the array.
On a tie Python compares the next tuple element. If that is not comparable, it raises an error. A counter in the middle prevents it and keeps FIFO order.
A sorted array gives instant sorted iteration, ranks and both ends. A heap only guarantees its top item.
Most nodes are near the bottom and barely move, so the total work adds up to linear time.
A queue where items leave in order of priority instead of arrival time. It supports insert, peek and extract, and can be built on a heap, a sorted array or other structures.
No. A heap only guarantees that each parent is at least as urgent as its children, so the top item is correct but the rest is only partly ordered. A sorted array is fully ordered.
It is a design choice for simplicity. For a max-heap, negate numeric priorities when pushing and negate again after popping, or wrap items in a class with reversed comparison.
Yes. Heapsort builds a heap and then extracts the top item repeatedly, which gives an O(n log n) sort that works in place.
When the list can grow large and you insert often. Its SortedList avoids the full-array shifting of a plain list and stays fast at sizes where bisect.insort becomes slow.
No. Searching a heap is O(n) because only the top is organised. If you need lookups by key as well as priority, pair the heap with a dictionary or use an indexed priority queue.
The post Heap vs Sorted Array for Priority Handling appeared first on Learn With Examples.
]]>The post LRU Cache: Build One With a Hash Map and a Linked List appeared first on Learn With Examples.
]]>Your phone keeps the apps you used recently and quietly drops the ones you have ignored. Your browser, your database and your CPU do the same thing with data. The rule behind it is called Least Recently Used, and building it yourself is one of the most satisfying exercises in programming: two simple data structures, each weak on its own, snapped together into something that runs in constant time.
If you have ever sat through a software interview, you have probably met this question: “Design a data structure that follows a Least Recently Used cache.” It is a favourite of interviewers for good reason. It is short to state, it cannot be solved with a single built-in structure, and the solution reveals whether you understand how data structures complement each other.
But it is far more than an interview trick. I have used LRU caches to speed up image thumbnails, to keep recent database lookups in memory, and to throttle expensive API calls. The idea is so useful that it appears at almost every level of computing, from the cache inside your processor to the apps in your phone’s switcher.
In this article we will build an LRU cache from scratch. I will explain the idea with everyday examples, show why each simple approach fails, then assemble the real solution step by step with working code you can paste and run. There are diagrams, a traced example, a simulation comparing LRU with other policies, and a quiz.
The one-sentence version. An LRU cache stores a limited number of items, and when it is full it throws away the item that has gone unused for the longest time. It is built from a hash map (for instant lookup) and a doubly linked list (to track order of use).
A cache is a small, fast store that holds copies of things that are slow to get. If you read the same record from a database a thousand times, it makes sense to keep the answer in memory rather than asking the database again.
The catch is that fast storage is small. Memory is limited, so the cache must have a maximum size, called its capacity. Once it is full and a new item arrives, something has to go. Deciding what to remove is the job of the eviction policy.
Think of a small desk. You can keep ten books on it. Whenever you need an eleventh book, you must send one back to the shelf. Which one? You would probably return the book you have not touched for the longest time, because you are least likely to need it soon. That intuition is the entire LRU policy.
LRU is not the only choice. Here are the common ones in plain words.
| Policy | Evicts | Works well when | Weak spot |
|---|---|---|---|
| LRU | The item unused for the longest time | Recent use predicts future use | One-time scans and big loops |
| LFU | The item used the fewest times | Some items are long-term favourites | Slow to adapt when tastes change |
| FIFO | The oldest item inserted | Simplicity matters most | Ignores how useful an item is |
| Random | Any item at all | Cheap approximation is fine | No intelligence at all |
LRU strikes a very good balance. It is easy to explain, it adapts quickly to changing behaviour, and as we will see in a simulation, it beats FIFO and random eviction on realistic access patterns.
We need a class with two operations:
get(key) returns the stored value, or -1 if the key is not there. If it is there, it counts as a use.put(key, value) stores or updates a value. If the cache is already at capacity and the key is new, it first evicts the least recently used item.And here is the demanding part: both operations must run in O(1) time, meaning constant time no matter how many items are cached. That requirement is what makes the problem interesting.
Let us test the obvious ideas first, because the failures teach you the design.
A hash map (a dictionary in Python, an object or Map in JavaScript) finds a value by key in constant time. Perfect for get. But a plain hash map has no useful notion of “least recently used.” When the cache is full, you would have to scan every entry to find the oldest. That makes eviction O(n).
Keep items in an array, most recent at the front. Now the order is explicit and the least recent is always at the end. But finding a key means searching the array, which is O(n). And moving an item to the front means shifting the others. Both are slow.
The hash map is excellent at finding things and terrible at order. The list is excellent at order and terrible at finding things. So use both: the hash map finds the item instantly, and the list tracks order. To connect them, the hash map does not store values directly. It stores a pointer to the item’s node in the list.
A linked list is a chain of nodes where each node points to the next. In a singly linked list you can only move forward. A doubly linked list also keeps a pointer to the previous node.
That second pointer is the whole trick. When the hash map hands us a node in the middle of the list, we want to cut it out and move it to the front. Cutting out requires updating the previous node’s next pointer and the following node’s prev pointer. With a doubly linked list, the node already knows both neighbours, so removal takes a few assignments, with no searching. That is O(1).
We also add two dummy nodes, a head and a tail, that never hold data. They sit at the two ends so we never have to write special code for an empty list or for the first and last real node. This small habit eliminates a whole class of bugs.
Notice that the hash map keys (A, C, D) are in no particular order, and the lines cross. That is fine. The map only answers “where is this item?” The list answers “which item is oldest?”
Every operation reduces to a handful of pointer moves.
Look up the node through the map, unlink it from where it is, and insert it right after HEAD. Return its value.
If the key is not in the map, return -1. The order does not change.
Update the stored value, then move the node to the front, because writing counts as a use.
If the cache is full, remove the node just before TAIL, and delete its key from the map. Then create a new node, add it to the map, and insert it after HEAD.
Here is the whole thing. It is about fifty lines, and I have tested it against a second implementation on thousands of random operations.
class Node:
__slots__ = ("key", "value", "prev", "next")
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.map = {} # key -> Node
self.head = Node() # dummy node on the "most recent" side
self.tail = Node() # dummy node on the "least recent" side
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node): # unlink a node from the list
node.prev.next = node.next
node.next.prev = node.prev
def _add_front(self, node): # insert right after head
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def get(self, key):
node = self.map.get(key)
if node is None:
return -1
self._remove(node) # it was just used,
self._add_front(node) # so move it to the front
return node.value
def put(self, key, value):
if self.capacity <= 0:
return
node = self.map.get(key)
if node is not None: # key exists: update and refresh
node.value = value
self._remove(node)
self._add_front(node)
return
if len(self.map) >= self.capacity:
lru = self.tail.prev # least recently used node
self._remove(lru)
del self.map[lru.key] # this is why nodes store their key
node = Node(key, value)
self.map[key] = node
self._add_front(node)
def order(self): # helper for demos: most recent first
out, cur = [], self.head.next
while cur is not self.tail:
out.append(cur.key)
cur = cur.next
return out
Let us walk through the key details.
_remove and _add_front never meet a null neighbour._remove and _add_front: these two helpers do all the pointer surgery. Every public method is built from them, which keeps the code short and the bugs few.put: check for an existing key first, then evict if full, then insert. Mixing the order leads to evicting the very key you are about to update.Code is easier to trust when you have watched it work. Here is a run with capacity 3. The last column shows the list from most recent to least recent after each step. I generated this table by actually running the code above.
| Step | Operation | Returns | Order (recent to old) | What happened |
|---|---|---|---|---|
| 1 | put(A,1) | – | A | A inserted |
| 2 | put(B,2) | – | B → A | B inserted |
| 3 | put(C,3) | – | C → B → A | C inserted. Cache is now full |
| 4 | get(A) | 1 | A → C → B | Hit. A moves to the front |
| 5 | put(D,4) | – | D → A → C | Full, so B (least recent) is evicted |
| 6 | get(B) | -1 | D → A → C | Miss. B was evicted earlier |
| 7 | put(C,30) | – | C → D → A | C updated to 30 and refreshed |
| 8 | get(D) | 4 | D → C → A | Hit. D moves to the front |
| 9 | put(E,5) | – | E → D → C | Full, so A (least recent) is evicted |
| 10 | get(A) | -1 | E → D → C | Miss. A was evicted earlier |
| 11 | get(C) | 30 | C → E → D | Hit. C moves to the front |
Look at step 5. The cache holds A, C, B with B at the back, so adding D evicts B. At step 6 we ask for B and get -1, because it was evicted. Step 7 updates C, and step 9 evicts A, which has not been touched since step 4. Every eviction was the item that had waited longest.
Python gives you a dictionary that remembers insertion order and can move items to the end in constant time. That means you can write an LRU cache in about fifteen lines.
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.data = OrderedDict()
def get(self, key):
if key not in self.data:
return -1
self.data.move_to_end(key) # mark as most recently used
return self.data[key]
def put(self, key, value):
if self.capacity <= 0:
return
if key in self.data:
self.data.move_to_end(key)
self.data[key] = value
if len(self.data) > self.capacity:
self.data.popitem(last=False) # drop the oldest entry
This version is shorter, and in real projects it is usually the right choice. But in an interview, or when you want to understand the machinery, build the manual version first. Interviewers often use the OrderedDict version as a follow-up: “Great, now tell me how that works inside.” The answer is that it is a hash map plus a doubly linked list, exactly like ours.
There is also a ready-made option for caching function results. The functools.lru_cache decorator wraps a function and remembers its most recent calls, discarding the least recently used when it hits its limit.
JavaScript’s Map remembers insertion order too. Delete and re-insert a key to make it the most recent, and the first key in the map is always the least recent.
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.map = new Map(); // Map remembers insertion order
}
get(key) {
if (!this.map.has(key)) return -1;
const value = this.map.get(key);
this.map.delete(key);
this.map.set(key, value); // re-insert = most recent
return value;
}
put(key, value) {
if (this.capacity <= 0) return;
if (this.map.has(key)) this.map.delete(key);
this.map.set(key, value);
if (this.map.size > this.capacity) {
const oldest = this.map.keys().next().value;
this.map.delete(oldest); // first key = least recent
}
}
}
This is neat for small caches. For very large or very hot caches, a hand-built linked list can have better performance characteristics, since the delete-and-reinsert approach relies on the engine’s implementation details. For most web applications, this version is perfectly good.
| Step inside get or put | Cost |
|---|---|
| Look up key in hash map | O(1) on average |
| Unlink a node (doubly linked) | O(1), a few pointer assignments |
| Insert after head | O(1), a few pointer assignments |
| Find least recent (tail’s previous) | O(1), no scanning |
| Delete from hash map | O(1) on average |
Every step is constant time, so get and put are O(1) on average. The memory cost is O(capacity), because we store at most that many nodes and map entries.
Tap through five places you already rely on, each with the reasoning behind it.
You open ten apps during the day. Later you return to a map app you used this morning, and it restarts from scratch while the app you used five minutes ago is still there.
Why LRU. Mobile systems keep recently used apps in memory and reclaim the ones untouched for longest when memory runs low. It is not a pure LRU cache, since other factors matter too, but the intuition is the same: the app you just used is the one you are most likely to use again.
A website caches user profiles in memory. The cache has a fixed size, and new profiles keep arriving.
maxmemory 100mb
maxmemory-policy allkeys-lruWhy LRU. When memory is full, Redis can evict keys using an LRU policy. In practice, Redis uses an approximated LRU: it samples a few keys and removes the best candidate, which avoids the cost of tracking exact order for millions of keys. The result is close to true LRU at a fraction of the bookkeeping.
Your processor keeps recently used memory lines close, and your operating system keeps recently read disk pages in RAM.
Why LRU. Exact LRU needs bookkeeping on every access, which is expensive in hardware and in kernels, so real systems use cheaper approximations such as pseudo-LRU in CPU caches and clock-style algorithms for memory pages. The goal is the same as ours, only the implementation is trimmed for speed.
A database keeps frequently read table pages in memory. Then someone runs a huge report that reads every row once.
Why LRU. A plain LRU would let that one-time scan push out all the genuinely hot pages. Many databases therefore use LRU variants. MySQL’s InnoDB, for example, inserts new pages at a midpoint of its list, so one-off reads do not instantly displace pages that are used repeatedly.
A function computes something slow, like fetching an exchange rate, and the same inputs keep coming back.
from functools import lru_cache
@lru_cache(maxsize=128)
def get_rate(currency):
... # slow work here
get_rate("USD")
print(get_rate.cache_info())Why LRU. The standard library already ships an LRU cache as a decorator. It remembers the results of the last 128 distinct calls and evicts the least recently used one when a new call arrives. Building your own, as in this article, is how you learn what it does under the hood.
Notice how often real systems use an approximation of LRU. Perfect order tracking costs memory and time on every access, so engineers trade a little accuracy for a lot of speed. Understanding the exact version, which is what we built, is what lets you reason about those approximations.
Claims are cheap, so I ran an experiment. I generated 20,000 requests over 200 different keys, with some keys far more popular than others, which is how real traffic tends to behave. Then I replayed the same requests against LRU, FIFO and random eviction at four cache sizes.
| Cache size | LRU hit rate | FIFO hit rate | Random hit rate |
|---|---|---|---|
| 10 | 31.8% | 27.8% | 28.0% |
| 20 | 46.2% | 40.5% | 40.2% |
| 40 | 61.5% | 55.6% | 55.4% |
| 80 | 77.4% | 72.2% | 72.6% |
LRU gets roughly four to six percentage points more hits than FIFO and random eviction at each size. That may sound modest, but remember what a hit means: a request answered from fast memory instead of a slow database or network call. Across millions of requests, those extra hits add up to real money and real seconds.
The result also shows something else: bigger caches help a lot. Going from 10 slots to 80 lifts LRU from about 32% to about 77%. Eviction policy matters, but capacity matters more.
I would be misleading you if I presented LRU as perfect. It has two classic weaknesses, and knowing them is the mark of someone who has used caches in production.
Suppose your cache holds 10 items and your program loops over 11 different keys, again and again. Every time you ask for a key, it has just been evicted, because it was the least recently used. The result: a hit rate of exactly 0%. I confirmed this by running it: with a loop of 11 keys and a capacity of 10, both LRU and FIFO miss on every single request after the first pass.
Imagine a database that caches popular rows, and then someone runs a report that reads every row once. Each of those one-time reads enters the cache as the “most recently used” and pushes out the genuinely popular items, which are never used again by the scan. This is called cache pollution. Variants such as 2Q, LRU-K and ARC, or a midpoint-insertion scheme, exist specifically to resist it.
The lesson is not to avoid LRU. It is to know that no policy is best for every workload, and to measure your own traffic before deciding.
When I review an LRU cache, I look for these cases. They catch most bugs.
get on an empty cache returns -1.The randomised check is my favourite. It finds the strange edge cases you did not think to write by hand.
If get does not move the node to the front, you have built a FIFO cache, not an LRU cache.
When you evict the tail node you need its key to remove the map entry. Without it, you cannot clean up the dictionary.
Updating a value is also a use. Update the value and move the node to the front.
You cannot remove a middle node in constant time because you cannot reach its predecessor. That breaks the O(1) requirement.
Without sentinels you need special cases for empty lists and for the first and last real nodes. Those special cases are where bugs hide.
The cache is not safe when several threads call it at once. Wrap operations in a lock, or choose a library built for concurrency.
Tap each question to reveal the answer and its reasoning.
To unlink a node you must update the previous node’s next pointer. A prev pointer lets you do that without walking the list.
When you evict the tail node, you must delete that key from the map too, and the node is the only place that knows it.
After get(1), key 1 is most recent, so put(3,3) evicts key 2. Then get(2) is a miss, returning -1.
The hash map finds the node in O(1) on average, and the linked list moves or removes it in O(1).
With capacity N and a repeating loop over N + 1 keys, each key is evicted just before it is needed again, so every access misses.
Least Recently Used. When the cache is full, the entry that has gone unused for the longest time is removed first.
A hash map gives instant lookup but remembers no order. A linked list remembers order and lets you reorder in constant time, but searching it is slow. Combined, each covers the other’s weakness.
Yes. In Python, OrderedDict has move_to_end and popitem(last=False), which give you the whole behaviour in a few lines. Interviewers often ask for the manual version to see whether you understand how it works.
No. It does well when recent use predicts future use. It does poorly with large one-time scans and with loops slightly bigger than the cache. LFU, 2Q and ARC are alternatives that handle some of those cases.
No. Concurrent get and put calls could corrupt the list. In multi-threaded code, protect every operation with a lock, or use a library designed for concurrency.
LRU evicts what was used least recently. LFU (Least Frequently Used) evicts what has been used the fewest times in total. LFU favours long-term favourites, while LRU adapts faster to changing interests.
An LRU cache is two ideas working together: a hash map that finds any item instantly, and a doubly linked list that keeps items in order of use. The map points into the list, the list tells you who is oldest, and every operation is a few pointer moves.
Build it once from scratch, test it with a randomised comparison, and you will understand not just this cache but the habit behind many great data structures: when one tool is weak where another is strong, connect them.
The post LRU Cache: Build One With a Hash Map and a Linked List appeared first on Learn With Examples.
]]>The post Git and GitHub Explained for Beginners (With Real Examples) appeared first on Learn With Examples.
]]>If you have ever saved a file as report_final_v2_REAL_final.docx, you already understand why Git exists. It is the grown-up version of that habit: a tool that remembers every version of your work, lets you travel back in time, and lets a team edit the same project without overwriting each other. GitHub is where those projects usually live online. This guide explains both from zero.
I started programming long before Git was everywhere, and I remember what folders looked like in those days. site_backup, site_backup_old, site_NEW, site_NEW_fixed, and one called site_dont_touch. Nobody knew which was current. When a client said “the version from last Tuesday was better,” the honest answer was often “I think I overwrote it.”
Now picture a team of five editing the same files. Two people change the same paragraph. Someone emails a zip file. Someone else works from an older copy. Merging all of that by hand is miserable, and mistakes are guaranteed.
Version control is the fix. Instead of copying folders, you tell a tool when you have reached a meaningful point, and it stores a snapshot. The most popular version control system in the world is Git, created in 2005 by Linus Torvalds for the Linux kernel. Today it is used by solo students, small studios and the largest software companies alike.
The one-sentence version. Git is a tool on your computer that records snapshots of your project over time. GitHub is a website that stores your Git projects online so you can back them up, share them and collaborate.
Beginners often use the two names as if they were one thing. They are not, and the difference matters.
The analogy I use in workshops: Git is a camera, and GitHub is the photo-sharing site. You can take photos without ever uploading them, but the website makes it easy to back them up and show them to others. Similar sites exist, including GitLab and Bitbucket. They all speak Git.
| Question | Git | GitHub |
|---|---|---|
| What is it? | A version-control program | A hosting website and platform |
| Where does it run? | On your computer | Online, in the cloud |
| Needs internet? | No | Yes |
| Main job | Track and organise your changes | Store, share and review them with others |
| Alternatives | Mercurial, Subversion | GitLab, Bitbucket |
Git has a reputation for jargon. In reality, you need about eight words, and each has a plain-English meaning.
| Term | Plain meaning | Everyday analogy |
|---|---|---|
| repository | A project folder that Git tracks, with its full history | A filing cabinet with every past version |
| commit | A saved snapshot with a message | A save point in a video game |
| branch | A separate line of work | A parallel draft you can experiment in |
| merge | Combining one branch into another | Pasting your draft into the main document |
| clone | Copying a repository to your computer | Downloading the whole cabinet |
| remote | A copy of the repository hosted elsewhere | The cloud backup |
| push / pull | Send your commits up / bring others’ commits down | Upload and download |
| pull request | A request to merge your branch, with review | “Please check my work before it goes in” |
This is the single most useful mental model in the whole subject, and almost every beginner tutorial skips it. A change passes through four places.
.git folder.The staging area surprises people. Why not save everything at once? Because it lets you craft tidy commits. Imagine you fixed a typo and also started a risky new feature in the same afternoon. Staging lets you commit the typo fix by itself, with a clear message, and leave the unfinished feature for later.
Download Git from the official site (git-scm.com) or install it with your system’s package manager. Then check that it works.
git --version
Next, tell Git who you are. Every commit carries a name and email, so this is a one-time setup.
git config --global user.name "Your Name"
git config --global user.email "[email protected]"
git config --global init.defaultBranch main
The last line makes new repositories start with a branch called main, which matches what GitHub uses today. Older installs may still default to master, so setting it avoids confusion.
Let us track a small recipe collection. Open a terminal, make a folder and start Git inside it.
mkdir recipes
cd recipes
git init
Git replies with something like:
Initialized empty Git repository in /home/you/recipes/.git/
Now create a file called pasta.txt with three lines of text in any editor, then ask Git what it sees.
git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
pasta.txt
nothing added to commit but untracked files present (use "git add" to track)
“Untracked” means Git sees the file but is not recording it yet. git status is your best friend: run it constantly, especially when confused. Now stage the file and commit it.
git add pasta.txt
git commit -m "Add pasta recipe"
[main (root-commit) a1b2c3d] Add pasta recipe
1 file changed, 3 insertions(+)
create mode 100644 pasta.txt
Congratulations, you have made your first snapshot. The odd string a1b2c3d is the start of a commit’s unique ID. (Yours will look different; the examples here use made-up IDs.) Add a sauce recipe and a typo fix the same way, and your history looks like this.
git log --oneline
1f2e3d4 (HEAD -> main) Add dessert
7c8d9e0 Fix typo
e4f5a6b Add sauce
a1b2c3d Add pasta recipe
Every dot in that chain is a version you can return to. That is the whole magic: history becomes a thing you can read, search and travel through.
A commit message is a note to your future self, and to everybody else. Compare these two histories:
The second tells a story. A few habits help: write in the imperative (“Add”, not “Added”), keep the first line under about 50 characters, and describe what and why, not how. Six months later, when something breaks, you will be searching that history for answers, and clear messages turn a two-hour hunt into a two-minute one.
Not every file belongs in version control. Passwords, API keys, temporary files and huge generated folders should stay out. Create a plain text file named .gitignore and list patterns.
.env
node_modules/
*.log
.DS_Store
This is more than tidiness. Never commit secrets such as API keys or passwords. Once something is pushed to a public repository, bots can find it within minutes. If it happens, rotate the key immediately. Deleting the file later does not erase it from history.
A branch is simply a movable label on a line of commits. Your main branch holds the version that works. When you want to try something, you create a new branch, work there, and leave main untouched.
git switch -c feature/bigger-portions
# edit files, then
git add pasta.txt
git commit -m "Increase pasta portion size"
Think of it as photocopying a document to scribble on, with the guarantee that the original stays clean. If the idea works, you merge it in. If it does not, you delete the branch and nothing is lost.
When you are ready, switch back to main and merge.
git switch main
git merge feature/bigger-portions
Sometimes two branches change the same line. Git cannot guess which you want, so it stops and asks you to decide. It marks the file like this.
<<<<<<< HEAD
Use 200g of spaghetti
=======
Use 250g of spaghetti
>>>>>>> feature/bigger-portions
The top part is your current branch, the bottom part is the incoming one. Edit the file to keep what you want (and delete the marker lines), then git add it and git commit. A conflict is just Git being polite and asking a question. In teams, conflicts become rare once people commit small changes often and pull regularly.
So far everything has lived on your computer. Now let us put it online. Create a free account at github.com, click the button to create a new repository, and name it recipes. GitHub will show you the web address of your empty repository. Connect your local project to it and upload.
git remote add origin https://github.com/YOU/recipes.git
git branch -M main
git push -u origin main
Here origin is just the nickname Git gives to your main remote. The -u flag remembers the connection so that later you can type only git push and git pull.
A practical note on logging in: GitHub no longer accepts your account password for Git operations over HTTPS. Use a personal access token, set up SSH keys, or install the GitHub CLI or Git Credential Manager, which handle sign-in for you. Their setup pages walk through it in a few minutes.
To get a project that already exists on GitHub onto your machine, you clone it.
git clone https://github.com/YOU/recipes.git
That downloads every file and the whole history, and sets up origin automatically.
On a team, nobody pushes straight to main. Instead, the loop looks like this.
A pull request (often shortened to PR) is a page on GitHub that shows exactly what changed between your branch and main. Teammates can comment on specific lines, request changes and approve. When everyone is happy, someone clicks Merge. Automated checks, such as tests, can run on the PR too, so a broken change is caught before it reaches the shared code.
I have watched this process save projects. A new developer once pushed a change that would have deleted a customer table. Two people spotted it in review because the diff showed hundreds of red lines where there should have been five. Without a pull request, that change would have gone straight to production.
Tap through the tabs. Each one shows a realistic scenario with the actual commands.
You are writing a small recipe book as text files and want a safety net. No team, no internet needed.
git init
git add pasta.txt
git commit -m "Add pasta recipe"
git log --onelineWhy it works. Each commit is a snapshot you can return to. After a month of edits you can look back and see exactly when the sauce recipe changed, and why.
You edited the wrong file, or committed something you regret. Git has a different tool for each size of mistake.
# discard edits you have not committed
git restore pasta.txt
# take a file back out of the staging area
git restore --staged pasta.txt
# undo a commit by adding a new opposite commit
git revert 7c8d9e0Why it works. Use restore for uncommitted edits, and revert when the commit has already been shared, because it leaves history intact. Treat reset –hard as the sharp knife: it can throw work away.
You and three colleagues are adding a search bar to a website. Nobody should break the working version.
git switch -c feature/search
# ...edit files...
git add .
git commit -m "Add search box"
git push -u origin feature/search
# open a pull request on GitHub, get a review, merge
git switch main
git pullWhy it works. Everyone works on their own branch, then proposes changes with a pull request. A teammate reads the changes before they reach main. That review step catches more bugs than most people expect.
You found a typo in a popular project’s documentation and want to fix it, but you are not a maintainer.
# click Fork on GitHub, then:
git clone https://github.com/YOU/project.git
cd project
git switch -c fix-typo
# fix the typo, then
git commit -am "Fix typo in README"
git push -u origin fix-typo
# open a pull request to the original projectWhy it works. A fork is your personal copy of someone else’s repository on GitHub. You change your copy and ask the owners to pull your change in. This is how thousands of strangers contribute to the same project.
You built a one-page portfolio and want it online without paying for hosting.
git add index.html
git commit -m "Launch portfolio"
git push origin main
# On GitHub: Settings, Pages, deploy from branch mainWhy it works. GitHub Pages serves static files straight from a repository. After setup, every push updates the live site. Your history doubles as a deployment log.
You will notice the same handful of commands appearing in every tab. That is the good news: the entire daily workflow of most developers is built from perhaps ten commands.
| Command | What it does | When to use it |
|---|---|---|
| git status | Shows what has changed and what is staged | Constantly. Any time you are unsure. |
| git add <file> | Stages a file for the next commit | Before every commit |
| git commit -m “msg” | Saves a snapshot of staged changes | After each small, finished step |
| git log –oneline | Lists past commits in short form | To see history |
| git diff | Shows exact line changes not yet staged | Before staging, to review your work |
| git switch -c name | Creates and switches to a new branch | Starting a new task |
| git merge name | Merges a branch into the current one | Finishing a task locally |
| git push | Uploads commits to the remote | Sharing or backing up your work |
| git pull | Downloads and merges remote changes | Start of your working day |
| git clone <url> | Copies a remote repository to your computer | Joining an existing project |
| git restore <file> | Discards uncommitted edits to a file | When an experiment went wrong |
| git revert <id> | Undoes a commit by adding an opposite one | Undoing something already shared |
git switch main and git pull so you start from the latest version.git switch -c and a descriptive name.git status and git diff often, and commit each small finished step.It feels like a lot at first, and then one day you realise you have stopped thinking about it. That is when Git becomes a superpower instead of a chore.
Messages like “update” tell nobody anything. Take ten seconds to describe the change. Your future self is the main beneficiary.
Add sensitive files to .gitignore from day one. If a key leaks, revoke it immediately; deleting the file later does not remove it from history.
Even solo, branches give you a safe place to experiment. On teams, working on main is how broken code reaches everyone.
If a teammate has pushed since you last pulled, your push may be rejected. Pull first, resolve any conflicts, then push.
It can destroy uncommitted work permanently. Prefer git restore for single files and git revert for shared commits unless you are certain.
A commit with a bug fix, a refactor and a new feature is hard to review and hard to undo. Stage and commit them separately.
A conflict is a question, not a failure. Read the markers, choose what to keep, remove the markers, add, and commit.
Tap each question to reveal the answer and the reasoning.
Git tracks changes locally. GitHub hosts repositories online and adds collaboration features such as pull requests.
git add stages changes. git commit then saves the staged snapshot into history.
git restore puts the file back to its last committed state. Be sure you want to lose those edits first.
A pull request asks the project to pull your changes in, and lets teammates review them first.
List patterns in .gitignore for files Git should leave alone. Secrets that were already committed need extra steps, so never commit them in the first place.
No. Git works completely on your own computer. GitHub is optional, but it makes backup, sharing and teamwork much easier. GitLab and Bitbucket are alternatives.
A repository, or repo, is a project folder that Git tracks, along with the full history of its changes stored in a hidden .git folder.
git fetch downloads new history from the remote without changing your files. git pull fetches and then merges it into your current branch.
It copies an entire repository from a remote location such as GitHub to your computer, including its history, and sets up the connection called origin.
Commit whenever you finish a small, meaningful step that you could describe in one sentence. Small commits are easier to review and easier to undo.
Treat it as compromised: change the password or revoke the key right away. Removing it from history is a separate, harder job, so rotating the secret comes first.
Git remembers every version of your project, GitHub keeps a copy online and helps people work together, and the daily routine boils down to a few commands: status, add, commit, push, pull, and switch. You do not need to master everything. You need to commit small, write clear messages, work on branches and never commit secrets.
Make a practice repository today. Add three files, make five commits, create a branch, merge it, push it to GitHub. An hour of doing beats a week of reading, and you will never again have a folder called “final_v2_REAL_final.”
The post Git and GitHub Explained for Beginners (With Real Examples) appeared first on Learn With Examples.
]]>