数据结构之队列——输出杨辉三角形

定义

队列是一种操做受限的线性表,只容许在一端进行插入,另外一端进行删除。插入的一端称为队尾,删除的一端称为队头,因为这样的限制,因此队列具备先进先出的特性,所以队列也是一种先进先出的线性表。算法

顺序存储

队列的顺序存储结构,除了存储的数组,还须要一个队尾指针(rear),和队头指针(front),初始化队列的时候,rear和front都指向同一个下标0,这shi队为空。数组

  • 当一个元素入队,队尾指针+1
  • 当一个元素出队,队头指针+1
  • 在非空队列中,队头指针始终指向对头元素,而队尾指针始终指向队尾元素的下一个位置

在这种状况下,会出现假溢出现象,由于入队和出队操做中,头,尾指针都只增长,不减小,致使被删除的元素空间永远没法从新利用。即尽管队列中的元素个数远远小于数组的大小,但因为尾指针已经超出数组的上界,致使不能进行入队操做,这种现象称为假溢出ui

数组大小为4,队列操做时,头、尾指针变化过程以下图 spa

循环队列

为了克服假溢出,能够怎么改进呢?很天然的就想到,把新的元素放到空余的空间里,即又回到数组下标0位置处,这样看上去就像一个首尾相接的圆环,这种队列称为循环队列指针

循环队列的出队和入队,仍然是头,尾指针加一,只不过当头,尾指针到达数组上界时,加1操做,又回到了下界0处。code

// i 表明头,尾指针
if i+1 == maxSize {
    i = 0
} else {
    i++
}
复制代码

上述的这种操做,可使用求模运算简化,即i = (i+1) % maxSize,这样就能充分利用数组上的全部空间,除非数组空间被占满,不然不会形成溢出。 来看下循环队列,头尾指针的变化过程。 orm

从中会发现一个问题,当front == rear的时候,有多是队空,也多是队满。为何会出现这个问题呢?

咱们以队列最大容量为4,来分析这个问题:cdn

  • 首先,是以front,rear的相对位置来判断队列的状态,那么front,rear的相对位置有:0,1,2,3四种状况
  • 实际上,队列里的状态有:0(空队列),1个元素,2个元素,3个元素,4个元素(队满)五种状况
  • so,要用4种状况来区分5种状况,很显然,不合实际

怎么解决呢?通常都两种方案:blog

一、使用额外的标志位tag,当入队的时候,把tag设置成1,当出队的时候,把tag设置成0,那么当front==rear时,就能够经过tag的值来判断是空队,仍是满队队列

二、少用一个空间,即数组最大容量为4,但咱们只用3个容量,这样判断空队列仍然是front==rear,而判断队列是否满,则就变成(rear+1)%maxSize == front,则为满。(下面的实例代码,以此方案实现)即以下图

固然,也可使用链式存储的方式来构建队列,若是使用链式,就不存在容量的问题,这样也就不须要判断队满。

主要操做

结构描述

type data interface{}

type Queue struct {
	list    []data
	front   int // 头指针
	rear    int // 尾指针
	maxSize int // 最大容量
}

func New(maxSize int) *Queue {
	q := &Queue{
		list:    make([]data, maxSize+1),
		front:   0,
		rear:    0,
		maxSize: maxSize + 1, // 空余一个容量不使用
	}
	return q
}
复制代码

判满

func (q *Queue) IsFull() bool {
	return (q.rear + 1) % q.maxSize == q.front
}
复制代码

判空

func (q *Queue) IsEmpty() bool {
	return q.front == q.rear
}
复制代码

入队

判断队是否已经满,满就报错,不然入队

func (q *Queue) Enqueue(value data) (bool, error) {
	if q.IsFull() {
		return false, errors.New("队已满")
	}
	q.list[q.rear] = value
	q.rear = (q.rear + 1) % q.maxSize
	return true, nil
}
复制代码

出队

队为空,则报错,不然出队

func (q *Queue) Dequeue() (data, error) {
	if q.IsEmpty() {
		return nil, errors.New("队为空")
	}
	value := q.list[q.front]
	q.list[q.front] = nil
	q.front = (q.front + 1) % q.maxSize
	return value, nil
}
复制代码

获取队头的值

func (q *Queue) GetHead() (data, error) {
	if q.IsEmpty() {
		return nil, errors.New("队为空")
	}
	return q.list[q.front], nil
}
复制代码

应用:输出杨辉三角形

杨辉三角,是二项式系数在三角形中的一种几何排列,以下图

它有两个比较显著的特色:

  • 每行最两端的值为1
  • 每一个数字等于上一行的左右两个数字之和。可用此性质写出整个杨辉三角,即第n+1行的第i个数等于第n行的第i-1个数和第i个数之和,即 C(n+1,i)=C(n,i)+C(n,i-1)。

基于以上的性质,使用程序输入杨辉三角的时候,一种想法就是,利用两个数组,在输出当前行的时候,就计算下一行的值,放到另外一个数组里,两个数组交替使用。

第二种方案,咱们能够利用队列来输出,在空间上能够减小一个数组,在使用队列输出的杨辉三角的时候,有一个小技巧,就是在每行的两端添加两个0,即成以下的形式

0 1 0
 0 1 1 0
0 1 2 1 0 
复制代码

在这个前提下,算法思路(n表明行数):

一、初始化一个队列,将第一列 0 1 0 依次入队;

二、此时每一行的元素个数为n + 2,依次出队并输出该行的每个元素,0出队但不输出;

三、在元素出队的同时,计算下一行对应位置的数值,即出队元素 + 新的队头元素,并把计算获得的值入队

四、当该行的每个元素都输出完了,队列里也就计算好了下一行的元素,此时再把0入队,这个0便是这一行结束的0,也是下一行开始的0

五、重复2,3,4直到n结束。

咱们以 n = 4为例,看看队列里元素的变化:

const maxSize = 1000

func printYangHui(n int) {
	q := Queue.New(maxSize)
	q.Enqueue(0)
	q.Enqueue(1)
	q.Enqueue(0)
	for i := 1; i <= n; i++ {
		formatPrint(n-i) // 格式化输出
		for j := 1; j < i+2; j++ {
			// 第i行,在队列中有i + 2个数字,包括头尾两个0,
			// 0 1 0
			// 0 1 1 0
			// 0 1 2 1 0
			s, _ := q.Dequeue()
			if s != 0 {
				fmt.Printf(" %d", s)
			}
			t, _ := q.GetHead()
			q.Enqueue(s.(int) + t.(int)) // 下一行中的数字就是其左右肩之和
		}
		q.Enqueue(0) // 再把每行的0入队
		fmt.Println()
	}
}

printYangHui(4) // 结果以下图
复制代码

总结

以上的应用只是队列的一个小应用,理论上数据流符合先进先出的规则,均可以考虑使用队列解决问题。好比打印机的打印调度,先进的内容,会被先打印出来,等等。

Thanks!

相关文章
相关标签/搜索