goSTL/blog/数据结构STL——golang实现队列queue.md

10 KiB
Raw Blame History

github仓库存储地址https://github.com/hlccd/goSTL

概述

队列queue是一个封装了动态大小数组的顺序容器。除了可以包含任意类型的元素外更主要是的它满足FIFO的先进先出模式,对于一些排队问题可以考虑使用队列来存储。

对于queue的实现,由于它也是一个线性容器底层依然可以考虑使用动态数组来实现但它和vector仍有一定的不同vector的冗余量主要是在尾部毕竟vector要实现随机读取的话中间和首部不能有空余量而对于queue来说它的添加只在尾部而首部仅仅只做删除所以除了在尾部留有一定的空间做添加之外也可以在首部删除后留有不多的余量以避免多次分配空间

原理

对于一个queue来说它可以在尾部添加元素在首部删除元素。而同vector一样如果每次增删都要重新分配空间将会极大的降低效率所以可以考虑对前后都留有一定的冗余以此来减少分配空间和复制的次数从而减少时间开销。

对于首部的冗余来说,它的存在主要是为了减少删除后就重新分配空间并复制,同时对于尾部添加时,如果尾部冗余不足可以将使用的元素整体前移到首部,即将首部冗余挪到尾部去,从而减少了空间分配的次数。

对于尾部的冗余来说它存在的目的和vector类似仅仅是为了后续元素时更快而已。

和vector一样都是通过设置一定量的冗余来换取操作次数的减少从而提高效率减少时间损耗。

扩容策略

对于queue的动态数组扩容来说由于其添加是线性且连续的即每一次只会增加一个并且每一次都只会在它的尾部不像vector一样可能在数组的任意位置进行添加同时由于首部也有冗余所以它的扩容也需要考虑首部的情况。

queue的扩容主要由三种方案

  1. 利用首部:严格来说其实并没有进行扩容,而是将承载的元素前移到首部,即将首部的冗余平移到最后进行利用。
  2. 固定扩容:固定的去增加一定量的空间,该方案时候在数组较大时添加空间,可以避免直接翻倍导致的冗余过量问题到由于增加量是固定的如果需要一次扩容很多量的话就会比较缓慢。同vector
  3. 翻倍扩容将原有容量直接翻倍该方案适合在数组不太大的适合添加空间可以提高扩容量增加扩容效率但数组太大时使用的话会导致一次性增加太多空间进而造成空间的浪费。同vector

缩容策略

对于queue的数组来说它的缩容不同于扩容由于删除元素只会在首部进行所以缩容其实也只会在首部进行考虑到首部并不会进行添加所以也不需要冗余太多的量进行减缓即可以将设定上限减少一些如(2^10)

  1. 固定缩容:释放一个固定的空间,该方案适合在当前数组较大的时候进行,可以减缓需要缩小的量,当首部冗余超过上限时进行缩容,一次性全部释放即可
  2. 折半缩容:当首部冗余超过了实际承载的元素的数量时,需要对首部进行缩容,同前者一样,不需要考虑首部添加的问题,即采用对首部冗余全部释放的方式。

实现

queue底层同样使用动态数组实现同时由于首部只做删除尾部只做添加而首尾两侧都有一定的冗余所以需要对两侧都进行记录也可以根据尾部下标减首部下标得出实际承载元素的量与此同时需要引入cap以记录实际分配的空间大小也可以根据cap和end计算出尾部冗余量。同时为了解决在高并发情况下的数据不一致问题引入了并发控制锁。

type queue struct {
	data  []interface{} //泛型切片
	begin uint64        //首节点下标
	end   uint64        //尾节点下标
	cap   uint64        //容量
	mutex sync.Mutex    //并发控制锁
}

接口

type queuer interface {
	Iterator() (i *Iterator.Iterator) //返回包含队列中所有元素的迭代器
	Size() (num uint64)               //返回该队列中元素的使用空间大小
	Clear()                           //清空该队列
	Empty() (b bool)                  //判断该队列是否为空
	Push(e interface{})               //将元素e添加到该队列末尾
	Pop() (e interface{})             //将该队列首元素弹出并返回
	Front() (e interface{})           //获取该队列首元素
	Back() (e interface{})            //获取该队列尾元素
}

New

创建一个queue容器并初始化同时返回其指针。

func New() (q *queue) {
	return &queue{
		data:  make([]interface{}, 1, 1),
		begin: 0,
		end:   0,
		cap:   1,
		mutex: sync.Mutex{},
	}
}

Iterator

将队列中承载元素传入迭代器中,不清除队列中的冗余量。返回迭代器指针,用于遍历队列。

func (q *queue) Iterator() (i *Iterator.Iterator) {
   if q == nil {
      q=New()
   }
   q.mutex.Lock()
   tmp:=make([]interface{},q.end-q.begin,q.end-q.begin)
   copy(tmp, q.data[q.begin:q.end])
   i = Iterator.New(&tmp)
   q.mutex.Unlock()
   return i
}

Size

返回queue中当前所包含的元素个数由于其数值必然是非负整数所以选用了uint64

