数据结构STL——golang实现queue

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 {
	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{},
	}
}
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为下标指向的位置即可,另一种情况是没有冗余量了,需要对动态数组进行扩容,此时就需要利用扩容策略。另一种是尾部没有冗余量,但首部仍有冗余量,此时可以将承载元素前移,把首部冗余量”借“过来使用。

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

​ 固定扩容值设为216,翻倍扩容上限也为216。

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 mainimport (	"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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值