Bird
Raised Fist0
Interview Prepcustom-data-structureshardAmazonGoogle

Design Skiplist

Choose your preparation mode4 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
📋
Problem

Imagine you need a data structure that supports fast search, insertion, and deletion, but you want to avoid the complexity of balanced trees. Skiplist offers a probabilistic alternative that is easier to implement and efficient.

Design a Skiplist without using any built-in libraries. A Skiplist is a data structure that allows fast search, insertion, and deletion operations in average O(log n) time. Implement the Skiplist class with the following methods: - boolean search(int target): Returns true if the target exists in the Skiplist, otherwise false. - void add(int num): Inserts the number into the Skiplist. - boolean erase(int num): Removes one occurrence of num from the Skiplist if present. Returns true if the number existed and was erased, false otherwise. The Skiplist should use multiple levels of linked lists with probabilistic promotion of nodes to higher levels.

1 ≤ number of operations ≤ 10^5-2 * 10^4 ≤ num, target ≤ 2 * 10^4At most 5 levels in the Skiplist (typical max level)
Edge cases: search in an empty Skiplist → falseerase a number not present → falseadd duplicate numbers and erase one occurrence → true
</>
IDE
class Skiplist: def __init__(self): pass def search(self, target: int) -> bool: pass def add(self, num: int) -> None: pass def erase(self, num: int) -> bool: passpublic class Skiplist { public Skiplist() {} public boolean search(int target) { return false; } public void add(int num) {} public boolean erase(int num) { return false; } }class Skiplist { public: Skiplist() {} bool search(int target) { return false; } void add(int num) {} bool erase(int num) { return false; } };class Skiplist { constructor() {} search(target) { return false; } add(num) {} erase(num) { return false; } }
class Skiplist:
    def __init__(self):
        # Write your solution here
        pass
    def search(self, target: int) -> bool:
        pass
    def add(self, num: int) -> None:
        pass
    def erase(self, num: int) -> bool:
        pass
public class Skiplist {
    public Skiplist() {
        // Write your solution here
    }
    public boolean search(int target) {
        return false;
    }
    public void add(int num) {
        // Write your solution here
    }
    public boolean erase(int num) {
        return false;
    }
}
class Skiplist {
public:
    Skiplist() {
        // Write your solution here
    }
    bool search(int target) {
        return false;
    }
    void add(int num) {
        // Write your solution here
    }
    bool erase(int num) {
        return false;
    }
};
class Skiplist {
    constructor() {
        // Write your solution here
    }
    search(target) {
        return false;
    }
    add(num) {
        // Write your solution here
    }
    erase(num) {
        return false;
    }
}
Coming soon
0/9
Common Bugs to Avoid
Wrong: [true, false, true, false]Erase method always returns true regardless of element presence.Add a check before erase to confirm element exists; return false if not found. Detected by t1_01.
Wrong: [true, true, false, false]Search method returns false for existing elements due to incorrect traversal or comparison.Ensure search traverses nodes correctly and compares target with node values properly. Detected by t1_02.
Wrong: [true, true, true, false]Add method inserts nodes at the end instead of sorted position, breaking search and erase.Traverse to correct sorted position before inserting new node. Detected by t3_01.
Wrong: [false, false]Search and erase do not handle empty Skiplist correctly, possibly causing errors or wrong returns.Check for empty head.next before traversal and return false if empty. Detected by t2_01.
Wrong: TLE or timeoutImplementation uses single-level linked list causing O(n) per operation, leading to timeout on large inputs.Implement multi-level Skiplist with probabilistic promotion to achieve O(log n) average time. Detected by t4_01.
Test Cases
t1_01basic
Input{"operations":["add","add","add","search","add","search","erase","erase","search"],"arguments":[[1],[2],[3],[0],[4],[1],[0],[1],[1]]}
Expected[false,true,false,false]

Initially, 1, 2, 3 are added. Searching 0 returns false. Adding 4, searching 1 returns true. Erasing 0 returns false since 0 not present. Erasing 1 returns true. Searching 1 now returns false.

t1_02basic
Input{"operations":["add","add","search","erase","search","add","search"],"arguments":[[5],[10],[5],[5],[5],[7],[7]]}
Expected[true,true,false,true]

Add 5 and 10. Search 5 returns true. Erase 5 returns true. Search 5 now returns false. Add 7 and search 7 returns true.

t2_01edge
Input{"operations":["search","erase"],"arguments":[[10],[10]]}
Expected[false,false]