func (q *queue) Size() (num uint64) {
	if q == nil {
		q = New()
	}
	return q.end - q.begin
}

Clear

清空了queue中所承载的所有元素。

func (q *queue) Clear() {
	if q == nil {
		q = New()
	}
	q.mutex.Lock()
	q.data = make([]interface{}, 1, 1)
	q.begin = 0
	q.end = 0
	q.cap = 1
	q.mutex.Unlock()
}

Empty

判断queue中是否为空通过Size()是否为0进行判断。

func (q *queue) Empty() (b bool) {
	if q == nil {
		q = New()
	}
	return q.Size() <= 0
}

Push

以queue为接受器向queue尾部添加一个元素添加元素会出现两种情况第三种是还有冗余量此时直接覆盖以len为下标指向的位置即可另一种情况是没有冗余量了需要对动态数组进行扩容,此时就需要利用扩容策略。另一种是尾部没有冗余量,但首部仍有冗余量,此时可以将承载元素前移,把首部冗余量”借“过来使用。

扩容策略上文已做描述,可返回参考,该实现过程种将两者进行了结合使用,可参考下方注释。

固定扩容值设为2^16翻倍扩容上限也为2^16。

func (q *queue) Push(e interface{}) {
	if q == nil {
		q = New()
	}
	q.mutex.Lock()
	if q.end < q.cap {
		//不需要扩容
		q.data[q.end] = e
	} else {
		//需要扩容
		if q.begin > 0 {
			//首部有冗余,整体前移
			for i := uint64(0); i < q.end-q.begin; i++ {
				q.data[i] = q.data[i+q.begin]
			}
			q.end -= q.begin
			q.begin = 0
		} else {
			//冗余不足,需要扩容
			if q.cap <= 65536 {
				//容量翻倍
				if q.cap == 0 {
					q.cap = 1
				}
				q.cap *= 2
			} else {
				//容量增加2^16
				q.cap += 2 ^ 16
			}
			//复制扩容前的元素
			tmp := make([]interface{}, q.cap, q.cap)
			copy(tmp, q.data)
			q.data = tmp
		}
		q.data[q.end] = e
	}
	q.end++
	q.mutex.Unlock()
}

Pop

以queue队列容器做接收者弹出容器首部元素,同时begin++即可,若容器为空,则不进行弹出,当弹出元素后,可能进行缩容由于首部不会进行添加所以不需要太多的冗余即将首部冗余上限设为2^10固定缩容和折半缩容都参考此值执行具体缩容策略可参考上文介绍。

func (q *queue) Pop() (e interface{}) {
	if q == nil {
		q = New()
		return nil
	}
	if q.Empty() {
		q.Clear()
		return nil
	}
	q.mutex.Lock()
	e = q.data[q.begin]
	q.begin++
	if q.begin >= 1024 || q.begin*2>q.end {
		//首部冗余超过2^10或首部冗余超过实际使用
		q.cap -= q.begin
		q.end -= q.begin
		tmp := make([]interface{}, q.cap, q.cap)
		copy(tmp, q.data[q.begin:])
		q.data = tmp
		q.begin=0
	}
	q.mutex.Unlock()
	return e
}

Front

以queue为接受器返回vector所承载的元素中位于首部的元素如果queue为nil或者元素数组为nil或为空则返回nil。考虑到仅仅只是读取元素故不对该过程进行加锁操作。

func (q *queue) Front() (e interface{}) {
	if q == nil {
		q=New()
		return nil
	}
	if q.Empty() {
		q.Clear()
		return nil
	}
	return q.data[q.begin]
}

Back

以queue为接受器返回queue所承载的元素中位于尾部的元素如果queue为nil或者元素数组为nil或为空则返回nil。考虑到仅仅只是读取元素故不对该过程进行加锁操作。

func (q *queue) Back() (e interface{}) {
	if q == nil {
		q=New()
		return nil
	}
	if q.Empty() {
		q.Clear()
		return nil
	}
	return q.data[q.end-1]
}

使用示例

package main

import (
	"fmt"
	"github.com/hlccd/goSTL/data_structure/queue"
	"sync"
)

func main() {
	q := queue.New()
	wg := sync.WaitGroup{}
	//随机插入队列中
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go func(num int) {
			fmt.Println(num)
			q.Push(num)
			wg.Done()
		}(i)
	}
	wg.Wait()
	fmt.Println("输出首部:", q.Front())
	fmt.Println("输出尾部:", q.Back())
	fmt.Println("弹出并输出前4个:")
	for i := uint64(0); i < q.Size()-1; i++ {
		fmt.Println(q.Pop())
	}
	//在尾部再添加4个,从10开始以做区分
	for i := 10; i < 14; i++ {
		q.Push(i)
	}
	fmt.Println("从头输出全部:")
	for ;!q.Empty();{
		fmt.Println(q.Pop())
	}
}

注:由于过程中的增删过程是并发执行的,所以其结果和下方示例并不完全相同

0 7 1 2 3 4 5 6 输出首部: 0 输出尾部: 6 弹出并输出前4个: 0 7 1 2 从头输出全部: 3 4 5 6 10 11 12 13