SkipList Demo

Keith Vetter 2003-11-21 : Skip lists are really cool. They're a probabilistic data structure that seem likely to supplant balanced trees. They implement all of balanced trees major operations like search, insert, delete, merge, etc. with the same time bounds of O(log n), but with a smaller constant factor.

I just noticed that there is now a skiplist module as part of the struct module of tcllib. This is code I wrote a while ago and completely forgot about--thanks to whoever picked up the ball and got it into tcllib.

Skip lists have a probabilistic time bound meaning that the worst case behavior can be bad but, due to using randomness, the probability of this happening can be bounded. This is much like quicksort which pivots on a random element. In comparison, AVL trees, 2-3 trees and red-black trees have a deterministic bound and splay trees have an amortized bound.

Here's a little demo illustrating how skip lists work. It exploits inner knowledge of how the tcllib skiplist module works. More more details of this cool data structure see the reference on the tcllib skiplist page.


Jeff Smith 2020-08-10 : Below is an online demo using CloudTk. This demo runs SkipList Demo in an Alpine Linux Docker Container. It is a 27.4MB image which is made up of Alpine Linux + tclkit + SkipList-Demo.kit + libx11 + libxft + fontconfig + ttf-linux-libertine. It is run under a user account in the Container. The Container is restrictive with permissions for "Other" removed for "execute" and "read" for certain directories.


 ##+##########################################################################
 #
 # skiplist.tcl - Demos for how skiplists work
 # by Keith Vetter, November 21, 2003
 #
 # NB. uses internal knowledge of tcllib's ::struct::skiplist package
 #
 
 package require Tk 8.2
 package require struct 1.3
 
 set S(title) "Skip Lists"
 array set S {lm 20 bm 20 box,x 30 box,y 15 box,dy 0 box,dx 20 MaxKey 1000}
 array set S {bg antiquewhite2 c,link cyan c,value yellow c,nil lightgreen}
 
 
 proc DoDisplay {} {
    global S
 
    wm title . $S(title)
    wm geom . +10+10
    pack [frame .ctrl -relief ridge -bd 2 -padx 5 -pady 5] \
        -side bottom -fill x -ipadx 5
    pack [frame .screen -bd 2 -relief raised] -side top -fill both -expand 1
 
    set w [expr {[winfo screenwidth .] - 100}]
    if {$w > 900} {set w 900}
    canvas .c -relief raised -bd 0 -height 200 -width $w \
        -xscrollcommand {.sb set} -bg $S(bg) -highlightthickness 0
    .c create text -100 -100 -tag txt
    eval font create bfont "[font actual [.c itemcget txt -font]] -weight bold"
    .c delete txt
 
    label .msg -font {Times 24} -text "Skip List Demo" -bg $S(bg)
    scrollbar .sb -orient horizontal -command {.c xview}
    pack .msg -in .screen -side top -fill x
    pack .c -in .screen -side top -fill both -expand 1
    pack .sb -in .screen -side bottom -fill x
    bind all <Key-F2> {console show}
    DoCtrlFrame
 
    trace variable S(key) w tracer
    set S(key) ""
    update
    focus .key
 }
 proc DoCtrlFrame {} {
    global S
 
    frame .row2
    button .insert -text "Insert" -bd 4 -command DoInsert
    .insert configure  -font "[font actual [.insert cget -font]] -weight bold"
    option add *Button.font [.insert cget -font]
    option add *Label.font [.insert cget -font]
 
    button .search -text "Search" -bd 4 -command DoSearch
    button .delete -text "Delete" -bd 4 -command DoDelete
    button .reset -text "Reset" -bd 4 -command Reset
    button .random -text "Insert Random" -bd 4 -command DoInsertRandom
 
    label .lkey -text "Key:"
    entry .key -textvariable S(key) -width 6 -justify center
    label .lvalue -text "Value:"
    entry .value -textvariable S(value) -width 6 -justify center
 
    label .lresult -text "Result:"
    label .result -textvariable S(result) -bd 2 -bg white -width 30 \
        -relief ridge
 
    button .about -text About -bd 4 -command \
       [list tk_messageBox -message "$S(title)\nby Keith Vetter, November 2003"]
 
    grid .lkey .key .lvalue .value .search .insert .delete .lresult .result \
        -in .ctrl -row 0 -sticky news
    grid .row2 -columnspan 20 -in .ctrl -row 1 -sticky ew -pady 5
    grid .reset .random .about -in .row2 -row 1 -sticky news -padx 5
 
    grid config .search .insert .delete -padx 5
    grid columnconfigure .ctrl 50 -weight 1
    grid columnconfigure .row2 50 -weight 1
    grid rowconfigure .row2 0 -minsize 10
 }
 proc tracer {var1 var2 op} {
    global S
 
    set state disabled
    if {[string is integer -strict $S(key)]} {set state normal}
    foreach w [list .search .insert .delete] {
        $w config -state $state
    }
 }
 
 proc Pos2XY {lvl nth} {
    global S
 
    set xy {}
    set cx [expr {$S(lm) + ($nth+.5) * ($S(box,x) + $S(box,dx))}]
    set cy [winfo height .c]
    set cy [expr {$cy - $S(bm) - ($lvl+.5) * ($S(box,y) + $S(box,dy))}]
    if {$lvl > 0} {set cy [expr {$cy - 5}]}
 
    set l [expr {$cx - $S(box,x) / 2.0}]
    set t [expr {$cy - $S(box,y) / 2.0}]
    set r [expr {$l + $S(box,x)}]
    set b [expr {$t + $S(box,y)}]
 
    return [list $cx $cy $l $t $r $b]
 }
 
 proc DrawSkiplist {} {
    global S nodes state nid2pos key2pos
 
    .c delete all
    set S(msg) "Skiplist: Level: $state(level) Probability: $state(prob)"
 
    catch {unset nid2pos}
    for {set x header; set cnt 0} {$x != "nil"} {set x $nodes($x,1); incr cnt} {
        set nid2pos($x) $cnt
        set key2pos($nodes($x,key)) $cnt
    }
    for {set x header; set cnt 0} {$x != "nil"} {set x $nodes($x,1); incr cnt} {
        DrawNode $x
    }
 
    foreach {x0 y0 x1 y1} [.c bbox all] break
    incr x1 $S(lm)
    .c config -scrollregion [list 0 $y0 $x1 $y1]
 }
 proc DrawNode {nid} {
    global state nodes nid2pos S
 
    set lvls [llength [array names nodes $nid,*]]
    incr lvls -1
    if {$lvls > $state(level)+1} { set lvls [expr {$state(level) + 2}] }
    for {set lvl 0} {$lvl < $lvls} {incr lvl} {
        set xy [Pos2XY $lvl $nid2pos($nid)]
        foreach {cx cy x0 y0 x1 y1} $xy break
        set n [.c create rect $x0 $y0 $x1 $y1]
        if {$lvl == 0} {
            .c itemconfig $n -width 2 -fill $S(c,value)
            .c create text $cx $cy -anchor c -text $nodes($nid,key) -font bfont
            if {1} {
                set xy [Pos2XY -1 $nid2pos($nid)]
                foreach {cx2 cy2} $xy break
                .c create text $cx2 $cy2 -text $nid -font bfont
            }
        } elseif {$nodes($nid,$lvl) == "nil"} {
            .c itemconfig $n -fill $S(c,nil)
            .c create text $cx $cy -anchor c -text \u03a9 -tag nil -font bfont
        } else {
            .c itemconfig $n -fill $S(c,link)
            set xy [Pos2XY $lvl $nid2pos($nodes($nid,$lvl))]
 
            foreach {cx2 cy2 x3 y3} $xy break
            .c create oval [Box $cx $cy 3] -fill black
            .c create line $cx $cy $x3 $cy2 -arrow last -width 2
        }
    }
 }
 proc Box {x y d} {
    return [list [expr {$x-$d}] [expr {$y-$d}] [expr {$x+$d}] [expr {$y+$d}]]
 }
 proc DoInsert {} {
    global S
    set n [mySList insert $S(key) $S(value)]
    DrawSkiplist
    if {$n} {
        set S(result) "Inserted: node (key=$S(key) value=$S(value))"
    } else {
        set S(result) "Updated: node (key=$S(key) value=$S(value))"
    }
 }
 proc DoDelete {} {
    global S
 
    foreach {k v} [mySList search $S(key)] break
    if {$k == 0} {
        set S(result) "Cannot find node with key '$S(key)'"
        return
    }
    mySList delete $S(key)
    DrawSkiplist
    set S(result) "Deleted: node (key=$S(key) value=$S(value))"
 }
 proc DoInsertRandom {{draw 1}} {
    global S
 
    for {set i 0} {$i < $S(MaxKey)} {incr i} {
        set S(key) [expr {int(rand() * $S(MaxKey))}]
        if {[llength [mySList search $S(key)]] == 1} break
    }
    set S(value) V$S(key)
    mySList insert $S(key) $S(value)
    if {$draw} {
        DrawSkiplist
        set S(result) "Random: node (key=$S(key) value=$S(value))"
    }
 }
 proc Reset {{draw 1}} {
    uplevel \#0 {
        set name mySList
        catch {$name destroy}
        ::struct::skiplist $name
        upvar \#0 ::struct::skiplist::skiplist${name}::state state
        upvar \#0 ::struct::skiplist::skiplist${name}::nodes nodes
    }
    if {$draw} DrawSkiplist
    set S(key) [set S(value) ""]
    set S(result) ""
 }
 
 proc DoSearch {} {
    global S nid2pos nodes
 
    .c delete search
    foreach {found path} [SkipSearch $S(key)] break
 
    set x -1
    foreach {nid lvl} $path {
        if {$nid == "nil"} continue
        set xy [Pos2XY $lvl $nid2pos($nid)]
        foreach {cx cy x0 y0 x1 y1} $xy break
        if {$x != -1} {
            set xy [MakeArc $x $y $cx $y0]
            .c create line $xy -tag search -fill red -width 2 -arrow last \
                -smooth 1
        }
        set x $cx
        set y $y0
    }
 
    if {$found == 0} {
        set S(value) ""
        set S(result) "Not found: node with key $S(key)"
    } else {
        set S(value) $nodes($nid,value)
        set S(result) "Found: node (key=$S(key) value=$S(value))"
    }
 }
 proc SkipSearch {key} {
    global S nodes state
 
    set look {}
    set x header
    for {set i $state(level)} {$i >= 1} {incr i -1} {
        lappend look $x $i
        while {1} {
            set fwd $nodes($x,$i)
            lappend look $fwd $i
            if {$nodes($fwd,key) == $::struct::skiplist::MAXINT} break
            if {$nodes($fwd,key) >= $key} break
            set x $fwd
        }
    }
    set x $nodes($x,1)
    if {$nodes($x,key) == $key} {
        return [list 1 $look]
    }
    return [list 0 $look]
 }
 proc MakeArc {x0 y0 x1 y1} {
    if {$x0 == $x1} {return [list $x0 $y0 $x1 $y1]}
    set cx [expr {($x0 + $x1) / 2}]
    if {abs($x0 - $x1) < 100} {
        set cy [expr {$y0 - 20}]
    } else {
        set cy [expr {$y0 - 50}]
    }
    return [list $x0 $y0 $cx $cy $x1 $y1]
 }
 
 ################################################################
 DoDisplay
 
 Reset 0
 for {set i 0} {$i < 15} {incr i} {
    DoInsertRandom 0
 }
 DrawSkiplist

frame appears not to support the options -padx and -pady (in Tcl 8.3).


(Deleted some code that seemed to have crept in from A tiny input manager.)