Design Skiplist
Time O(logn), on average · Space O(n) · Official statement on LeetCode
Solutions
// Time: O(logn) on average for each operation
// Space: O(n)
// see proof in references:
// 1. https://kunigami.blog/2012/09/25/skip-lists-in-python/
// 2. https://opendatastructures.org/ods-cpp/4_4_Analysis_Skiplists.html
// 3. https://brilliant.org/wiki/skip-lists/
class Skiplist {
private:
class SkipNode {
public:
SkipNode() : SkipNode(0, -1) {
}
SkipNode(int level, int num)
: num(num)
, nexts(level) {
}
int num;
vector<SkipNode *> nexts;
};
public:
Skiplist()
: gen_((random_device())())
, len_(0)
, head_(new SkipNode()) {
}
~Skiplist() {
if (head_->nexts.empty()) {
return;
}
auto curr = head_->nexts[0];
while (curr) {
auto next = curr->nexts[0];
delete curr;
curr = next;
}
}
bool search(int target) const {
return find(target, find_prev_nodes(target)) != nullptr;
}
void add(int num) {
auto node = new SkipNode(random_level(), num);
if (head_->nexts.size() < node->nexts.size()) {
head_->nexts.resize(node->nexts.size());
}
auto prevs = find_prev_nodes(num);
for (int i = 0; i < node->nexts.size(); ++i) {
node->nexts[i] = prevs[i]->nexts[i];
prevs[i]->nexts[i] = node;
}
++len_;
}
bool erase(int num) {
auto prevs = find_prev_nodes(num);
auto curr = find(num, prevs);
if (!curr) {
return false;
}
--len_;
for (int i = curr->nexts.size() - 1; i >= 0; --i) {
prevs[i]->nexts[i] = curr->nexts[i];
if (!head_->nexts[i]) {
head_->nexts.pop_back();
}
}
delete curr;
return true;
}
int size() const {
return len_;
}
private:
SkipNode *find(int num, const vector<SkipNode *>& prevs) const {
if (!prevs.empty()) {
auto candidate = prevs[0]->nexts[0];
if (candidate && candidate->num == num) {
return candidate;
}
}
return nullptr;
}
vector<SkipNode *> find_prev_nodes(int num) const {
vector<SkipNode *> prevs(head_->nexts.size());
auto curr = head_;
for (int i = head_->nexts.size() - 1; i >= 0; --i) {
while (curr->nexts[i] && curr->nexts[i]->num < num) {
curr = curr->nexts[i];
}
prevs[i] = curr;
}
return prevs;
}
int random_level() {
static const int P_NUMERATOR = 1;
static const int P_DENOMINATOR = 2; // P = 1/4 in redis implementation
static const int MAX_LEVEL = 32; // enough for 2^32 elements
int level = 1;
while (uniform_int_distribution<int>{1, P_DENOMINATOR}(gen_) <= P_NUMERATOR &&
level < MAX_LEVEL) {
++level;
}
return level;
}
void print_list() const {
for (int i = head_->nexts.size() - 1; i >= 0; --i) {
auto curr = head_->nexts[i];
cout << curr->num;
curr = curr->nexts[i];
while (curr) {
cout << "->" << curr->num;
curr = curr->nexts[i];
}
cout << endl;
}
}
default_random_engine gen_;
int len_;
SkipNode *head_;
};
// Time: O(logn) on average for each operation
// Space: O(n)
// smart pointer version (a little bit slower)
class Skiplist2 {
private:
class SkipNode {
public:
SkipNode() : SkipNode(0, -1) {
}
SkipNode(int level, int num)
: num(num)
, nexts(level) {
}
int num;
vector<shared_ptr<SkipNode>> nexts;
};
public:
Skiplist2()
: gen_((random_device())())
, len_(0)
, head_(make_shared<SkipNode>()) {
}
bool search(int target) const {
return find(target, find_prev_nodes(target)) != nullptr;
}
void add(int num) {
auto node = make_shared<SkipNode>(random_level(), num);
if (head_->nexts.size() < node->nexts.size()) {
head_->nexts.resize(node->nexts.size());
}
auto prevs = find_prev_nodes(num);
for (int i = 0; i < node->nexts.size(); ++i) {
node->nexts[i] = prevs[i]->nexts[i];
prevs[i]->nexts[i] = node;
}
++len_;
}
bool erase(int num) {
auto prevs = find_prev_nodes(num);
auto curr = find(num, prevs);
if (!curr) {
return false;
}
--len_;
for (int i = curr->nexts.size() - 1; i >= 0; --i) {
prevs[i]->nexts[i] = curr->nexts[i];
if (!head_->nexts[i]) {
head_->nexts.pop_back();
}
}
return true;
}
int size() const {
return len_;
}
private:
shared_ptr<SkipNode> find(int num, const vector<shared_ptr<SkipNode>>& prevs) const {
if (!prevs.empty()) {
auto candidate = prevs[0]->nexts[0];
if (candidate && candidate->num == num) {
return candidate;
}
}
return nullptr;
}
vector<shared_ptr<SkipNode>> find_prev_nodes(int num) const {
vector<shared_ptr<SkipNode>> prevs(head_->nexts.size());
auto curr = head_;
for (int i = head_->nexts.size() - 1; i >= 0; --i) {
while (curr->nexts[i] && curr->nexts[i]->num < num) {
curr = curr->nexts[i];
}
prevs[i] = curr;
}
return prevs;
}
int random_level() {
static const int P_NUMERATOR = 1;
static const int P_DENOMINATOR = 2; // P = 1/4 in redis implementation
static const int MAX_LEVEL = 32; // enough for 2^32 elements
int level = 1;
while (uniform_int_distribution<int>{1, P_DENOMINATOR}(gen_) <= P_NUMERATOR &&
level < MAX_LEVEL) {
++level;
}
return level;
}
void print_list() const {
for (int i = head_->nexts.size() - 1; i >= 0; --i) {
auto curr = head_->nexts[i];
cout << curr->num;
curr = curr->nexts[i];
while (curr) {
cout << "->" << curr->num;
curr = curr->nexts[i];
}
cout << endl;
}
}
default_random_engine gen_;
int len_;
shared_ptr<SkipNode> head_;
};
Beginner Explanation
What is Design Skiplist?
Design Skiplist (LeetCode #1206) is a Hard problem that primarily trains design.
How to think about it
- Restate the goal in your own words before coding.
- Work a tiny example by hand so the invariant becomes obvious.
- Identify the pattern — this problem aligns with general problem-solving.
- Only then translate the idea into code.
Why this problem matters
Hard problems force you to combine patterns and prove complexity carefully — interview gold.
AlgoForge explanations are original teaching notes. Always open the official problem statement on LeetCode for constraints and examples.
Interview Walkthrough
Interview approach for Design Skiplist
Opening (30–60 seconds)
- Clarify inputs/outputs and edge cases (empty input, single element, duplicates, overflow).
- State a brute force so the interviewer knows you can solve it naively.
- Propose the optimal direction tied to general problem-solving.
Core solution narrative
- Define the state you track (pointers, DP cell, set membership, stack top, etc.).
- Explain the transition when you process the next element.
- Call out time (O(logn), on average) and space (O(n)) before coding.
- Code cleanly; narrate variable names.
What interviewers listen for
- Correctness on edge cases
- Complexity honesty
- Ability to discuss trade-offs (e.g., hash map space vs. sort + two pointers)
Follow-up questions they may ask
- Can you solve it with less memory?
- What if the input stream is infinite / doesn't fit in RAM?
- How would tests look for adversarial inputs?
Optimized Approach
Optimized solution notes
The reference solutions on AlgoForge target O(logn), on average time and O(n) space.
Pattern focus: general problem-solving
Use the pattern as a checklist:
- Identify the dominant pattern and stick to one clear invariant
Multiple methods appear in the source solutions — compare them and explain when each is preferable.
Implementation tips
- Prefer readable names over micro-optimizations in interviews.
- Extract helpers only when they clarify (e.g., expand-around-center, DFS visit).
- After AC-level logic, re-scan for off-by-one and null checks.
Complexity Analysis
Complexity
| Measure | Bound |
|---|---|
| Time | O(logn), on average |
| Space | O(n) |
How to justify this in an interview
- Time: count loops, map/set operations, and recursive branching; state average vs worst case if relevant.
- Space: include hash maps, recursion stack, and output allocation when the problem asks for it.
If your implementation differs from the reference, re-derive big-O from your code — never memorize a complexity you cannot defend.
Common Mistakes
Common mistakes on Design Skiplist
- Skipping edge cases — empty collections, single-element inputs, max constraints.
- Wrong invariant for general problem-solving — updating state too early or too late.
- Mutating input unexpectedly when the problem forbids it.
- Off-by-one in windows, ranges, or binary search bounds.
- Ignoring overflow / precision for integer arithmetic problems.
- Overengineering — jumping to an advanced structure when a simpler approach works.
Alternative Approaches
Alternatives
The source file includes more than one method. Compare:
- Primary optimized path — best complexity for typical interviews.
- Secondary approach — often brute force, sorting-based, or space-optimized variant.
Practice articulating when you would pick each (constraints, readability, follow-ups).
Edge Cases
Edge cases checklist
- Minimum input size
- Maximum input size / time limits
- Duplicates and already-sorted input
- Negative numbers / zeros (if applicable)
- Disconnected structures (graphs/trees)
- Single path vs branching recursion depth
Pattern Recognition
Spotting this pattern
Signal phrases that point to general problem-solving:
- Sorted input or ability to sort without changing the answer class
- Need for contiguous subarray / substring → consider sliding window
- Need for O(1) membership → hash set/map
- Optimal substructure + overlapping subproblems → DP
- Connectivity / components → graph DFS/BFS or Union-Find
Primary topics: design.
Follow-up Interview Questions
Follow-ups
- How does the solution change if the input is a stream?
- Can you solve it in-place?
- What if duplicates must be handled differently?
- How would you parallelize the approach?
- Design tests that would break a buggy implementation.
Practice Recommendations
What to practice next
- Re-solve Design Skiplist in a second language (cpp, python).
- Drill 3–5 more problems tagged design.
- Teach the solution out loud in under 5 minutes.
- Add this problem to your revision calendar in 3 days and 14 days.
Visualization
Study checklist
- Read the official problem statement on LeetCode
- Solve on paper / whiteboard first
- Implement the general problem-solving approach
- Verify edge cases from the checklist
- State time and space complexity aloud
- Compare with the AlgoForge reference solution
- Schedule a revision session
Revision notes
Design Skiplist (#1206) — Hard. Pattern: general problem-solving. Complexity: O(logn), on average time / O(n) space. Re-derive the invariant before coding.
FAQs
What is the time complexity of Design Skiplist?+
The reference solutions aim for O(logn), on average time and O(n) space. Always re-derive complexity from the code you write in the interview.
What pattern does Design Skiplist use?+
It primarily maps to general problem-solving, within the broader topic of design.
Is Design Skiplist good for interviews?+
Yes — as a Hard problem it is a solid practice target. Pair it with related problems in the same pattern family for spaced repetition.
Where can I read the official statement?+
Open the official LeetCode page for constraints and examples: https://leetcode.com/problems/design-skiplist/