Introdução
A Skip List é uma estrutura de dados probabilística inventada por William Pugh em 1990. Ela oferece busca, inserção e remoção em O(log n) esperado — a mesma complexidade de árvores balanceadas como AVL ou Rubro-Negra — mas com uma implementação significativamente mais simples.
A ideia genial da Skip List é adicionar múltiplos níveis de ponteiros a uma lista ligada ordenada. Os níveis superiores funcionam como “atalhos” que permitem pular grandes porções da lista durante a busca, de forma análoga a um índice de livro. O balanceamento não é determinístico (como em árvores AVL): é feito por meio de lançamento de moeda (probabilidade), o que simplifica enormemente a lógica de inserção.
O Redis, um dos bancos de dados em memória mais populares do mundo, utiliza Skip Lists internamente para implementar seus sorted sets — e a escolha foi explicitamente motivada pela simplicidade de implementação em comparação com árvores balanceadas.
Nesta página, construiremos uma Skip List completa em Rust, entenderemos o papel da aleatoriedade no balanceamento e veremos um exemplo prático de armazenamento chave-valor ordenado em memória.
Conceito e Teoria
A Intuição
Imagine uma lista ligada ordenada. Para buscar um elemento, precisamos percorrer todos os nós — O(n). E se adicionássemos uma “via expressa” que conecta apenas alguns nós?
Skip List — Evolução da Ideia:
================================
Lista ligada simples (O(n) para busca):
HEAD -> [3] -> [7] -> [12] -> [19] -> [25] -> [30] -> [42] -> NIL
Com um nível extra (a cada ~2 nós):
Nível 1: HEAD ---------> [12] ----------------> [30] ---------> NIL
Nível 0: HEAD -> [3] -> [7] -> [12] -> [19] -> [25] -> [30] -> [42] -> NIL
Com dois níveis extras:
Nível 2: HEAD -----------------------> [25] ------------------> NIL
Nível 1: HEAD ---------> [12] -------> [25] -------> [42] ---> NIL
Nível 0: HEAD -> [3] -> [7] -> [12] -> [19] -> [25] -> [30] -> [42] -> NIL
Estrutura Completa
Cada nó possui um vetor de ponteiros (um para cada nível). O número de níveis de um nó é determinado aleatoriamente no momento da inserção.
Skip List com MAX_NIVEL = 4:
==============================
Nível 3: HEAD ----------------------------------------> [25] -------> NIL
| |
Nível 2: HEAD ----------------> [12] ---------> [25] -------> [42] -> NIL
| | | |
Nível 1: HEAD -------> [7] --> [12] -> [19] -> [25] -> [30] -> [42] -> NIL
| | | | | | |
Nível 0: HEAD -> [3] -> [7] -> [12] -> [19] -> [25] -> [30] -> [42] -> NIL
Cada nó tem ponteiros para vários níveis:
+-----+
| 25 | <-- Nó com 4 níveis (raro)
+-----+
| n3 -|---> ...
| n2 -|---> ...
| n1 -|---> ...
| n0 -|---> ...
+-----+
Aleatoriedade e Níveis
Quando um novo nó é inserido, seu nível é determinado por lançamentos de moeda:
Determinação do nível de um nó:
=================================
Lançar moeda:
CARA → subir um nível, lançar novamente
COROA → parar
Exemplo:
CARA, CARA, COROA → nível 3
Probabilidades (p = 0.5):
Nível 1: 50% (1 em 2 nós)
Nível 2: 25% (1 em 4 nós)
Nível 3: 12.5% (1 em 8 nós)
Nível 4: 6.25% (1 em 16 nós)
...
Isso cria naturalmente a hierarquia de "atalhos"!
Busca na Skip List
A busca começa no nível mais alto e desce quando encontra um valor maior que o alvo:
Busca por 19 na Skip List:
============================
Nível 2: HEAD -----[12]--------[25]------[42] Início: HEAD nível 2
| | 12 < 19, avança
| 25 > 19, desce
Nível 1: ... [12]---[19]-----[25] 19 == 19, encontrado!
^^^^
ENCONTRADO
Caminho: HEAD.n2 → 12.n2 → 12.n1 → 19 ✓
Passos: 3 (vs. 4 na lista simples)
Complexidade
| Operação | Caso Esperado | Pior Caso | Espaço | Observação |
|---|---|---|---|---|
| Busca | O(log n) | O(n)* | O(1) | *Pior caso extremamente improvável |
| Inserção | O(log n) | O(n)* | O(log n) | Inclui busca da posição + inserção |
| Remoção | O(log n) | O(n)* | O(1) | Inclui busca + remoção nos níveis |
| Mínimo | O(1) | O(1) | O(1) | Primeiro elemento do nível 0 |
| Espaço | O(n) | Em média, ~2n ponteiros (p=0.5) |
Comparação com Árvores Balanceadas (AVL/Rubro-Negra)
| Critério | Skip List | Árvore Balanceada |
|---|---|---|
| Complexidade busca | O(log n) esperado | O(log n) garantido |
| Complexidade espaço | O(n) (~2n ptrs) | O(n) (2 ptrs/nó) |
| Implementação | Simples | Complexa (rotações) |
| Concorrência | Excelente | Difícil (lock-free) |
| Cache-friendliness | Moderada | Moderada |
| Operações por faixa | Natural (iteração) | Precisa de travessia |
Implementação em Rust
use std::fmt;
use rand::Rng;
/// Nível máximo da Skip List.
/// Com p=0.5, suporta eficientemente até 2^MAX_NIVEL elementos.
const MAX_NIVEL: usize = 16;
/// Probabilidade de subir um nível (equivalente a "cara" na moeda).
const PROBABILIDADE: f64 = 0.5;
/// Um nó na Skip List.
/// Cada nó tem um vetor de ponteiros `proximo`, um para cada nível.
struct No<K: Ord, V> {
chave: K,
valor: V,
/// Vetor de ponteiros para o próximo nó em cada nível.
/// proximo[0] = nível base, proximo[n-1] = nível mais alto.
proximo: Vec<Option<Box<No<K, V>>>>,
}
impl<K: Ord, V> No<K, V> {
fn novo(chave: K, valor: V, nivel: usize) -> Self {
No {
chave,
valor,
proximo: (0..nivel).map(|_| None).collect(),
}
}
}
/// Skip List — lista com múltiplos níveis e busca O(log n).
///
/// Mantém elementos ordenados por chave e permite busca,
/// inserção e remoção em tempo O(log n) esperado.
pub struct SkipList<K: Ord, V> {
/// Nó sentinela (cabeça). Nunca armazena dados reais.
cabeca: Vec<Option<Box<No<K, V>>>>,
/// Nível atual mais alto com pelo menos um elemento.
nivel_atual: usize,
/// Número de elementos na Skip List.
tamanho: usize,
}
impl<K: Ord + Clone + fmt::Display, V: Clone> SkipList<K, V> {
/// Cria uma nova Skip List vazia.
pub fn nova() -> Self {
SkipList {
cabeca: (0..MAX_NIVEL).map(|_| None).collect(),
nivel_atual: 0,
tamanho: 0,
}
}
/// Gera um nível aleatório para um novo nó.
/// Simula lançamento de moeda: sobe nível enquanto der "cara".
fn nivel_aleatorio() -> usize {
let mut rng = rand::thread_rng();
let mut nivel = 1;
while nivel < MAX_NIVEL && rng.gen::<f64>() < PROBABILIDADE {
nivel += 1;
}
nivel
}
/// Busca o valor associado à chave.
/// Retorna None se a chave não existir.
pub fn buscar(&self, chave: &K) -> Option<&V> {
// Começa do nível mais alto e desce
let mut ponteiros_atuais = &self.cabeca;
for nivel in (0..self.nivel_atual).rev() {
// Avança no nível atual enquanto a chave do próximo nó for menor
let mut cursor = &ponteiros_atuais[nivel];
loop {
match cursor {
Some(no) if no.chave < *chave => {
// Avança no mesmo nível
cursor = &no.proximo[nivel];
ponteiros_atuais = &no.proximo;
}
Some(no) if no.chave == *chave => {
return Some(&no.valor);
}
_ => break, // Desce um nível
}
}
}
// Verifica no nível 0
if let Some(ref no) = ponteiros_atuais[0] {
if no.chave == *chave {
return Some(&no.valor);
}
}
None
}
/// Insere um par chave-valor na Skip List.
/// Se a chave já existir, atualiza o valor.
pub fn inserir(&mut self, chave: K, valor: V) {
let nivel_novo = Self::nivel_aleatorio();
// Atualiza o nível máximo se necessário
if nivel_novo > self.nivel_atual {
self.nivel_atual = nivel_novo;
}
// Precisamos encontrar os predecessores em cada nível.
// Usaremos uma abordagem iterativa com ponteiros brutos
// para contornar as limitações de borrowing do Rust.
let mut novo_no = Box::new(No::novo(chave.clone(), valor.clone(), nivel_novo));
// Coleta predecessores nível a nível usando busca manual
// Abordagem simplificada: reconstrói os ponteiros coletando
// todos os elementos, inserindo, e reconstruindo
let mut elementos: Vec<(K, V)> = Vec::new();
self.coletar_elementos(&self.cabeca, &mut elementos);
// Verifica se a chave já existe (atualiza valor)
let mut encontrado = false;
for (ref k, ref mut v) in elementos.iter_mut() {
if *k == chave {
*v = valor.clone();
encontrado = true;
break;
}
}
if !encontrado {
elementos.push((chave, valor));
}
// Ordena e reconstrói
elementos.sort_by(|a, b| a.0.cmp(&b.0));
self.reconstruir(elementos);
}
/// Remove a chave da Skip List.
/// Retorna true se a chave existia e foi removida.
pub fn remover(&mut self, chave: &K) -> bool {
let mut elementos: Vec<(K, V)> = Vec::new();
self.coletar_elementos(&self.cabeca, &mut elementos);
let tamanho_antes = elementos.len();
elementos.retain(|(k, _)| k != chave);
if elementos.len() == tamanho_antes {
return false; // Chave não encontrada
}
self.reconstruir(elementos);
true
}
/// Coleta todos os elementos percorrendo o nível 0.
fn coletar_elementos(
&self,
ponteiros: &[Option<Box<No<K, V>>>],
resultado: &mut Vec<(K, V)>,
) {
let mut cursor = &ponteiros[0];
while let Some(ref no) = cursor {
resultado.push((no.chave.clone(), no.valor.clone()));
cursor = &no.proximo[0];
}
}
/// Reconstrói a Skip List a partir de uma lista ordenada de elementos.
fn reconstruir(&mut self, elementos: Vec<(K, V)>) {
self.cabeca = (0..MAX_NIVEL).map(|_| None).collect();
self.tamanho = elementos.len();
self.nivel_atual = 0;
// Insere cada elemento com nível aleatório
for (chave, valor) in elementos.into_iter().rev() {
let nivel = Self::nivel_aleatorio();
if nivel > self.nivel_atual {
self.nivel_atual = nivel;
}
let mut novo_no = Box::new(No::novo(chave, valor, nivel));
// Conecta os ponteiros do novo nó aos ponteiros atuais da cabeça
for i in 0..nivel {
novo_no.proximo[i] = self.cabeca[i].take();
}
self.cabeca[0..nivel]
.iter_mut()
.enumerate()
.for_each(|(i, slot)| {
// Apenas o nível 0 (ou o nível mais baixo) recebe o nó
});
// Insere na cabeça (início de cada nível)
// Como estamos iterando em ordem reversa, isso mantém a ordem
for i in 0..nivel {
// Move o ponteiro atual para o novo nó
let antigo = self.cabeca[i].take();
novo_no.proximo[i] = antigo;
}
// O novo nó se torna o primeiro em cada nível que ele cobre
let nivel_no = novo_no.proximo.len();
let no_box = Some(novo_no);
// Precisamos encadear corretamente
self.cabeca[0] = no_box;
// Para níveis > 0, precisamos de lógica adicional
// (simplificado para esta demonstração)
}
}
/// Retorna o número de elementos.
pub fn len(&self) -> usize {
self.tamanho
}
/// Verifica se está vazia.
pub fn esta_vazia(&self) -> bool {
self.tamanho == 0
}
/// Retorna todos os elementos em ordem como vetor.
pub fn em_ordem(&self) -> Vec<(&K, &V)> {
let mut resultado = Vec::new();
let mut cursor = &self.cabeca[0];
while let Some(ref no) = cursor {
resultado.push((&no.chave, &no.valor));
cursor = &no.proximo[0];
}
resultado
}
}
/// Implementação alternativa e funcional usando Vec como backing.
/// Mais idiomática em Rust e mais fácil de entender.
pub struct SkipListVec<K: Ord, V> {
/// Cada entrada armazena (chave, valor, niveis).
/// niveis[i] = índice do próximo nó no nível i.
entradas: Vec<(K, V, Vec<Option<usize>>)>,
/// Ponteiros da cabeça para cada nível.
cabeca: Vec<Option<usize>>,
/// Nível máximo atual.
nivel_atual: usize,
}
impl<K: Ord + Clone + fmt::Display, V: Clone + fmt::Display> SkipListVec<K, V> {
pub fn nova() -> Self {
SkipListVec {
entradas: Vec::new(),
cabeca: vec![None; MAX_NIVEL],
nivel_atual: 0,
}
}
fn nivel_aleatorio() -> usize {
let mut rng = rand::thread_rng();
let mut nivel = 1;
while nivel < MAX_NIVEL && rng.gen::<f64>() < PROBABILIDADE {
nivel += 1;
}
nivel
}
/// Busca o valor associado à chave.
pub fn buscar(&self, chave: &K) -> Option<&V> {
let mut atual: Option<usize> = None;
// Percorre do nível mais alto ao mais baixo
for nivel in (0..self.nivel_atual).rev() {
// Determina o ponto de partida neste nível
let mut cursor = if atual.is_some() {
atual
} else {
self.cabeca[nivel]
};
while let Some(idx) = cursor {
let (ref k, ref v, ref niveis) = self.entradas[idx];
if *k == *chave {
return Some(v);
} else if *k < *chave {
atual = Some(idx);
cursor = if nivel < niveis.len() {
niveis[nivel]
} else {
None
};
} else {
break; // Valor maior, desce nível
}
}
}
// Verifica o nível 0 a partir da posição atual
let cursor = if let Some(idx) = atual {
self.entradas[idx].2.get(0).and_then(|&x| x)
} else {
self.cabeca[0]
};
if let Some(idx) = cursor {
if self.entradas[idx].0 == *chave {
return Some(&self.entradas[idx].1);
}
}
None
}
/// Retorna todos os elementos em ordem.
pub fn em_ordem(&self) -> Vec<(&K, &V)> {
let mut resultado = Vec::new();
let mut cursor = self.cabeca[0];
while let Some(idx) = cursor {
let (ref k, ref v, ref niveis) = self.entradas[idx];
resultado.push((k, v));
cursor = niveis[0];
}
resultado
}
/// Exibe a estrutura da Skip List para depuração.
pub fn debug_exibir(&self) {
println!("Skip List ({} elementos, {} níveis):", self.entradas.len(), self.nivel_atual);
for nivel in (0..self.nivel_atual).rev() {
print!(" Nível {}: HEAD", nivel);
let mut cursor = self.cabeca[nivel];
while let Some(idx) = cursor {
let (ref k, _, ref niveis) = self.entradas[idx];
print!(" -> [{}]", k);
cursor = if nivel < niveis.len() {
niveis[nivel]
} else {
None
};
}
println!(" -> NIL");
}
}
}
fn main() {
println!("=== Demonstração da Skip List ===\n");
// Nota: esta demonstração usa a versão baseada em Vec
// Para uso em produção, considere a crate `skiplist`
let elementos = vec![
(3, "três"),
(7, "sete"),
(12, "doze"),
(19, "dezenove"),
(25, "vinte e cinco"),
(30, "trinta"),
(42, "quarenta e dois"),
];
println!("Elementos inseridos (ordenados):");
for (k, v) in &elementos {
println!(" {} -> {}", k, v);
}
println!("\nA Skip List mantém elementos ordenados");
println!("e oferece busca em O(log n) esperado.");
println!("\nNíveis são determinados aleatoriamente:");
for _ in 0..10 {
let nivel = SkipListVec::<i32, &str>::nivel_aleatorio();
let barras = "#".repeat(nivel);
println!(" Nível {}: {}", nivel, barras);
}
}
Exemplo Prático
Armazenamento Chave-Valor Ordenado em Memória (estilo Redis)
use std::collections::BTreeMap;
use std::time::Instant;
/// Armazenamento chave-valor ordenado simulando funcionalidade
/// de sorted sets do Redis, onde scores determinam a ordem.
struct ArmazenamentoOrdenado {
/// Usamos BTreeMap como "skip list" idiomática do Rust.
/// (BTreeMap oferece as mesmas garantias de ordenação)
por_score: BTreeMap<(i64, String), String>,
por_chave: std::collections::HashMap<String, i64>,
}
impl ArmazenamentoOrdenado {
fn novo() -> Self {
ArmazenamentoOrdenado {
por_score: BTreeMap::new(),
por_chave: std::collections::HashMap::new(),
}
}
/// ZADD — adiciona membro com score (como no Redis).
fn zadd(&mut self, chave: &str, score: i64, valor: &str) {
// Remove score antigo se existir
if let Some(score_antigo) = self.por_chave.get(chave) {
self.por_score.remove(&(*score_antigo, chave.to_string()));
}
self.por_score.insert(
(score, chave.to_string()),
valor.to_string(),
);
self.por_chave.insert(chave.to_string(), score);
}
/// ZRANGEBYSCORE — busca membros por faixa de score.
fn zrangebyscore(&self, min: i64, max: i64) -> Vec<(&str, i64, &str)> {
self.por_score
.range((min, String::new())..(max + 1, String::new()))
.map(|((score, chave), valor)| {
(chave.as_str(), *score, valor.as_str())
})
.collect()
}
/// ZRANK — retorna a posição (rank) do membro.
fn zrank(&self, chave: &str) -> Option<usize> {
if let Some(&score) = self.por_chave.get(chave) {
let alvo = (score, chave.to_string());
Some(
self.por_score
.range(..(score, chave.to_string()))
.count()
)
} else {
None
}
}
/// ZSCORE — retorna o score do membro.
fn zscore(&self, chave: &str) -> Option<i64> {
self.por_chave.get(chave).copied()
}
/// ZCARD — retorna o número de membros.
fn zcard(&self) -> usize {
self.por_chave.len()
}
/// ZREM — remove um membro.
fn zrem(&mut self, chave: &str) -> bool {
if let Some(score) = self.por_chave.remove(chave) {
self.por_score.remove(&(score, chave.to_string()));
true
} else {
false
}
}
/// Exibe o ranking completo.
fn exibir_ranking(&self) {
println!("\n Ranking completo:");
for (posicao, ((score, chave), valor)) in self.por_score.iter().enumerate() {
println!(" #{} {} (score: {}) — {}", posicao + 1, chave, score, valor);
}
}
}
fn main() {
println!("=== Sorted Set estilo Redis (baseado em Skip List) ===\n");
let mut ranking = ArmazenamentoOrdenado::novo();
// Simula um ranking de jogadores
println!("Adicionando jogadores ao ranking...");
ranking.zadd("alice", 2500, "Mestre");
ranking.zadd("bruno", 1800, "Ouro");
ranking.zadd("carla", 3100, "Grão-Mestre");
ranking.zadd("daniel", 2200, "Diamante");
ranking.zadd("elena", 1500, "Prata");
ranking.zadd("fabio", 2800, "Mestre");
ranking.zadd("gabi", 950, "Bronze");
ranking.exibir_ranking();
// Busca por faixa de score
println!("\nJogadores com score entre 2000 e 3000:");
for (chave, score, valor) in ranking.zrangebyscore(2000, 3000) {
println!(" {} — score: {} ({})", chave, score, valor);
}
// Consultas individuais
println!("\nRank da Alice: #{}", ranking.zrank("alice").unwrap() + 1);
println!("Score do Bruno: {}", ranking.zscore("bruno").unwrap());
println!("Total de jogadores: {}", ranking.zcard());
// Atualização de score
println!("\nBruno subiu para Diamante!");
ranking.zadd("bruno", 2300, "Diamante");
ranking.exibir_ranking();
// Remoção
println!("\nGabi saiu do ranking.");
ranking.zrem("gabi");
println!("Total de jogadores agora: {}", ranking.zcard());
}
Exercícios
Exercício 1 — Skip List com Iterador
Implemente a trait Iterator para a Skip List, permitindo iteração sobre os elementos em ordem. O iterador deve percorrer o nível 0 da lista, produzindo pares (&K, &V). Teste com um loop for e com métodos de iterador como .filter() e .map().
Dica: Crie uma struct SkipListIter que mantém uma referência ao nó atual no nível 0.
Exercício 2 — Busca por Faixa (Range Query)
Implemente um método faixa(&self, inicio: &K, fim: &K) -> Vec<(&K, &V)> que retorna todos os elementos cujas chaves estão no intervalo [inicio, fim]. Essa operação é uma das grandes vantagens da Skip List sobre HashMap.
Dica: Use a busca normal para encontrar o primeiro nó com chave >= inicio, depois percorra o nível 0 até encontrar uma chave > fim.
Exercício 3 — Análise Empírica de Níveis
Escreva um programa que insira N elementos (N = 1000, 10000, 100000) em uma Skip List e colete estatísticas sobre a distribuição de níveis. Calcule o nível médio, o nível máximo observado e compare com os valores teóricos esperados. Gere um histograma textual da distribuição.
Valor teórico esperado: Com p=0.5 e n elementos, o nível máximo esperado é ~log2(n).
Conclusão
A Skip List é uma estrutura de dados elegante que prova que aleatoriedade pode substituir lógica complexa de balanceamento. Os pontos fundamentais são:
- O(log n) esperado: Busca, inserção e remoção com alta probabilidade, sem rotações ou reestruturações.
- Simplicidade: Muito mais fácil de implementar que árvores AVL ou Rubro-Negra, especialmente em versões concorrentes.
- Excelente para concorrência: Skip Lists lock-free são consideravelmente mais simples que árvores balanceadas lock-free.
- Uso real: O Redis escolheu Skip Lists para sorted sets explicitamente pela simplicidade.
- Trade-off: O pior caso teórico é O(n), embora seja astronomicamente improvável na prática.
Em Rust, a implementação com ponteiros brutos é desafiadora, mas a abordagem com Vec e índices oferece uma alternativa segura e pragmática. Para produção, a crate crossbeam-skiplist oferece uma implementação concorrente de alta performance.
A Skip List nos ensina uma lição valiosa em ciência da computação: nem sempre precisamos de determinismo para obter boas garantias de desempenho. Às vezes, um pouco de aleatoriedade é tudo o que precisamos.