
splice-style shifting.A hash table was great — but only for price === 50. The moment you ask price < 50, it's helpless. So what's missing?
The answer is hiding in plain sight: if the data were sorted, "less than 50" would be a window, not a hunt. A sorted structure lets you "land near a value" and then walk.
So the move is obvious: keep the rows sorted by the field you search on. Let's do exactly that.
If our books are kept sorted by price, then "under 50" isn't a full sweep — it's "start at the front, walk until prices exceed 50." And crucially, finding a specific value stops being O(n).
But how do you find it fast? You can't just guess the index. You need a way to zero in. That's binary search.
The idea is the classic "guess the middle, decide which half." Each probe discards half the candidates.
// find the index of `target` in a sorted array, or -1
function binarySearch(arr, target) {
let lo = 0;
let hi = arr.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (arr[mid] === target) return mid;
if (arr[mid] < target) lo = mid + 1; // target is on the right half
else hi = mid - 1; // target is on the left half
}
return -1;
}Why is this O(log n)? Because every step halves the remaining range. 100k items → 50k → 25k → ... → done in about 17 steps. That's the geometric magic: doubling the data adds only one step.
data: 10 -> 2^10 items ~10 steps
data: 100 -> 2^100 items ~100 steps (absurdly huge)Binary search alone is nice, but the reason databases love sorted structures is ranges. Watch: to get "price < 50," you don't find one item, you find where the boundary is, then walk.
// lowest index whose value is >= target — the "lower bound"
function lowerBound(arr, target) {
let lo = 0, hi = arr.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (arr[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo;
}
// "SELECT * FROM books WHERE price < 50" → walk from index 0 to lowerBound(50)
const start = 0;
const end = lowerBound(prices, 50);
for (let i = start; i < end; i++) console.log(rows[i]);Two nice properties fall out:
binarySearch → O(log n);lowerBound, then sweep a contiguous run → you only touch the results, not the whole table.That's the payoff of "sorted." It fixed what the hash table couldn't.
But you paid a price. When you insert a new row, where does it go? By the sorted field — and that means everything after it must shift to make room.
// insert a row with price 40 into a sorted-by-price array
const rows = [{price:10},{price:20},{price:50}];
// want 40 in between 20 and 50 → all from index 2 onward must move right
rows.splice(2, 0, {price:40});
// [{price:10},{price:20},{price:40},{price:50}]In the worst case that's O(n) — a single insert might move 100k entries. Over many inserts, that's brutal. So here's the dilemma you've now carved out with your own hands:
You want searches fast and inserts fast. No array can do both. History's answer to "is there a structure that does both?" is a tree — and that's Part 5.
binarySearch on 1 million sorted numbers for a few targets, and log how many times the loop ran. Confirm it's ~20, not ~1M.lowerBound to answer price >= 30 && price < 60, and print how many rows you visited — compare it to scanning all rows.splice insert in the middle, then time 1000 of them. Note how it degrades.lowerBound finds the boundary, then you sweep only the results → cheap ranges;Keywords: sorted array、binary search、O(log n)、range query、lower bound、insertion cost O(n)
You've now seen it from both sides: the hash table is fast but range-blind; the sorted array handles ranges but insertion is O(n). So you've been forced into a genuinely interesting question: is there a structure where both find and insert are cheap?
That's the story of the tree — and specifically the structure real databases actually use by default, the B+ tree, which is Part 5.