proc qinit {qvar} { upvar 1 $qvar Q set Q [list] } proc qput {qvar elem} { upvar 1 $qvar Q lappend Q $elem } proc qget {qvar} { upvar 1 $qvar Q set head [lindex $Q 0] set Q [lrange $Q 1 end] return $head } proc qempty {qvar} { upvar 1 $qvar Q return [expr {[llength $Q] == 0}] }The above implementation of qput is efficient, since lappend has amortized O(1) runtime, but qget is problematic. lrange $Q 1 end is O(N), so in the worst case an algorithm using this queue implementation would take O(N^2) time.We can improve on this by borrowing an idea from Hood-Melville queues: split the queue into two pieces, "L" and "R". qput adds elements to R, and qget takes them from L; if L is empty, move the contents of R onto L. Instead of actually removing elements from L, we just keep track of the index of the next item; the left half of the queue is empty when this index reaches the end of the list.
proc qinit {qvar} { upvar 1 $qvar Q set Q(i) 0 set Q(l) [list] set Q(r) [list] } proc qput {qvar elem} { upvar 1 $qvar Q lappend Q(r) $elem } proc qget {qvar} { upvar 1 $qvar Q if {$Q(i) >= [llength $Q(l)]} { set Q(l) $Q(r) set Q(r) [list] set Q(i) 0 } set head [lindex $Q(l) $Q(i)] incr Q(i) return $head } proc qempty {qvar} { upvar 1 $qvar Q return [expr {$Q(i) >= [llength $Q(l)] && [llength $Q(r)] == 0}] }Now qput and qget both run in O(1) time, and the space usage is at most a constant factor more than the naive implementation using a single list.--Joe English
KPV 2003-08-06: Having left and right queues seems overly complicated to me. Why not just have a head pointer that points to the current head of the queue. The head would get initialized to 0 and the queue is empty when the head is equal to the length of the queue. qput is still just an lappend and qget is just a lindex and an increment.JE That also has good time complexity, but the space complexity is worse since elements taken from the head of the queue are never freed. Worst-case, the size of the queue grows without bound.I just recently wrote such a beast when I needed to do a shortest-path search via a breadth first search. One key requirement for the BFS is that queue must not be destroyed--it is needed for walking back along the shortest path.Here's code that implements this idea:
proc q'init {qvar} { upvar 1 $qvar Q set Q(q) [list] set Q(h) 0 } proc q'put {qvar elem} { upvar 1 $qvar Q lappend Q(q) $elem } proc q'get {qvar} { upvar 1 $qvar Q set head [lindex $Q(q) $Q(h)] incr Q(h) return $head } proc q'empty {qvar} { upvar 1 $qvar Q return [expr {[llength $Q(q)] == $Q(h)}] }
AMG: Here's a queue implementation that uses [namespace ensemble]. Also it tries to give [lrange] an unshared queue object so it can work in-place and avoid unnecessary copying. It's a bit more flexible in that it allows specifying an initial value for the queue, and any number of elements can be enqueued at once.
namespace eval queue { namespace ensemble create -subcommands {create put get empty} proc create {queueVar args} { upvar 1 $queueVar queue set queue $args } proc put {queueVar args} { upvar 1 $queueVar queue lappend queue {*}$args } proc get {queueVar} { upvar 1 $queueVar queue set head [lindex $queue 0] set queue [lrange $queue[set queue ""] 1 end] return $head } proc empty {queueVar} { upvar 1 $queueVar queue expr {![llength $queue]} } }Example usage:
% queue create foo a b c a b c % queue empty foo 0 % queue put foo d e f a b c d e f % while {![queue empty foo]} {puts [queue get foo]} a b c d e f % queue empty foo 1Performance testing:
% set q [lrepeat 1000000 x]; time {queue get q} 1000 2759.502 microseconds per iteration % set q [lrepeat 1000000 x]; time {qget q} 1000 32072.98 microseconds per iteration % set q [lrepeat 1000 x]; time {queue get q} 1000 8.483 microseconds per iteration % set q [lrepeat 1000 x]; list; time {qget q} 1000 17.357 microseconds per iterationI tried using [lassign] instead of [lrange], but it was far slower. I think it was making a copy despite being passed an unshared object. I also tried [lreplace] instead of [lrange], but the two had identical performance.Using $queue[set queue ""] is a clear performance win, but it still takes substantially more time to get an element from a long queue than a short one. This is because of the memory move used to delete the first element. Changing the code to reverse the queue order fixes this problem:
proc qget {qvar} { upvar 1 $qvar Q set head [lindex $Q end] set Q [lrange $Q 0 end-1] return $head } proc ::queue::get queueVar { upvar 1 $queueVar queue set head [lindex $queue end] set queue [lrange $queue[set queue ""] 0 end-1] return $head } % set q [lrepeat 1000000 x]; time {queue get q} 1000 7.425 microseconds per iteration % set q [lrepeat 1000000 x]; time {qget q} 1000 32813.389 microseconds per iteration % set q [lrepeat 1000 x]; time {queue get q} 1000 7.597 microseconds per iteration % set q [lrepeat 1000 x]; time {qget q} 1000 16.465 microseconds per iterationNow the queue length has no measurable impact on the timing of [queue get]. It still affects [qget] majorly, since it copies.Of course, fixing the performance of [queue get] simply moved the problem to [queue put]! Only one of the two can have O(1) time, at least when using a simple linear array as backing store. Linked lists allow for constant time enqueuing and dequeuing, but locality of reference suffers.$var[set var ""] may have tremendous performance benefits, but it's clumsy and counterintuitive. I suggest this alternate formulation:
proc take {varName} { upvar 1 $varName var return $var[set var ""] } # example: set queue [lrange [take queue] 0 end-1]although it really should be bytecoded. See Bytecoded K for code that can be adapted.
[EPSJ]: I would suggest using arrays for queues. The access (put/get) is a little slower, but it is always O(1). See example bellow:
namespace eval queue { namespace ensemble create -subcommands {create put get empty size} proc create {queueVar args} { upvar 1 $queueVar queue set queue(in_idx) 0 set queue(out_idx) 0 if {[llength $args]} { put queue $args } } proc put {queueVar args} { upvar 1 $queueVar queue set queue($queue(in_idx)) $args if { [incr queue(in_idx)]==2147483647} { set queue(in_idx) 0} return } proc get {queueVar} { upvar 1 $queueVar queue set result {} if {($queue(out_idx)!=$queue(in_idx))} { set result $queue($queue(out_idx)) unset queue($queue(out_idx)) if { [incr queue(out_idx)]==2147483647} { set queue(out_idx) 0} } return $result } proc empty {queueVar} { upvar 1 $queueVar queue expr {$queue(out_idx)==$queue(in_idx)} } proc size {queueVar} { upvar 1 $queueVar queue set result [expr {$queue(in_idx)-$queue(out_idx)}] expr {($result<0) ? 2147483647 + $result : $result} } } % queue create q % time {queue put q x} 1000000 1.602769 microseconds per iteration % time {queue get q} 1000000 1.604104 microseconds per iteration
See also:
- struct package of the tcllib. It also provides a C implementation.
- Stacks and queues.