Merge Sort Tree — Practice Tasks¶
All tasks should be solved in Go, Java, and Python. Each comes with constraints, expected output, and (where useful) a reference solution or solution sketch. Build on the
MergeSortTreefromjunior.md/interview.md; translations between languages are mechanical.
The tasks are graded: Beginner (build and basic queries), Intermediate (k-th smallest, bands, offline comparison), Advanced (fractional cascading, rebuild strategies, alternatives). A benchmark task closes the set.
Beginner Tasks¶
Task 1 — Build the merge sort tree from scratch. Implement Build(a) that constructs a flat 4n-node tree where each node holds the sorted list of its index range, built by merging children (no calling a library sort on each node). Verify the root equals sorted(a) and every leaf is a single element.
- Constraints:
O(n log n)build via linear merges; allocate4nnodes; handlen = 0andn = 1. - Expected Output: for
a = [2,6,4,1,9,3], root list= [1,2,3,4,6,9]. - Evaluation: correct merge (not re-sort), correct allocation, edge cases.
Go¶
Java¶
public class Task1 {
public static void main(String[] args) {
// implement build and print the root's sorted list
}
}
Python¶
def main():
# implement build and print the root's sorted list
pass
if __name__ == "__main__":
main()
Task 2 — countLE(l, r, k). Implement the count of indices i ∈ [l, r] with A[i] ≤ k. Cross-check against an O(n) brute-force scan on random arrays of length 50, including duplicate-heavy arrays.
- Constraints: use
upperBound(bisect_right) so ties withkcount;O(log² n)per query. - Expected Output:
countLE(1, 4, 5) == 2fora = [2,6,4,1,9,3]. - Evaluation: correctness vs brute force across all
(l, r, k)on small arrays.
Reference (Python):
def brute_count_le(a, l, r, k):
return sum(1 for i in range(l, r + 1) if a[i] <= k)
import random
for _ in range(200):
a = [random.randint(0, 9) for _ in range(random.randint(1, 50))]
t = MergeSortTree(a)
for _ in range(50):
l = random.randrange(len(a)); r = random.randint(l, len(a) - 1)
k = random.randint(-1, 10)
assert t.count_le(l, r, k) == brute_count_le(a, l, r, k)
print("countLE OK")
Task 3 — countInBand(l, r, a, b). Count indices i ∈ [l, r] with a ≤ A[i] ≤ b, using countLE(l,r,b) − countLE(l,r,a−1). Test that countInBand(0, 5, 3, 6) == 3 for a = [2,6,4,1,9,3] (elements 6, 4, 3).
- Constraints: guard
a > b(return 0); avoid underflow whenais the minimum value. - Expected Output:
3for the sample. - Evaluation: correct subtraction, tie handling at both ends, brute-force cross-check.
Task 4 — Count strictly-less-than and strictly-greater-than. Add countLT(l, r, k) (count < k) and countGT(l, r, k) (count > k). Express each via lowerBound / upperBound and a range-length subtraction: countGT = (r − l + 1) − countLE(l, r, k).
- Constraints: must distinguish
≤/</>correctly on arrays full of duplicates. - Expected Output: for
a = [5,5,5,5],countLT(0,3,5)==0,countLE(0,3,5)==4,countGT(0,3,5)==0. - Evaluation: correctness across the three predicates with ties.
Task 5 — Single-element and empty edge cases. Write tests that exercise n = 0 (all queries return 0), n = 1, l > r (return 0), k below all values (0), and k ≥ max (r − l + 1). Confirm no out-of-bounds and no crashes.
- Constraints: the constructor must not crash on
n = 0; queries must not recurse onl > r. - Expected Output: every edge case returns the documented value.
- Evaluation: robustness; allocate
4 * max(1, n).
Intermediate Tasks¶
Task 6 — K-th smallest in a range (binary search on the answer). Implement kthSmallest(l, r, k) returning the k-th smallest value (1-indexed) in A[l..r]. Coordinate-compress the distinct values and binary-search for the smallest value v with countLE(l, r, v) ≥ k.
- Constraints:
O(log³ n)is acceptable; compress values first. - Expected Output: for
a = [2,6,4,1,9,3],kthSmallest(0, 5, 1) == 1,kthSmallest(0, 5, 4) == 4,kthSmallest(1, 4, 2) == 4(range[6,4,1,9]sorted[1,4,6,9], 2nd is 4). - Evaluation: correctness vs brute-force
sorted(a[l:r+1])[k-1].
Reference sketch (Python):
def kth_smallest(t, a, l, r, k):
vals = sorted(set(a))
lo, hi = 0, len(vals) - 1
while lo < hi:
mid = (lo + hi) // 2
if t.count_le(l, r, vals[mid]) >= k:
hi = mid
else:
lo = mid + 1
return vals[lo]
Task 7 — LeetCode 315 style: count smaller after self. Given nums, return counts[i] = number of j > i with nums[j] < nums[i]. Solve with a merge sort tree: counts[i] = countLT(i+1, n-1, nums[i]).
- Constraints:
O(n log² n)total; handle the empty suffix (i = n-1→ 0). - Expected Output:
nums = [5,2,6,1]→[2,1,1,0]. - Evaluation: matches the brute-force
O(n²)reference.
Task 8 — Offline BIT sweep vs merge sort tree. Implement the offline count-≤-k solution (sort queries by k, sweep a Fenwick keyed by index) and compare its answers and wall time against the merge sort tree on the same (l, r, k) query set.
- Constraints: BIT sweep is
O((n + q) log n); produce identical answers. - Expected Output: answer vectors equal; print both timings.
- Evaluation: correctness parity + a short note on which was faster and why (offline drops a
log, smaller constant).
Task 9 — Count-in-band stress test with negatives and duplicates. Build a randomized property test: random arrays with negative values and many duplicates, random [l, r] and [a, b] bands, asserting countInBand matches brute force. Include a = b, a > b, and b ≥ max.
- Constraints: no underflow on
a − 1; correct tie handling at both band ends. - Expected Output: thousands of random cases pass.
- Evaluation: the test catches
lowerBound/upperBoundswaps and off-by-one band bugs.
Task 10 — Hybrid block-scan variant (drop bottom levels). Build a merge sort tree that stores sorted lists only at nodes covering ≥ B elements; for sub-B ranges, scan the raw array. Measure the memory saved versus the full tree and the query-time cost of the scans.
- Constraints: correctness identical to the full tree; tune
B(e.g.,B = 32). - Expected Output: report memory (cells stored) and query time for
B ∈ {1, 16, 32, 64}. - Evaluation: correct hybrid query; a table of the memory/time trade-off.
Advanced Tasks¶
Task 11 — Fractional cascading. Augment the merge sort tree with bridge pointers (Lbridge, Rbridge) so that a countLE query does one binary search at the root and O(1) hops thereafter, achieving O(log n) per query. Verify answers match the plain tree and measure the speedup at n = 10^5.
- Constraints: build remains
O(n log n); query becomesO(log n); answers identical to plain tree. - Expected Output: identical answer vector; query time noticeably lower than plain
O(log² n). - Evaluation: correct bridges, correct rank reconstruction along the path, measured speedup.
Task 12 — Seal-and-build partitioned service. Simulate an append-mostly system: a small mutable "live" buffer plus immutable per-partition merge sort trees. On a "seal" event, build a tree for the live buffer and clear it. A countLE over a window spanning sealed + live answers the sealed part via trees and scans the live tail.
- Constraints: sealed trees immutable; live scan
O(live size); correct combination. - Expected Output: queries over mixed windows match a brute-force over the whole logical array.
- Evaluation: correct partition routing and result combination; atomic swap of the published tree set.
Task 13 — Delta-log hybrid (LSM-style). Keep an immutable merge sort tree plus a delta buffer of recent inserts/deletes. A countLE answers tree.countLE(...) then corrects with a linear scan of the delta buffer. When the buffer exceeds a threshold, rebuild (fold deltas in) and clear it.
- Constraints: bounded staleness via the threshold; correct correction sign for inserts vs deletes.
- Expected Output: queries match a brute force over (base + deltas) at all times.
- Evaluation: correct correction logic, rebuild trigger, and post-rebuild consistency.
Task 14 — Compare four structures on k-th smallest. Implement k-th-smallest-in-range with (a) merge sort tree + binary-search-on-answer, (b) a persistent segment tree, (c) sqrt decomposition with sorted blocks, and (optionally) (d) a wavelet tree. On the same query set, verify identical answers and tabulate build time, query time, and memory.
- Constraints: identical answers across all implemented structures.
- Expected Output: a comparison table (build/query/memory) with a one-line recommendation per scenario.
- Evaluation: parity of answers; an honest trade-off discussion (online/offline, memory, k-th vs count).
Task 15 — Updatable range-count via sqrt decomposition. Since the merge sort tree can't update, implement the standard substitute: sqrt decomposition with sorted blocks supporting pointUpdate(i, v) and countLE(l, r, k). A point update re-sorts (or binary-inserts into) one block; a count binary-searches each fully-covered block and scans partial ends.
- Constraints: update
O(√n)/O(√n log √n); queryO(√n log √n); correct against a brute force under interleaved updates and queries. - Expected Output: mixed update/query stream matches the brute-force reference.
- Evaluation: correct block rebuild on update; correct query combination; demonstrates the "merge sort tree with updates" alternative.
Benchmark Task¶
Compare performance across all 3 languages for build +
countLEat scale.
Go¶
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
sizes := []int{1000, 10000, 100000}
for _, n := range sizes {
a := make([]int, n)
for i := range a {
a[i] = rand.Intn(1_000_000)
}
start := time.Now()
t := Build(a) // from your implementation
build := time.Since(start)
start = time.Now()
var sink int
for i := 0; i < 100000; i++ {
l := rand.Intn(n)
r := l + rand.Intn(n-l)
sink += t.CountLE(l, r, rand.Intn(1_000_000))
}
fmt.Printf("n=%7d build=%v 100kq=%v sink=%d\n", n, build, time.Since(start), sink)
}
}
Java¶
import java.util.Random;
public class Benchmark {
public static void main(String[] args) {
int[] sizes = {1000, 10000, 100000};
Random rng = new Random(1);
for (int n : sizes) {
int[] a = new int[n];
for (int i = 0; i < n; i++) a[i] = rng.nextInt(1_000_000);
long s = System.nanoTime();
Solution t = new Solution(a); // your merge sort tree
long build = System.nanoTime() - s;
s = System.nanoTime();
long sink = 0;
for (int i = 0; i < 100000; i++) {
int l = rng.nextInt(n);
int r = l + rng.nextInt(n - l);
sink += t.countLE(l, r, rng.nextInt(1_000_000));
}
System.out.printf("n=%7d build=%.1fms q=%.1fms sink=%d%n",
n, build / 1e6, (System.nanoTime() - s) / 1e6, sink);
}
}
}
Python¶
import random, time
for n in [1000, 10000, 100000]:
a = [random.randrange(1_000_000) for _ in range(n)]
s = time.perf_counter()
t = MergeSortTree(a)
build = time.perf_counter() - s
s = time.perf_counter()
sink = 0
for _ in range(100000):
l = random.randrange(n)
r = l + (random.randrange(n - l) if n - l else 0)
sink += t.count_le(l, r, random.randrange(1_000_000))
print(f"n={n:>7} build={build*1000:.1f}ms q={(time.perf_counter()-s)*1000:.1f}ms sink={sink}")
Report: for each n, build time, 100k-query time, and resident memory. Confirm the query time grows roughly like log² n and the memory like n log n. Note where Python's interpreter overhead dominates and where coordinate compression would help.
Wrap-up¶
After these tasks you will have:
- Built and stress-tested the static merge sort tree (
countLE, band,</>). - Implemented k-th smallest by binary-searching the answer.
- Compared it against the offline BIT sweep, persistent segment tree, sqrt decomposition, and (optionally) the wavelet tree.
- Implemented fractional cascading to reach
O(log n)queries. - Practiced the rebuild strategies (seal-and-build, delta log) and the updatable substitute (sqrt decomposition) that make the static structure usable in real systems.
That covers the full operational range of the merge sort tree. Revisit senior.md for production trade-offs and professional.md for the proofs behind these implementations.