数据结构——队列

344 阅读5分钟

队列(queue)是只允许在一端进行插入操作,在另一端进行删除操作的线性表,允许插入(也称入队、进队)的一端称为队尾(queue-tail),允许删除(也称出队)的一端称为队头(queue-head)。

循环队列

队列的顺序存储结构称为顺序队列(sequential queue)。假设队列有n个元素,顺序队列把队列的所有元素存储在数组的前n个单元。如果把队头元素放在数组下标为0的一端,则入队操作相当于追加,不需要移动元素,其时间性能为O(1);但是出队操作的时间性能为O(n),因为要保证剩下的(n-1)个元素仍然存储在数组的前(n-1)个单元,所有元素都要向前移动一个元素。

如果放宽队列的所有元素都必须存储在数组的前n个单元这一条件,就可以得到一种更为有效的存储方法,即循环队列可以实现入队和出队操作的时间性能都是O(1),因为没有移动任何元素,但是队列的队头和队尾都是活动的,因此,需要设置队头、队尾两个位置变量front和rear,入队时rear加1,出队时front加1。

关于循环队列对于空队列、满队列的判定,它们的条件都为:rear=front,那如何将队空和队满的判定条件加以区分呢?可以浪费一个数组元素空间,在队列还差一个元素达到队满的条件时,此时认为队列已满(即浪费一个数组元素空间),此时队尾的位置和队头的位置正好差1,即队满的条件是:(rear+1)%QueueSize=front。

算法设计有一个重要的原则:时空权衡,一般来说,牺牲空间或者其他替代资源,通常可以减少时间代价。例如,在单链表中的开始结点之前附设一个头结点,使得单链表的插入和删除等操作不用考虑表头的特殊情况;在双链表中,结点设置了指向前驱结点的指针指向后续结点的指针,增加了指针的结构性开销,减少了查找前驱和后驱的时间代价。

循环队列的实现

const int QueueSize = 100;
template <typename DataType>
class CirQueue{
    public:
        CirQueue();                // 构造函数,初始化空队列
        ~CirQueue(){}              // 析构函数
        void EnQueue(DataType x);  // 入队操作,将元素x入队
        DataType DeQueue();        // 出队操作,将队头元素出队
        DataType GetHead();        // 取队头元素(并不删除)
        bool Empty();              // 判断队列是否为空  
    private:
        DataType data[QueueSize];    // 存放队列元素的数组
        int front,rear;              // 游标,队头和队尾指针
};

template <typename DataType>
CirQueue<DataType>::CirQueue(){
    // 只需要front与rear同时指向数组的某一个位置
    front = rear = 0;
}

template <typename DataType>
void CirQueue<DataType>::EnQueue(DataType x){
    if ((rear+1)%QueueSize == front)
        throw "overflow";
    rear = (rear+1)%QueueSize;
    data[rear] = x;
}

template <typename DataType>
DataType CirQueue<DataType>::DeQueue(){
    if (front == rear)
        throw "queue is empty";
    front = (front+1)%QueueSize;
    return data[front];
}

template <typename DataType>
DataType CirQueue<DataType>::GetHead(){
    if (front == rear)
        throw "queue is empty";
    return data[(front+1)%QueueSize];        // 注意不修改队头指针
}

template <typename DataType>
bool CirQueue<DataType>::Empty(){
    return front==rear?true:false;    // 简化为front==rear皆可
}

链队列

队列的链接存储结构称为链队列(linked queue),通常用单链表表示,其结点结构与单链表的结点相同,为了使空队列和非空队列的操作一致,链队列也加上头结点,并设置队头指针指向链队列的头结点,队尾指针指向终端结点。

链队列的实现

template <typename DataType>
struct Node{
    DataType data;
    Node<DataType> *next;
};

template <typename DataType>
class LinkQueue{
    public:
        LinkQueue();                // 初始化空的链队列
        ~LinkQueue();               // 释放链队列的存储空间
        void EnQueue(DataType x);   // 入队操作,将元素x入队
        DataType DeQueue();         // 出队操作,将队头元素出队
        DataType GetHead();         // 取链队列的队头元素(并不删除)
        bool Empty();               // 判断链队列是否为空
    private:
        Node<DataType> *front,*rear;    // 队头和队尾指针
};

template <typename DataType>
LinkQueue<DataType>::LinkQueue(){
    Node<DataType> *s = new Node<DataType>;
    s->next = nullptr;
    front = rear = s;    // 将队头指针和队尾指针都指向头结点s
}

template <typename DataType>
LinkQueue<DataType>::~LinkQueue(){
    while (front != rear){
        Node<DataType> *p = front;
        front = front->next;
        delete p;
    }
    delete front;    // 删除头结点
}

template <typename DataType>
void LinkQueue<DataType>::EnQueue(DataType x){
    Node<DataType> *s = new Node<DataType>;
    s->data = x;
    s->next = nullptr;
    rear->next = s;
    rear = s;
}

template <typename DataType>
DataType LinkQueue<DataType>::DeQueue(){
    if (front == rear)
        throw "queue is empty";
    Node<DataType> *p = front->next;
    DataType x = p->data;
    front->next = p->next;
    if (p->next == nullptr)    
        rear = front;    // 出队前队列长度为1的特殊情况处理
    delete p;

    return x;
}

template <typename DataType>
DataType LinkQueue<DataType>::GetHead(){
    if (front == rear)
        throw "queue is empty";
    return front->next->data;
}

template <typename DataType>
bool LinkQueue<DataType>::Empty(){
    return front==rear?true:false;
}

双端队列

双端队列(double-ended queue)是队列的扩展,如果允许在队列的两端进行插入和删除操作,则称为双端队列。

具体实现可参照如下网址,需要注意的是head与tail关于第一个元素的使用有些许规定区别,自行设计代码时需要加以区分对待。

循环队列和链队列的比较

循环队列和链队列基本操作的时间复杂度均为O(1),因此,可以比较的只有空间性能。初始时循环队列必须确定一个固定的长度,所以有存储元素个数的限制和浪费空间的问题。链队列没有溢出问题,只有当内存没有可用空间时才会出现溢出,但是每个元素都需要一个指针域,从而产生了结构性开销。

作为一般规律,当队列中元素个数变化大时,应该采用链队列,反之,应该采用循环队列。