You should know all theory presented at the lecture: definitions of data structures and operations on them, all theorems and proofs thereof. The exam is oral with written preparation. You will get one major and one minor question from the following list.
Major questions
Define the Splay tree. Describe operations Splay, Find, Insert, and Delete. Compare Splay trees with other data structures, in particular balanced search trees. State and prove the theorem on amortized complexity of the Splay operation.
splay tree = self-adjusting binary search tree
static optimal trees minimize the total search time for a known sequence of operations find
we don't know the sequence → we move every searched element to the root
operation splay(x) … pull node x to the root
preferably using double rotation (zigzig, zigzag)
z has child y
y has child x
zigzig(x)/zigzag(x) moves x above both y and z
zigzig is used if x<y<z or z<y<x
zigzag is used if y<x<z
possibly with one single rotation (zig) – as the last step
y has child x
zig(x) makes y child of x
splay performs a sequence of the operations until x becomes the root
operations
find(x) or insert(x) or delete(x)
splay(lowest visited node)
lemma: for a,b,c∈R+ satisfying a+b≤c, it holds that log(a)+log(b)≤2log(c)−2
4ab=≤c2(a+b)2−≥0(a−b)2≤c2
log4+loga+logb≤logc2
note that we consider log2
notation, our choice of potential
s(x) … size = number of nodes in the subtree rooted at x (including x)
potential of a node x … Φ(x)=logs(x)
sometimes denoted as rank r(v)
total potential … Φ:=∑vΦ(v)
si,Φi … values after i-th step
p … parent of x
g … parent of p
theorem: the amortized cost of splay(x) is at most 3(logsi(x)−logsi−1(x))+1
zig step
Φi(x)=Φi−1(p)
Φi(p)<Φi(x)
the potential of the remaining nodes stays the same
note that a single splay takes at most O(n) time (if the tree is just a path)
Define the (a,b)-tree. Describe operations Find, Insert, and Delete. Analyze their complexity in the worst case. Compare (a,b)-trees with other data structures, in particular balanced search trees.
multiway search tree
k keys stored in a non-decreasing order in one node (x1,…,xk)
this node has k+1 children
subtrees T0,…,Tk
xi< keys in Ti<xi+1
terminology
internal node
external node (leaf) – contains no data, has no children
(a,b)-tree for a≥2,b≥2a−1 is a multiway search tree s.t.
a≤ # children in internal node ≤b
a−1≤ # keys ≤b−1
we relax this for the root to be able to store a single key in the entire data structure: 2≤ # children in root ≤b
all external nodes are on the same level
observation: for any (a,b)-tree of height h…
number of external nodes is at most bh
every node has at most b children
number of keys is at most bh−1
external nodes correspond to intervals – keys separate them
number of external nodes is at least 2ah−1
every node has at least a children (root has at least 2)
number of keys is at least 2ah−1−1
→ logb(n+1)≤h≤loga(2n+1)+1
h∈Ω(logbn)
h∈O(logan)=O(logalogn)
operation Find
O(logb) per node (to find the correct subtree)
in total O(logn⋅logalogb)=O(logn) if b is polynomially bounded
operation Insert(x)
we assume it is not there (Find ends in an external node)
we take the parent v of the external node; we add x there (and add an external node)
if v not oversized → stop
otherwise we split the node and push one of the keys (the middle one) to the upper node
we propagate this up
if the root is oversized, we create a new root
does the splitting create valid nodes?
oversized node has b keys → new nodes have ⌊2b−1⌋ and ⌈2b−1⌉ keys
we need ⌊2b−1⌋≥a−1
we have b≥2a−1, so this holds
operation Delete(x)
Find returns v
if v is not in the last internal level, we replace it by a suitable key from the nodes in the last internal level
suitable = successor
so now we are deleting a node v′ in the last internal level (including an external node)
if v′ not undersized → stop
otherwise, we borrow a key from a sibling (if it has at least a keys)
we perform a “rotation” of the keys (sibling to the parent, parent to the undersized node)
if a sibling does not have enough keys, we merge the nodes (we take a key from the parent)
in total, we get a−2+1+a−1=2a−2 keys which is alright (as b≥2a−1 and there can be at most b−1 keys)
this may propagate upwards if the parent does not have enough keys (again, we first try to borrow a key from a sibling)
complexity
Insert, Delete in O(blogalogn)
we need to manipulate at most b items at each level (merging/splitting the nodes)
Find in O(logblogalogn)
we need b small (probably 2a−1 or 2a)
2a to give some breathing space
if we consider such b, we can write O(alogalogn) and O(logalogalogn)
so we want a small → (2,4)
in practice, we store the trees in memory (we want one memory block to roughly correspond to one node) → (128,256)
comparison with other data structures
they are shallower than binary search trees → better disk/cache performance
unlike hash tables, they support range queries
Define I/O model for caches and compare cache-aware and cache-oblivious algorithms. Formulate a cache-oblivious algorithm for transposition of a square matrix. Analyze its time complexity and I/O complexity.
external memory model (I/O model)
we have internal and external memory
operations read (external → internal) and write (internal → external)
they are directly controlled by the programmer
definition: any algorithm has I/O complexity f(n,B,M) if for any input of size n it performs ≤f(n,B,M) read/write operations
M … total size of memory
B … size of one memory block
so the memory has M/B blocks
we usually care only about reads (there are more reads than writes)
cache-aware model
cache line of size B
size M of the cache
CPU knows B,M
assumptions
fully associative cache (any memory block can be in any cache line)
cache controller has an optimal strategy
it evicts the cache lines that won't be needed (or will be needed the latest in the future)
cache-oblivious model
same as cache-aware, but CPU does not know B,M
if our algorithm works well in this model, it may work well for any level of the cache (unlike the previous models which work well for certain values of parameters)
consider a matrix N×N
usually stored by rows
scanning by rows … O(N2/B+1) reads
scanning by columns … O(N2+1) if M<NB
matrix transposition
naive way: we traverse in both ways (by columns and by rows at once)
O(N2+1) block transfers if the memory is small
idea: split the problem into smaller subproblems which fit in the cache
“tiling” … tiles of size d×d
tiles on the main diagonal can be transposed directly
non-diagonal tiles have to be transposed and swapped with the opposite ones → we need two tiles to fit into the cache
number of tiles … ⌈N/d⌉2≤(N/d+1)2=O(N2/d2+1)
consider d=B
we need M≥2B2
usually, we make tall cache assumption … M=Ω(B2), “memory is at least as tall as it is wide”
we need O(B) transfers per tile → I/O complexity O(N2/B2+1)⋅O(B)=O(N2/B+1)
for cache-aware model
another approach: divide and conquer
consider N=2k
operation transpose (T)
T → 2T + TS
operation transpose and swap (TS)
TS → 4TS
each task splits into at most 4 subtasks
at level i, we will have at most 4i nodes
number of levels … logN=k
each time, we reduce the size by two
number of leaves ≤4logN=N2
number of internal nodes ≤ number of leaves ≤N2
in leaves
T … do nothing
TS … swap elements, O(1)
internal nodes … O(1) time
time O(N2)✓
consider a level where the subtree fits in the memory
min i such that d=2iN≤B
it becomes equivalent to the cache-aware algorithm with tiles d=Θ(B)
so we get O(N2/B+1) I/O complexity again
under the cache-oblivious model (with the tall cache assumption)
it is optimal – each nondiagonal element has to be transferred
note: we must show that there exists a replacement strategy that leads to this I/O complexity
consider a strategy where we load the entire subproblem into the cache at level i
this strategy serves as an upper bound (in terms of cache misses) for the optimal strategy
Describe hashing with chains and analyze its complexity. Define c-universal and k-independent systems of hash functions and provide constructions of such systems. Give an example when k-independent system in needed and c-universality does not suffice.
hashing function h:U→[m]
universe U
m buckets … [m]={0,…,m−1}
n elements are already in the hash table
collision → we add the element to the end of the chain
density α=n/m … expected length of the chain (in some cases?)
using a fixed hashing function is problematic – there exists an adversarial sequence of inputs
solution: pick our hashing function uniformly randomly from a parametrized family of hashing functions
we don't have enough memory to store a totally random function → we need a parametrized family
theorem
let H be c-universal, x1,…,xn,y∈U are distinct
then Eh∈H[#i∣h(xi)=h(y)]≤mcn
proof (from linearity of expectation)
indicator variable Ai for each collision
Ai=1 iff h(xi)=h(y)
E[Ai]=0⋅Pr[h(xi)=h(y)]+1⋅Pr[h(xi)=h(y)]≤mc
E[∑i=0nAi]=∑i=0nE[Ai]≤mcn
complexity of hashing with chaining
unsuccessful find … O(n/m)
successful find … O(n′/m)
n′ … number of elements at the point in time when this element was inserted
insert, delete – similar analysis
unsuccessful insert = successful find
if n=Ω(m), we get constant expected time
what if we don't know n (how many elements we need to hash)
idea: stretching
if α=mn>1, then m′=2m, pick new h∈H, rehash
if α<1/4, then m′=2m, pick new h, rehash
so we keep α in the interval [41,1] (but we could choose a different interval)
we need to ensure that we have linearly many operations between reallocations
so after reallocation, we want to be somewhere in the middle between the two bounds
definition: c-universality
let H be a family of functions from U to [m]
H is c-universal for some c>0 if ∀x,y∈U,x=y we have Prh∈H[h(x)=h(y)]≤mc
H is universal if it is c-universal for some c>0
definition: k-independence
H is (k,c)-independent if ∀ distinct x1,…,xk∈U and ∀a1,…,ak∈[m] it holds that Prh∈H[h(x1)=a1∧⋯∧h(xk)=ak]≤mkc
where k is integer s.t. 1≤k≤∣U∣ and c>0 is real
it's k-independent if it's (k,c)-independent for some c
observations
for k≥2, (k,c)-independence ⟹(k−1,c)-independence
(2,c)-independence ⟹c-universality
(1,c)-independence /⟹c-universality
consider a family of constant hashing functions
k-independence is necessary for linear probing and cuckoo hashing
2-universal example
ha,b(x)=(ax+bmodp)modm
L={ha,b∣a,b∈Zp}
theorem: L is 2-universal ∀ prime p≥m
proof
x=y fixed
r=(ax+b)modp
s=(ay+b)modp
claim: (a,b)↦(r,s) is a bijection
for any pair (r,s) there is exactly one choice of (a,b)
because the system of equations is regular (thanks to x=y)
so if I'm picking a,b uniformly randomly, then r,s is also chosen uniformly randomly
Pr[r=smodm]= # bad pairs / # all pairs ≤p2p⋅⌈p/m⌉
p/m … “how many times m fits inside p”
⌈p/m⌉≤mp+m≤m2p
so p2p⋅⌈p/m⌉≤pm2p=m2
k-independent example
ht(x)=∑i=0k−1tiximodp
ht:Zp→Zp,t∈Zpk
evaluation of the polynomial in Zp
Pk={ht∣t∈Zpk}
theorem: Pk is (k,1)-independent ∀p prime, k≥1
proof
we have distinct x1,…,xk
we have a1,…,ak
Pr[∀i:ht(xi)=ai]=pk1
by Lagrange interpolation theorem, there is exactly one polynomial of degree k−1 that intersects k points
example
for k=2, we get ha,b=ax+bmodp
we can see that P2 is (2,1)-independent (and thus 2-universal even without modm)
Describe and analyze hashing with linear probing (under fully random hashing function). Compare this hashing with other data structures, in particular based on other hashings.
probing
saves space (each bucket contains at most one item)
probe sequence: h(x,0),h(x,1),…
Insert follows the probe sequence until it finds an empty cell
linear probing
h(x,i)=h(x)+imodm
we store the items with the same h(x) right next to each other
alternative: double hashing
h(x,i)=h(x)+ig(x)modm
properties
it's cache-friendly – the correct bucket is likely to be loaded into the cache (this is often not the case with double hashing or quadratic probing)
it stores the data directly inside the hash table (unlike hashing with chains)
it degrades as the table becomes full (note m≥3n in the proof) – long clusters get longer
operation Delete generates tombstones (we cannot remove elements in the middle of clusters → we mark the buckets as deleted and later rehash the table if there's too many tombstones)
theorem: if m≥(1+ε)n, then the expected number of probes in Find is…
O(1/ε2) for a totally random hashing function
or tabulation hashing (even though it's only 3-independent!)
O(1/ε13/6) for any 5-independent family
Ω(logn) for some 4-independent family
Ω(n) for some 2-independent family
theorem
let m≥3n s.t. m is a power of 2
(we choose ε=2)
h:U→[m] totally random
x∈U
then the expected number of probes in Find(x) is O(1)
proof
we use Chernoff bound (for the right tail)
let X=X1+⋯+Xk s.t. Xi are binary and independent
then Pr[X>cμ]≤(ccec−1)μ
where μ=E[X] and c>1
observation: if we fix c and increase k, the probability drops exponentially
we also use the observation, that if we randomly generate a value of variable X, then E[X]≤∑i(maxIi⋅Pr[X∈Ii])
Ii are intervals where the value can fall
so we use the maximum from each interval and the probability that the value falls in that interval
aim: show that the expected length of a run R is constant
we will consider blocks of size 2t that together tile the entire table of size m
definition: block B of size 2t is critical if more than 32⋅2t items are hashed to B
it does not matter where the items are stored! we only care about the items being hashed to that block
lemma L1
let B be block of size 2t
then the probability that B is critical is ≤(4e)32t
proof
we consider indicators Xi indicating if h(xi)∈B
X=∑Xi … number of items hashed to B
μ=∑E[Xi]=n⋅m2t
as we consider a totally random function
μ≤32t
as m≥3n
Pr[B critical]≤Pr[X>2μ]≤(4e)32t
from Chernoff
to simplify the reasoning, we can consider m=3n
we can set q=(e/4)1/3
then Pr≤q2t
lemma L2
let R be run, ∣R∣≥2ℓ+2,B0,…,B3 first 4 blocks of size 2ℓ intersecting R
then at least one of B is critical
proof
consider interval L=R∩(B0∪⋯∪B3)
1+3⋅2ℓ≤ # items stored in L≤ # items hashed to L
we are considering a prefix of the run so every element stored in the blocks also has to be hashed there
suppose that none of the blocks is critical
there are at most 4⋅32⋅2ℓ items hashed to L
but this is less than 1+3⋅2ℓ → contradiction
lemma L3
consider run R containing h(x),∣R∣∈[2ℓ+2,2ℓ+3)
then at least one of 12 consecutive blocks of size 2ℓ is critical
we consider the 8 blocks before h(x), the block containing h(x), and the 3 blocks after h(x)
proof
4∣B∣≤∣R∣<8∣B∣
so R can begin up to 8 blocks before h(x)
then we use L2
we can apply union bound Pr[⋃iAi]≤∑iPr[Ai]
we get Pr[∣R∣∈[2ℓ+2,2ℓ+3)]≤12q2ℓ
we are using the fact at least one of the 12 blocks needs to be critical (to get a run this long)
now we use the intervals
I1=[0,3] and the remaining intervals
E[∣R∣]≤3⋅1+∑ℓ≥02ℓ+3Pr[∣R∣∈[2ℓ+2,2ℓ+3)]
≤3+12⋅8⋅∑ℓ≥1iqi
q<1 so the sum converges and we get O(1)
Define multi-dimensional range trees and describe the type of queries it supports. Analyze time and space complexity of their construction and complexity of range queries.
goal: poly-logarithmic range queries, not space optimal
O(logkn) time, O(nlogk−1n) space
idea: primary 1D search tree for x-coordinates of all points (x-tree) + secondary 1D search tree in each node v for y-coordinate of all points in int(v)×R (“band”)
observation: yi is only in secondary subtrees on the path to xi
→ O(nlogn) space if x-tree balanced
RangeQuery([a1,b1]×[a2,b2])
x-tree: RangeQuery([a1,b2]) → O(logn) points + bands (corresponding to subtrees)
points (xi,yi): if yi∈[a2,b2], report
bands: RangeQuery([a2,b2]) to the corresponding y-tree
analysis
O(logn) time in x-tree, O(logn) time in each y-tree, O(log2n) time in total
it the points are also reported, we need O(log2n+p) where p is the number of reported points
building the range tree
create two sorted lists of points: one sorted by the x-coordinate, the other sorted by the y-coordinate … O(nlogn)
recursion (O(n) subproblems, O(logn) levels)
find median in x-list … O(1) per subproblem
build y-tree … O(n) per level
select sub-lists … O(n) per level
total time: O(nlogn)
we assumed there are no shared coordinates
but we could attach a y-tree for points with the same x-coordinate
so we get 2 y-trees for the single x-tree node v
y-tree for band int(v)×R
y-tree for key(v)×R
k-D range trees (straightforward generalization)
primary x-tree (1D search tree) + secondary (k−1)-dimensional range tree for each band int(v)×Rk−1
space: every point in O(logn) secondary range trees → O(nlogk−1n) in total
O(logkn) time for RangeQuery (+p if enumeration)
building: k sorted arrays, O(nlogk−1n) time, constant is dependent on k
can be dynamized using lazily balanced trees
Define suffix arrays and LCP arrays. Describe and analyze algorithms for their construction. Give an example of their application.
the suffix array for α is a permutation S[0,…,n] s.t. ∀(0≤i<n):α[S[i]:]<α[S[i+1]:]
example for bananas
ε, 7
ananas, 1
anas, 3
as, 5
bananas, 0
nanas, 2
nas, 4
s, 6
the rank array R[0,…,n] is an inverse permutation to S
R[i] … number of suffixes that are lex. smaller than α[i:]
it's a mapping inverse to S (position → lexicographic order)
the LCP-array L[0,…,n−1]
L[i] … length of the longest common prefix of i-th suffix and (i+1)-th suffix (in the lex. order)
constructing R (we can easily construct S based on R) – using doubling
R[i] … lex. rank of substring starting at i
Rk[i] … number of positions j s.t. α[j:j+k]<α[i:i+k]
number of prefixes α[j] that are lexicographically smaller than α[i] if we compare only the first k characters
we can compute R2k[i] as the number of positions j s.t. Rk[j]<Rk[i] or Rk[j]=Rk[i]∧Rk[j+k]<Rk[i+k]
we want to sort triples (Rk[i],Rk[i+k],i)
we can use bucket sort in O(n)
Rn=R
we need O(nlogn) for the initial step (if the alphabet is large and we cannot rely on the bucket sort)
the sequence of characters at k+1 position is non-decreasing
the chars are the same (if their LCP is greater than k) or they differ (here we use the lex. order)
so the LCP of i,j has to be ≤k
observation
let's consider two suffixes that are lexicographically consecutive (adjacent): α,β
if we remove the first character from both of them, we get α′,β′
if LCP(α,β) is nonzero, then LCP(α′,β′)=LCP(α,β)−1
α′,β′ may not be lexicographically consecutive but LCP(α′,β′) is the minimum of that interval (according to the previous observation)
so to compute LCP of α′ and its successor, we can start with LCP(α,β)−1
S,R→L
Kasai's algorithm
iterate from the longest to the shortest suffix
index of the i-th suffix … R[i]
in every iteration
take the previous computed k, subtract one, clamp (so that it does not go below zero) → we get a new k
take the suffixes corresponding to indices R[i] and R[i]+1 (the lexicographic successor)
we need to compare their characters one by one
we can skip the first k characters
for every matching pair, we increment k by one
runs in O(n)
we can use k as potential
it's always non-negative and at most n
it always increases or decreases by one
there are at most n decreases → there are at most 2n increases
applications
histogram of all k-grams (might be useful for classifying a language or a species based on its DNA)
we can find it in linear time by going through LCP array
if the current LCP is below k, we reached a new k-gram
we need to check that S[i] is not too large (so that it truly is a k-gram – it should not be shorter than k)
longest repeating substring (redundancy measure)
find a maximum in the LCP array
longest common substring in α,β
we build a LCP array for α#β
Describe locks and atomic operations CAS and LL/SC. Describe and analyze a lock-free stack including the memory management. Explain the ABA problem and its solution.
lock (mutex)
where / how many
one per DS
one per item
locks bring some overhead
only works if there is not too much “structure” in the data structure
one per part
e.g. bucket in hash table (for hashing with chaining) + one global lock (for rehashing)
locks are blocking – the process needs to wait if the data structure is locked
problems: deadlock (ordering is not always available or possible to use), fairness, inverse priorities, fault-tolerance
HW assumptions (to support lock-free data structures)
atomic registers: read, write, (exchange), (test & set bit)
load linked (LL) / store conditional (SC)
not always available, only for limited number of registers
a linked load is a normal atomic read, but the processor remembers the address and keeps monitoring it for access by other processors
a later store conditional to the same address succeeds if the address was not written to by other processors (otherwise it reports failure)
compare & swap (CAS)
CAS(address, x, y)
atomically performs the following
load value from address
if value equals x, store y at address
return value
often available
lock-free stack
node: atomic pointer to next
global: atomic pointer to head
Push(n): (loop)
h ← head
n.next ← h
if CAS(head, h, n) == h
return
Pop: (loop)
h ← head
n ← h.next
if CAS(head, h, n) == h
return h
it does not work :(
for Pop, we are checking only the head – the next item might have been removed in the meantime
other process could have run this sequence of operations: Pop (removes A), Pop (removes B), Push(A)
ABA-problem
possible solutions
use LL/SC
double CAS (atomic) … only theoretical solution
if DCAS((head, h.next), (h, n), (n, n)) == (n, n)
wide CAS (atomic)
we can use pointer versioning
if WCAS((head, version), (h, ver), (n, ver+1)) == (h, ver)
theoretical problem: overflow (does not happen in practice)
restrictive memory management
another problem: memory management
if someone pops and deallocates head before we read h.next (after storing h), we may crash
solution: freelist
list of memory pieces that are ready to be deallocated
we deallocate only if we are sure that nobody uses them
how to be sure
synchronization points: we synchronize all processes at a point where they hold no pointers of their own (we can then free up all chunks from the freelist)
reference counting
hazard pointers – signal to the garbage collector that I am using the pointer
we store h in the hazard pointer
then we check if h still equals head (otherwise we need to continue into the next loop)
amortize scanning the freelist
scan only if freelist ≥2pr items
p … number of processes
r … number of hazard pointers per processor
this releases ≥pr
so scan can be accounted to the adding
Minor questions
Describe a flexible array with growing and shrinking. Analyze its amortized complexity.
let's consider a stack, it has operations append(x) and removelast
shrink to C/2 if n<C/2?
this is problematic, we would get linear amortized complexity
we should use n<C/4 instead
amortized analysis … average over operations in the worst-case scenario
operations
append – double the capacity if the array is full … costs O(C) in this case
removelast – halve the capacity if the array becomes shorter than C/4 … costs O(C)
we consider blocks of operations
block boundaries = growing/shrinking operation
“how many elements does the array contain at the beginning and at the end of the block?”
always C/2 at the beginning (after growing/shrinking operation, the array is half full)
if growing: C at the end
if shrinking: C/4 at the end
so there are at least C/4 operations in each block (to get from C/2 elements to C/4 or C elements)
we can account the O(C) reallocation to the Ω(C) operations
→ amortized O(1) for operation
“accounting method” – we account the cost to the operations
note that the buffer does not need to be exactly C/4 but has to be linearly dependent on C
Define the lazily balanced trees BB[α]. Analyze their amortized complexity. Give an example of their application.
notation
n … number of items stored in the tree
ℓ(v),r(v) … left/right child of v
s(v) … size = number of nodes in the subtree rooted at v (including v)
definition: BST is perfectly balanced if for every node v it holds that ∣s(ℓ(v))−s(r(v))∣≤1
the ratio is approximately 1 : 1
definition: BST is balanced if for every v and its every child c it holds that s(c)≤32s(v)
the ratio is between 1 : 2 and 2 : 1
lemma: any balanced BST has depth O(logn)
imagine the longest path from the root to a leaf
in each step from the root to the leaf, the size drops to at most 2/3 of the preceding size
size of the root … n
size of the leaf … 1
1≤(32)dn
d≤log2/3(1/n)=log3/2n=O(logn)
insert operation
find, add a leaf, update sizes (we keep track of the sizes s(v))
if not balanced, rebuild in the highest unbalanced node
observation: for n sorted items, build takes linear time
select the middle item as the root and split the rest in two subtrees
proceed recursively
Φ:=∑vφ(v)
φ(v):={∣s(ℓ(v))−s(r(v))∣0if at least 2otherwise
the clamping ensures that perfectly balanced tree has zero potential
cost of insert
no rebuild: A=R+ΔΦ=O(logn)+O(logn)=O(logn)
ΔΦ=O(logn) because Δφ≤2 for each visited node (usually Δφ=1 but there may be nodes with φ hopping from 0 to 2 due to the clamping)
rebuild at v
the invariant was broken for v and its child c
WLOG s(ℓ(v))>32s(v)⟹s(r(v))<31s(v)⟹φ(v)>31s(v)
after the rebuild, this contribution and all the contributions in the subtree become zero
mostly in functional programming (new versions of a tree can share some subtrees with the original tree)
Design operations Find, Insert, and Delete on a Splay tree. Analyze their amortized complexity. (It suffices to state the complexity of Splay operation without proof.)
operations
find(x) or insert(x) or delete(x)
as in an ordinary BST
when deleting a node with two children, we need to replace it by its successor (WLOG)
splay(lowest visited node)
analyzing operations
we need A≤R+ΔΦ
find does not change the potential
going from the root to a node at depth d has cost Θ(d)
splaying this node also costs Θ(d) and it amortizes to O(logn)
we can account the cost of the former part on the splay → find has total amortized complexity O(logn)
delete decreases the potential – that's fine 👍
A≤R
insert increases the potential
only for the nodes on the path v1,…,vt,newvt+1
we can apply telescopic cancellation again
ΔΦ=Φi(vt+1)+∑i=1t(Φi(vi)−Φi−1(vi))
s(vt+1)=1⟹Φi(vt+1)=0
si(vi)=si−1(vi)+1≤si−1(vi−1)
ΔΦ≤Φi(v1)−Φi−1(vt)
so ΔΦ is O(logn)
so all the operations are O(logn) amortized
State and prove the theorem on amortized complexity on Insert and Delete on (a,2a-1)-trees and (a,2a)-trees.
theorem: m inserts on empty (a,b)-tree do O(m) modifications
number of splits ≤ number of nodes in (a,b)-tree with m keys ≤m
observation: (a,2a−1)-tree cannot have O(1) amortized modifications for inserts/deletes
there exists a problematic sequence of operations which performs logn modifications for every operation (similar to the shrinking array)
example: tree where every node on a path from the root to the leaves contains 2 keys and every other node contains 1 key (if we consider a (2,3)-tree)
inserting a key on the path leads to logn modifications
if we then delete it, logn modifications need to be performed again
we end up with the same tree as before → we can repeat these two operations
theorem: any sequence of m inserts/deletes on empty (a,2a)-tree does O(m) modifications
proof
A=R+ΔΦ
goal: find Φ s.t. A=O(1) for add/remove/move and A≤0 for split/merge
Φ=∑v internalf(∣v∣)
∣v∣ … number of keys in v
we need to define the contribution f for every possible size of a node (including oversized and undersized nodes)
add/remove/move: ∣f(i+1)−f(i)∣≤c for some constant c
split: f(2a)≥f(a−1)+f(a)+c+1 (should be 3 but we write 1 → up to rescaling)
so Asplit≤0
merge: f(a−1)+f(a−2)≥f(2a−2)+c+1
so Amerge≤0
possible function
c=2
f(a−2)=2
f(a−1)=1
f(a)=0
…
f(2a−2)=0
f(2a−1)=2
f(2a)=4
to conclude
∑Ri=∑Ai−ΔΦ
∑Ai=O(m)
ΔΦ=≥0Φm−=0Φ0
so we get ∑Ri=O(m)
Analyze k-way Mergesort in the cache-aware model. Which is the optimum value of k?
standard Mergesort
we merge two “runs” (sorted arrays) of sizes n1,n2 into a single run of size n1+n2
O(nlogn) … O(logn) passes; O(n) operations in each one
one merge = three scans of the array
one scan has I/O complexity O(N/B+1)
K-way Mergesort
number of passes … O(logKN)=O(logKlogN)
we need a heap to keep minimum of K elements … O(logK)
time
one pass … O(NlogK)
total … O(NlogK⋅logKlogN)=O(NlogN)
I/O complexity
K+1 scans per one run
one pass … O(N/B+1)
total … O(N/B⋅logKlogN+1)
how many memory blocks we need?
K+1 blocks for merge + K−1 for heap
K−1 is more than enough (even a logarithm of K would suffice but we want to get a nice sum)
so we need M≥2KB
optimal K=⌊M/2B⌋
which leads to I/O complexity O(N/B⋅logM/BlogN+1)
in cache-aware model
State and prove the Sleator-Tarjan theorem on competivity of LRU.
optimal strategy: “evict the cache line needed the latest” (it's an offline strategy)
cost of strategy S … TS = number of cache misses
w.r.t. sequence of requests for particular blocks a1,…,an
we assume fully associative cache
no assumption on the initial contents of the cache
strategy is k-competitive for a constant k≥1 if (∀C)(∀a1,…,an):TSTR≤k⋅TOPT
C … cache size
TOPT … cost of the optimal strategy
LRU (least-recently used) strategy: “evict the cache line unused the longest time”
theorem: ∀C and ∀ε>0 there is an access sequence s.t. TLRU≥C⋅(1−ε)⋅TOPT
in other words, LRU is not k-competitive for any k (if we want the k to be independent of C)
consider an access sequence 1,…,C,C+1,1,…,C,C+1,… repeated
a polynomial of degree d−1 has at most d−1 roots (which is ≤d)
R∘L is (2,25)-independent if p≥4dm
to hash strings of length at most L, we set d:=L and we pad the strings by zeros (assuming that they don't occur elsewhere) → this part of the string then gets “skipped”
lemma
let F be a c-universal family of f:U→[r],r≥m
G is (2,d)-independent family of g:[r]→[m]
then H=F∘G={f∘g∣f∈F,g∈G} is (2,c′)-independent where c′=(rcm+1)d
Describe the cuckoo hashing and state the theorem on its complexity (without proof).
we have two hash functions f,g and two sets of m buckets Tf,Tg
we hash the inserted element and if the bucket is already full, we take the element present there and insert it it using the other function
we continue until we have correctly placed all the elements
when we meet the timeout (we keep cycling back and forth between the tables for long enough), we rehash
timeout ~ logn
invariant: ∀x∈X is in Tf(f(x)) or Tg(g(x))⟹Find, Delete in O(1) (worst case)
theorem
let m≥(2+ε)n,f,g from ⌈6logn⌉-independent family
then cuckoo hashing with timeout ⌈6logn⌉ has expected amortized Insert time O(1)
Describe hashing with linear probing and give overview of results on its complexity.
probing
saves space (each bucket contains at most one item)
probe sequence: h(x,0),h(x,1),…
Insert follows the probe sequence until it finds an empty cell
linear probing
h(x,i)=h(x)+imodm
we store the items with the same h(x) right next to each other
alternative: double hashing
h(x,i)=h(x)+ig(x)modm
properties
it's cache-friendly – the correct bucket is likely to be loaded into the cache (this is often not the case with double hashing or quadratic probing)
it stores the data directly inside the hash table (unlike hashing with chains)
it degrades as the table becomes full (note m≥3n in the proof) – long clusters get longer
operation Delete generates tombstones (we cannot remove elements in the middle of clusters → we mark the buckets as deleted and later rehash the table if there's too many tombstones)
theorem: if m≥(1+ε)n, then the expected number of probes in Find is…
O(1/ε2) for a totally random hashing function
or tabulation hashing (even though it's only 3-independent!)
O(1/ε13/6) for any 5-independent family
Ω(logn) for some 4-independent family
Ω(n) for some 2-independent family
Describe and analyze the Bloom filter. Give an example of its application.
filter for set representation with zero false negatives
consider a database and a query Find(x)
you first ask the Bloom filter – if the filter responds “no”, we don't need to ask the database
of course, Insert(x) has to change both the filter and the database
the larger memory we have available, the smaller the probability of false positives
simple Bloom filter
bit array [m], h∈H is c-universal
probability of false positivity
consider that there are already distinct x1,…,xn present in the filter
consider y distinct
Pr[y is FP]=Pr[∃i:h(y)=h(xi)]≤∑i=1nPrh[h(xi)=h(y)]≤mcn
let's consider that c=1 and that we want Pr[FP]≤ε
we set m:=⌈n/ε⌉ (number of required bits)
so if there are 106 items and we want ε=0.01, we need 100 Mb to store the filter
k-way Bloom filter
we take a conjunction of k Bloom filters
we need h1,…,hk to be independently chosen functions from a c-universal family
Pr[y is FP]≤∏i=1kmcn=2k1≤ε
if c=1 and m=2n
k:=⌈log1/ε⌉
M=mk=2n⌈log1/ε⌉
now for n=106 and ε=0.01, we need k=7 and M=14 Mb
for ε=0.001, we need k=10 and M=20 Mb
single-band Bloom filter
we set k bits using k hash functions that are chosen independently – in the same table
if the hash functions are totally random, we may get results similar to the k-way filter
how to delete elements?
if we set the bit to zero, we delete all the elements with this hash value
instead of bits, we could use counters → counting filter
b bits → at most t:=2b−1 elements can be hashed to the same position at the filter
Insert/Delete just increment/decrement the value in the filter
Find returns yes if the filter is not zero
problem: when we reach t, the counter is “stuck” (cannot be increased, cannot be decreased), so there's a new kind of false positives
for a fixed bucket i and totally random h, we have Pr[B[i]≥t]≤(tn)(m1)t≤(mtne)t
we use (tn)≤(tne)t
if we set m=ln2n≐1.44n and we use b=4, then (mtne)t=(teln2)t≤3.06⋅10−14
if we have m=109 counters, the probability that any is stuck is ≤3.06⋅10−5
Show how to perform 1-dimensional range queries on binary search trees.
for each node v we define the interval of v … int(v)
contains all real numbers whose search visits v
keys in the internal nodes cut the real line to parts – these intervals correspond to the external nodes
they are all open intervals
algorithm RangeQuery(v,Q)
if v is external, return
if int(v)⊆Q, report the whole subtree rooted at v and return
if key(v)∈Q, report key(v)
Qℓ←Q∩int(ℓ(v)),Qr←Q∩int(r(v))
if Qℓ=∅, RangeQuery(ℓ(v),Qℓ)
if Qr=∅, RangeQuery(r(v),Qr)
RangeQuery visits O(logn) nodes and subtrees if tree is balanced (has logarithmic depth)
Q=(α,β)
a … node where Find(α) stops
b … node where Find(β) stops
p … lowest common predecessor of a,b
visited nodes: on the paths to a,b
visited subtrees: right subtrees on the path p→a and left subtrees on the path p→b
Define k-d trees and show that they require Ω(n) time per 2-d range query.
keys = points in Rk
on level i split by i-th coordinate modk
to build a static k-d tree, we use a median value at each level as a root
median of m items in O(m) time
O(n) per level, O(logn) levels, O(nlogn) time, O(n) space
RangeQuery same as in 1D
alternate coordinates
int(v) is an open region (product of open intervals)
recurse until all coordinates fall into target intervals
lemma: RangeQuery in 2D search tree has Ω(n) worst-case time complexity
consider points {(i,i)∣1≤i≤n}
n=2t−1
t … depth
RangeQuery(root, {0}×R)
x-coordinate → go to the left
y-coordinate → go both left and right
we end up visiting 2t/2≈n nodes (we need to recurse into both subtrees at every other level)
note: it's Θ(n)
we assumed there are no shared coordinates
but we could attach a (k−1)-d subtree to each node where some points share a coordinate
Show how to use suffix array and LCP array for finding the longest common substring of two strings.
we build a suffix array and LCP array for the string α#β
# … separator that occurs neither in α nor in β
we find the maximum L[k] such that k-th (lex.) substring comes from α and k+1-th (lex.) substring comes from β or vice versa
we can use S to find out if the substring belongs to α or β (we can compare it to the position of #)
Describe parallel (a,b)-trees with the use of locks.
standard (a,b) tree: a−1≤ number of keys ≤b−1
top-down splitting and merging
we need b≥2a
we go top down and maintain the invariant that the current node can accept one key (pre-emptive splitting)
→ we don't need to go back
similarly, we maintain the invariant that the current node has more keys than a−1
we get a−1< number of keys <b−1
we only need to have two locks at the same time – the current node and its parent
for deleting, we may need to lock siblings
we lock nodes in left-to-right order
note that if we delete a key from a node, we need to lock its subtree (we use the successor as a replacement) – this problem does not occur if we use B+ trees (store data in leaves only)