Go数据结构与算法—队列

Posted 小圣.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go数据结构与算法—队列相关的知识,希望对你有一定的参考价值。

介绍

队列(queue),是一种特殊的线性表。队列只允许在一端插入数据,另一端取出数据,并且队列中的数据遵循先进先出(First In First Our, FIFO)的规则。其中取出数据的一端称为队头,插入数据的一端称为队尾。

队列可以分为:顺序队列、循环队列、链式队列。

顺序队列

type Queue struct {
	MaxSize int
	Front   int
	Rear    int
	Element []int
}

// 初始化队列
func (q *Queue) initQueue(maxSize int) {
	q.MaxSize = maxSize
	q.Element = make([]int, maxSize)
	q.Front = -1
	q.Rear = -1
}

// 判断队列是否为空
func (q *Queue) isEmpty() bool {
	flag := false
	if q.Front == q.Rear {
		flag = true
	}
	return flag
}

// 向队列中添加一个元素
func (q *Queue) add(data int) (err error) {
	// 判断满了
	if q.Rear == q.MaxSize-1 {
		err = errors.New("队列已满,无法添加!")
		return err
	}
	q.Rear++
	q.Element[q.Rear] = data
	return
}

// 获取队列的一个元素
func (q *Queue) poll() (data int, err error) {
	if q.isEmpty() {
		err = errors.New("the queue is empty")
		return
	}
	q.Front++
	data = q.Element[q.Front]
	return data, err
}

// 查看队列的所有元素
func (q *Queue) list() {
	for i := q.Front + 1; i < len(q.Element)-1; i++ {
		fmt.Printf("element[%d]:%d\\n", i, q.Element[i])
	}
}
func main() {
	queue := Queue{}
	var key int
	var value int
	var size int
	for {

		fmt.Println("0. 初始化队列")
		fmt.Println("1. 添加数据到队列")
		fmt.Println("2. 从队列中获取数据")
		fmt.Println("3. 显示队列数据")
		fmt.Println("4. 退出程序")
		fmt.Println("请选择(0-4):")
		fmt.Scanf("%d\\n", &key)
		switch key {
		case 0:
			fmt.Println("请输入初始化队列的容量")
			fmt.Scanf("%d\\n", &size)
			queue.initQueue(size)
		case 1:
			fmt.Println("请输入要添加的数据:")
			fmt.Scanf("%d\\n", &value)
			err := queue.add(value)
			if err != nil {
				fmt.Println(err)
				return
			}
		case 2:
			data, err := queue.poll()
			if err != nil {
				fmt.Println(err)
				return
			}
			fmt.Printf("从队列中取出的数据为: %d\\n", data)
		case 3:
			queue.list()
		case 4:
			os.Exit(0)
		}
	}
}

顺序队列会产生假溢出的现象,因为顺序队列的存储单元没有重复使用的机制。所以顺序队列的空间利用率也不是太高。解决的办法就是将顺序队列设计成循环结构。

循环队列

循环队列在逻辑上将队列的首尾进行相连。这样,被取出数据的位置在下一轮循环中可以继续使用,提高了队列的空间利用率。

以下几点需要注意:

  1. 队头和队尾下标都是从0开始的,为了循环使用,下标不适合像顺序队列那样从-1的开始。
  2. 当队头和队尾相等时,队列是空的。
  3. 当(rear + 1) % maxSize = front时队列满了。加一是因为队尾和队头空了一个位置。对maxSize取模是为了循环。其实用取模实现循环在很多地方都有应用。
  4. 队列的元素的个数为:(rear+maxSize-front) % maxSize。

    举个例子:队头为3,队尾为1:最大长度为5
    (rear+maxSize-front) % maxSize=(1+5-3)%5=3

  5. 队头和队尾之间空了一个位置,因为,队尾的指针是指向下一个未被使用的位置。如果不空一个位置,那么就会发生队尾指针指向队头指针的情况。就和注意点2冲突了
type CircleQueue struct {
	MaxSize int
	Front   int
	Rear    int
	Element []int
}

// 初始化队列
func (cq *CircleQueue) initCircleQueue(maxSize int) {
	cq.MaxSize = maxSize
	cq.Element = make([]int, maxSize)
	cq.Front = 0
	cq.Rear = 0
}

