Привет, Хабр! Сегодня я решил поделиться с вами одной из тех структур данных, которая, возможно, не так популярна, как хеш‑таблицы или деревья, но обладает своими уникальными фичами. Знакомьтесь — Skip List!
Итак, Skip List — это структура данных, которая позволяет быстро искать, вставлять и удалять элементы. Можно сказать, что это своего рода гибрид между списком и деревом, только без всяких заморочек.
Рассмотрим реализацию этой структуры в Golang, и для этого есть пакет huandu/skiplist.
Начнем с самого простого — установки библиотеки. Откройте терминал и введите:
go get github.com/huandu/skiplist
Создание и использование Skip List
Ничего сложного здесь нет.
package main
import (
"fmt"
"github.com/huandu/skiplist"
)
func main() {
// Создаем Skip List с ключами типа int
list := skiplist.New(skiplist.Int)
// Добавляем элементы
list.Set(10, "десять")
list.Set(20, "двадцать")
list.Set(30, "тридцать")
// Получаем элемент по ключу
elem := list.Get(20)
if elem != nil {
fmt.Println("Ключ:", elem.Key(), "Значение:", elem.Value)
}
// Итерация по элементам
for elem := list.Front(); elem != nil; elem = elem.Next() {
fmt.Printf("Ключ: %v, Значение: %v\n", elem.Key(), elem.Value)
}
// Удаление элемента
list.Remove(20)
fmt.Println("После удаления ключа 20:")
for elem := list.Front(); elem != nil; elem = elem.Next() {
fmt.Printf("Ключ: %v, Значение: %v\n", elem.Key(), elem.Value)
}
}
Мы начинаем с инициализации нового Skip List с ключами типа int
, затем вставляем три элемента с ключами 10, 20 и 30. Далее ищем элемент с ключом 20 и выводим его значение. После этого проходимся по всем элементам списка, выводя их на экран. Наконец, удаляем элемент с ключом 20 и снова отображаем обновленный список.
Вывод программы:
Ключ: 20 Значение: двадцать
Ключ: 10, Значение: десять
Ключ: 20, Значение: двадцать
Ключ: 30, Значение: тридцать
После удаления ключа 20:
Ключ: 10, Значение: десять
Ключ: 30, Значение: тридцать
Просто, как дважды два, правда. Но копнем чуть глубже и узнаем, как можно использовать эту структуру эффективней.
Кастомизация
Иногда стандартных возможностей недостаточно. Например, нужно использовать структуру в качестве ключа и сортировать элементы по какому‑то специфическому критерию.
Создадим Skip List, где ключами будут структуры с углами в радианах, и отсортируем их по значению синуса.
package main
import (
"fmt"
"math"
"github.com/huandu/skiplist"
)
type Angle struct {
Rad float64
}
func main() {
// Создаем Skip List с кастомной функцией сравнения
list := skiplist.New(skiplist.GreaterThanFunc(func(a, b interface{}) int {
angleA := a.(Angle).Rad
angleB := b.(Angle).Rad
sinA := math.Sin(angleA)
sinB := math.Sin(angleB)
switch {
case sinA > sinB:
return 1
case sinA < sinB:
return -1
default:
return 0
}
}))
// Добавляем элементы
list.Set(Angle{Rad: math.Pi / 8}, "sin(π/8)")
list.Set(Angle{Rad: math.Pi / 2}, "sin(π/2)")
list.Set(Angle{Rad: math.Pi}, "sin(π)")
// Выводим элементы в порядке сортировки
for elem := list.Front(); elem != nil; elem = elem.Next() {
fmt.Printf("Ключ: %.2f, Значение: %v\n", elem.Key().(Angle).Rad, elem.Value)
}
}
Определили структуру Angle
с полем для угла в радианах, настроили Skip List с пользовательской функцией сравнения на основе значений синуса углов, добавили три таких угла с соответствующими значениями синуса и вывели их. В результате элементы отсортировались по возрастанию значения синуса.
Вывод программы:
Ключ: 3.14, Значение: sin(π)
Ключ: 1.57, Значение: sin(π/2)
Ключ: 0.39, Значение: sin(π/8)
Элементы отсортированы не по значению угла, а по значению его синуса
Потокобезопасность
К сожалению, huandu/skiplist
не предоставляет встроенной поддержки потокобезопасности, но ничего страшного. Добавим сами с помощью sync.RWMutex
:
package main
import (
"fmt"
"sync"
"github.com/huandu/skiplist"
)
type SafeSkipList struct {
list *skiplist.SkipList
mutex sync.RWMutex
}
func NewSafeSkipList(comparable skiplist.Comparable) *SafeSkipList {
return &SafeSkipList{
list: skiplist.New(comparable),
}
}
func (s *SafeSkipList) Set(key, value interface{}) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.list.Set(key, value)
}
func (s *SafeSkipList) Get(key interface{}) (interface{}, bool) {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.list.GetValue(key)
}
func (s *SafeSkipList) Remove(key interface{}) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.list.Remove(key)
}
func main() {
safeList := NewSafeSkipList(skiplist.Int)
safeList.Set(1, "один")
safeList.Set(2, "два")
value, ok := safeList.Get(1)
if ok {
fmt.Println("Ключ 1 имеет значение:", value)
}
safeList.Remove(1)
_, ok = safeList.Get(1)
if !ok {
fmt.Println("Ключ 1 успешно удален")
}
}
Создали обертку SafeSkipList
, которая объединяет сам Skip List с мьютексом. Методы Set
, Get
и Remove
теперь аккуратно обернуты блокировками мьютекса, чтобы никакие горутины не нарушили целостность списка.
Если у вас есть какие‑то интересные примеры применения Skip List, обязательно делитесь в комментариях. С наступающим Новым Годом!
Всех разработчиков на Go приглашаем на открытый урок 25 декабря: «Каналы Go: от устройства до практического применения». Мы подробно разберем устройство и механизмы работы каналов, рассмотрим возможные ситуации, а затем на примерах кода посмотрим, как использовать каналы в программах. Записывайтесь по ссылке.
Все остальные открытые уроки по разным ИТ-направлениям можно посмотреть в календаре мероприятий.
Комментарии (3)
FreeNickname
22.12.2024 17:49Первая на моей памяти статья о структуре данных, в которой ни слова о структуре данных.
SpiderEkb
22.12.2024 17:49Вообще-то стоило бы начать со ссылки на статью автора алгоритма с описанием структуры и принципов работы
William Pugh Skip Lists: A Probabilistic Alternative to Balanced Trees
А если погуглить еще, то можно найти много расширений. Например, добавление возможности поиска не только по ключу, но и по номеру жлемента. Или реализацию конкурентного lockless списка...
И, кстати, тут ни слова о том, что оригинальная реализация подразумевает уникальность ключа...
wilcot
О каких уникальных фичах идет речь? Поиск по ключу, вставка, удаление, итерирование есть в обычном map[K]V. Упорядочивание по компаратору в каком-нибуть BTree, RBTree или AVLTree есть.
Хотелось бы конкретики. Открыл код, по сравнению с листом - код выглядит сложнее. По сравнению с деревьями поиска +- одинаково, может чуть проще.
Как итог:
Никакого описания принципа работы.
Отсутствие оценки скорости работы "уникальных операций" (хотя бы асимптотическая оценка).
Отсутствие примеров, когда данная структура может дать преимущество над общеизвестными бинарными деревьями поиска и хеш-таблицами.