Searching and erasing in an empty Skiplist should return false.

t2_02edge
Input{"operations":["add","search","erase","search"],"arguments":[[-20000],[-20000],[-20000],[-20000]]}
Expected[true,true,true,false]

Add minimum allowed value -20000, search returns true, erase returns true, search after erase returns false.

t2_03edge
Input{"operations":["add","add","add","erase","search"],"arguments":[[100],[100],[100],[100],[100]]}
Expected[true,true,true,true,false]

Add duplicate 100 three times, erase once returns true, search returns true (two remain), erase twice more returns true, search returns false after all erased.

t3_01corner
Input{"operations":["add","add","add","search","erase","search"],"arguments":[[1],[3],[2],[2],[2],[2]]}
Expected[true,true,true,true,true,false]

Add 1,3,2 in unsorted order. Search 2 returns true. Erase 2 returns true. Search 2 returns false after erase.

t3_02corner
Input{"operations":["add","add","add","add","search","erase","search"],"arguments":[[5],[5],[5],[5],[5],[5],[5]]}
Expected[true,true,true,true,true,true,false]

Add 5 four times, search 5 returns true, erase 5 removes one occurrence returns true, search 5 still true (3 remain), erase 3 more times removes all, search 5 returns false.

t3_03corner
Input{"operations":["add","add","add","search","erase","search"],"arguments":[[1],[2],[3],[4],[4],[4]]}
Expected[true,true,true,false,false,false]

Add 1,2,3. Search 4 returns false. Erase 4 returns false. Search 4 returns false.

t4_01performance
Input{"operations":["add","add","add","add","add","add","add","add","add","add","search","search","search","search","search","search","search","search","search","search","erase","erase","erase","erase","erase","erase","erase","erase","erase","erase"],"arguments":[[10000],[9999],[9998],[9997],[9996],[9995],[9994],[9993],[9992],[9991],[10000],[9999],[9998],[9997],[9996],[9995],[9994],[9993],[9992],[9991],[10000],[9999],[9998],[9997],[9996],[9995],[9994],[9993],[9992],[9991]]}
⏱ Performance - must finish in 2000ms

n=30 operations with large values near upper bound. O(log n) average complexity expected to complete within 2 seconds.

Practice

(1/5)
1. You are given a doubly linked list where some nodes have a child pointer to another doubly linked list. The child lists can themselves have children, and so on. Which approach best guarantees an in-place flattening of this multilevel list into a single-level doubly linked list without using extra space?
easy
A. Iteratively traverse the list, and whenever a node has a child, splice the child list in place by connecting the child's tail to the node's next, updating pointers accordingly.
B. Use a recursive depth-first traversal that returns the tail of each flattened child list to connect nodes.
C. Apply a greedy approach that always appends child lists at the end of the main list after full traversal.
D. Use dynamic programming to store intermediate flattened sublists and merge them bottom-up.

Solution

  1. Step 1: Understand the problem constraints

    The problem requires flattening a multilevel doubly linked list in-place without extra space.
  2. Step 2: Identify the approach that guarantees in-place flattening without extra space

    Using a recursive depth-first traversal that returns the tail of each flattened child list allows splicing child lists correctly and efficiently without extra data structures.
  3. Step 3: Evaluate other options

    Iterative splicing (Iteratively traverse the list, and whenever a node has a child, splice the child list in place by connecting the child's tail to the node's next, updating pointers accordingly.) can cause repeated tail searches leading to O(n^2) time and is less straightforward. Greedy appending (Apply a greedy approach that always appends child lists at the end of the main list after full traversal.) fails to maintain order. Dynamic programming (Use dynamic programming to store intermediate flattened sublists and merge them bottom-up.) is not applicable.
  4. Final Answer:

    Option B -> Option B
  5. Quick Check:

    Recursive DFS approach is standard and efficient for in-place flattening [OK]
Hint: Recursive DFS returns tail to connect lists efficiently [OK]
Common Mistakes:
  • Assuming iterative splicing is always best despite complexity
  • Appending child lists only at the end
  • Using DP which is not applicable here