// 判断队列是否为空
func (cq *CircleQueue) isEmpty() bool {
	flag := false
	if cq.Front == cq.Rear {
		flag = true
	}
	return flag
}

// 判断队列是否已满
func (cq *CircleQueue) isFull() bool {
	flag := false
	if (cq.Rear+1)%cq.MaxSize == cq.Front {
		flag = true
	}
	return flag
}

// 插入一个数据
func (cq *CircleQueue) add(data int) (err error) {
	if cq.isFull() {
		err = errors.New("the queue is full")
		return
	}
	cq.Element[cq.Rear] = data
	cq.Rear = (cq.Rear + 1) % cq.MaxSize
	return
}

// 取出一个数据
func (cq *CircleQueue) poll() (data int, err error) {
	if cq.isEmpty() {
		err = errors.New("the queue is empty")
		return
	}
	data = cq.Element[cq.Front]
	cq.Front = (cq.Front + 1) % cq.MaxSize
	return data, err
}

// 显示队列中所有的值
func (cq *CircleQueue) list() {
	if cq.isEmpty() {
		fmt.Println("the queue is empty")
		return
	}
	// 因为队列的头不能改变,所以设置一个辅助遍历
	tempFront := cq.Front
	for i := 0; i < (cq.Rear+cq.MaxSize-cq.Front)%cq.MaxSize; i++ {
		fmt.Printf("arr[%d]=%d\\n", tempFront, cq.Element[tempFront])
		tempFront = (tempFront + 1) % cq.MaxSize
	}
}

链式队列

链式队列和循环队列的比较:

  • 时间上:都是O(1)
  • 空间上:循环队列是事先申请好空间,使用期间不释放。而对于链式队列,每次申请和释放结点也都会花费一些时间,但是空间使用上更灵活一些。
  • 在可以确定队列长度最大值的情况下优先使用循环队列,否则优先使用链式队列。
// 队列节点
type Node struct {
	element int
	next    *Node
}

// 队列链表
type LinkedQueue struct {
	Front  *Node
	Rear   *Node
	length int
}

// 初始化队列
func (lq *LinkedQueue) initLinkedQueue() {
	lq.Front = nil
	lq.Rear = nil
	lq.length = 0
	fmt.Printf("初始化成功!\\n")
}

// 判断队列是否为空
func (lq *LinkedQueue) isEmpty() bool {
	flag := false
	if lq.length == 0 {
		flag = true
	}
	return flag
}

// 入队
func (lq *LinkedQueue) add(data int) {

	// 构造新的节点
	node := Node{element: data}

	// 判断是否为第一次插入
	if lq.isEmpty() {
		lq.Front = &node
		lq.Rear = &node
	} else {
		lq.Rear.next = &node
		lq.Rear = &node
	}
	lq.length++
}

// 出队
func (lq *LinkedQueue) poll() (data int, err error) {
	if lq.isEmpty() {
		err = errors.New("the quenen is empty")
		return
	}

	// 取出节点的数据
	data = lq.Front.element

	// 处理剩下最后一个节点的情况
	if lq.length == 1 {
		lq.Front = nil
		lq.Rear = nil
		lq.length--
		return
	}

	lq.Front = lq.Front.next
	lq.length--

	return data, err
}

// 查看队列的所有元素
func (lq *LinkedQueue) list() {
	if lq.isEmpty() {
		fmt.Println("the queue is empty")
		return
	}
	// 因为队列头不能变,所有用临时变量代替一下
	tempFront := lq.Front
	for i := 0; i < lq.length; i++ {
		fmt.Println(tempFront.element)
		tempFront = tempFront.next
	}
}

最后

完整的代码在这里:https://github.com/bigzoro/go_algorithm/tree/main/queue

以上是关于Go数据结构与算法—队列的主要内容,如果未能解决你的问题,请参考以下文章

java数据结构与算法:单向队列与环形队列详解(图片+代码)

go语言数据结构 环形队列

栈与队列:循环队列算法+可执行代码

Go-Golang学习总结笔记

栈与队列:链队列算法+可执行代码

数据结构与算法--队列