2. Examine the following buggy code snippet from the AllOne data structure. Which line contains the subtle bug that causes keys to remain in multiple buckets after incrementing, leading to incorrect max/min results?
def inc(self, key: str) -> None:
    if key not in self.key_to_bucket:
        if 1 not in self.count_to_bucket:
            new_bucket = Bucket(1)
            self._insert_bucket_after(new_bucket, self.head)
        self.count_to_bucket[1].keys.add(key)
        self.key_to_bucket[key] = self.count_to_bucket[1]
    else:
        curr_bucket = self.key_to_bucket[key]
        next_count = curr_bucket.count + 1
        if next_count not in self.count_to_bucket:
            new_bucket = Bucket(next_count)
            self._insert_bucket_after(new_bucket, curr_bucket)
        self.count_to_bucket[next_count].keys.add(key)
        self.key_to_bucket[key] = self.count_to_bucket[next_count]
        # BUG: Missing removal of key from old bucket
        # curr_bucket.keys.remove(key)
        if len(curr_bucket.keys) == 0:
            self._remove_bucket(curr_bucket)
medium
A. Line removing key from old bucket's keys set (commented out)
B. Line updating key_to_bucket mapping
C. Line removing empty bucket if keys set is empty
D. Line adding key to new bucket's keys set

Solution

  1. Step 1: Identify key movement between buckets

    When incrementing, key must be removed from old bucket's keys set.
  2. Step 2: Locate missing removal

    The commented out line that removes the key from old bucket is missing, causing key to appear in multiple buckets.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Without removal, max/min queries become incorrect due to duplicate keys [OK]
Hint: Key must be removed from old bucket after increment [OK]
Common Mistakes:
  • Forgetting to remove key from old bucket after increment/decrement
3. What is the time complexity of performing n operations (push, pop, increment) on the optimal CustomStack implementation that uses a lazy increment array?
medium
A. O(n * k), where k is the increment size
B. O(n + k), where k is the maximum increment size
C. O(n log n), due to increment propagation
D. O(n), total for all operations combined

Solution

  1. Step 1: Analyze push and pop operations

    Each push and pop is O(1) amortized, as they add or remove one element.
  2. Step 2: Analyze increment operation

    Increment only updates inc array at one index, O(1) per call. Propagation happens lazily during pop, each element's increment propagated once.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    All n operations combined run in O(n) total time [OK]
Hint: Lazy increments make all operations O(1) amortized, total O(n) [OK]
Common Mistakes:
  • Assuming increment is O(k) per call
  • Confusing total time with per-operation time
4. Suppose the data stream allows duplicate numbers to be removed after insertion, requiring support for removeNum(num). Which modification to the balanced two heaps approach is necessary to maintain correct median retrieval?
hard
A. Use a balanced binary search tree instead of heaps to allow efficient removals.
B. Ignore removals and only add numbers, as removals break the heap structure.
C. Rebuild both heaps from scratch after every removal to maintain heap properties.
D. Add a delayed deletion map to mark elements for lazy removal and prune heaps during addNum and findMedian.

Solution

  1. Step 1: Understand removal challenges

    Heaps do not support efficient arbitrary removals, so direct removal breaks heap properties.
  2. Step 2: Use lazy deletion

    Maintain a delayed deletion map to mark elements for removal and prune them lazily during heap operations, preserving heap structure and efficiency.
  3. Step 3: Avoid costly rebuilds

    Rebuilding heaps on every removal is inefficient; ignoring removals is incorrect.
  4. Final Answer:

    Option D -> Option D
  5. Quick Check:

    Lazy deletion with pruning maintains correctness and efficiency [OK]
Hint: Lazy deletion map enables efficient removals in heaps [OK]
Common Mistakes:
  • Rebuilding heaps on every removal
  • Ignoring removals breaks correctness
5. Suppose you want to extend the LRU Cache to support a "reuse" operation that marks an existing key as recently used without retrieving its value. Which modification to the optimal LRU Cache implementation is correct to support this efficiently?
hard
A. On reuse, remove the key from the cache and re-insert it with the same value.
B. Do nothing; reuse is equivalent to get, so no code change is needed.
C. Use a queue instead of a doubly linked list to track usage order for easier reuse.
D. Add a new method that calls _remove and _add on the node corresponding to the key.

Solution

  1. Step 1: Understand reuse operation

    Reuse means marking a key as recently used without returning its value, so usage order must be updated.
  2. Step 2: Efficiently update usage order

    Calling _remove and _add on the node moves it to the front in O(1) time, correctly updating usage.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Reuse requires usage update without value retrieval, done by repositioning node [OK]
Hint: Reuse updates usage order like get but without returning value [OK]
Common Mistakes:
  • Re-inserting wastes time
  • Queue can't reorder efficiently
  • Assuming reuse needs no code change