当前位置: 首页 > article >正文

【Queue新技法】用双数组实现一个队列 C++

目录

  • 1 常规的队列构建
  • 2 加入一些限制
    • 2-1形式化说明
    • 2-2 优化:平衡队列
  • 附录
    • 0 双数组或双链表实现队列
    • 1 单链表与循环缓冲区实现队列
    • 3 参考资料

1 常规的队列构建

  到火车站办理退票,排队的人构成队列。注意到有两个关键动作:

  1. 入队,即自觉站到队伍的末尾。
  2. 出队,从柜台离开。

  单链表:先建立一个漂亮的柜员姐姐(哨兵或额外的头结点),记忆当前队列的第一个人,再来个尾指针,用于添加下一个人。

  循环数组,也叫循环缓冲区,如图灰色部分就是队列的内容。

  这两种方式容易理解,其中入队、出队都是O(1)也容易实现,代码放在末尾附录中。

2 加入一些限制

  现先引入新情境:考虑一种情况在函数式环境不能用变量来记录末尾,怎么办?

  • 如果只能用链表实现(如Haskell语言),这时候入队必须遍历到末尾,时间复杂度O(N)
  • 如果只能用数组实现(如BASIC语言),入队必须把所有元素往后移动,时间复杂度又是O(N)

  如上图所示,左边是原来的队列,右边是翻转后的队列。然后会出现几个问题:

  1. 在队伍末尾直接插入,是O(N)复杂度,虽然翻转后插入是O(1),但翻转不也是O(N)吗?
  2. 翻转后入队是O(N),那出队呢?再翻转回来吗?不是更麻烦了吗?

  两个问题其实一个纠结在一个队列怎么能来回翻转呢?然而,我们可以用两个链表(或数组结构),一个只管出队,一个只管入队,只有在出队的队列为空,才把入队的队列翻转并放到出队(修改指针O(1)),可以看到本质是延时处理,翻转并不针对每次入队操作,其分摊性能可以降低为常数O(1)。

2-1形式化说明

  说明:记入队的列表为F(Front),出队的队列为R(Rear),那完整的队列 Q ( F , R ) Q(F,R) Q(FR),此外定义两个操作,设列表 X = { x 1 , x 2 , x 3 . . . } X=\{x_1,x_2,x_3...\} X={x1,x2,x3...},则

  • 函数 t a i l ( X ) = { x 2 , x 3 , x 4 . . . } tail(X)=\{x_2,x_3,x_4...\} tail(X)={x2,x3,x4...}即去掉首个元素的剩余部分;
  • 函数 b a l a n c e ( F , R ) = Q ( r e v e r s e ( R ) , ∅ ) : F = ∅ balance(F,R)=Q(reverse(R),\emptyset):F=\emptyset balance(F,R)=Q(reverse(R),):F=,此外当出队队列F不空,什么也不做。

  此时,入队和出队操作定义如下:
p u s h ( Q , x ) = b a l a n c e ( F , x ∪ R ) p o p ( Q ) = b a l a n c e ( t a i l ( F ) , R ) push(Q,x)=balance(F,{x} \cup R) \\ \\[2ex] pop(Q)=balance(tail(F),R) push(Q,x)=balance(F,xR)pop(Q)=balance(tail(F),R)  实现是容易的代码放在附录,现在先点这里平摊分析,证明一下两个链表(或数组)的分摊性能的确是常数级别。只有出队操作可能引发翻转,尽管翻转的总复杂度是O(N),可只有几次,但O(1)出队操作却有N个,即均摊到每次出队操作上只有 O ( 1 ) = O ( N ) / N O(1)=O(N)/N O(1)=O(N)/N
p o p ( Q ) = { O ( N ) , L为空集 1 , 其他 pop(Q) = \begin{cases} O(N), & \text{L为空集} \\ 1, & \text{其他} \end{cases} pop(Q)={O(N),1,L为空集其他

2-2 优化:平衡队列

  注意到,当入队的Rear队列过长时,翻转所消耗的时间很大,表现为在查看队头时,若发现队列为空,需要等好一会儿,才能完成翻转进而得到所需队头数据。
  造成这一困难的原因是入队队列Rear和出队队列Front之间的数量太不平衡。所以,我们加一个限制,来保证Front的长度不小于Rear。修改函数
b a l a n c e ( F , R ) = Q ( r e v e r s e ( R ) , ∅ ) : F . l e n < R . l e n balance(F,R)=Q(reverse(R),\emptyset):F.len<R.len balance(F,R)=Q(reverse(R),):F.len<R.len

  实现时,对链表来说可以用哨兵的key记录当前队列的长度;对数组来说,可以直接采用vector.size()获取长度信息。

附录

  队列的基本的方法说明:

  • 判断是否非空? empty(is_empty)
  • 入队 push 、(append、push_back
  • 出队 pop 、(tail、pop_front
  • 查看头部元素 front (head)

0 双数组或双链表实现队列

#include<iostream>
#include<vector>// 代替数组
#include<algorithm>// 翻转vector
using namespace std;

#define ERROR -1
using Key=int;

//定义节点
struct Node
{
    Key key;
    struct Node *nxt;
    Node(Key k, struct Node *ptr = nullptr) : key(k), nxt(ptr) {}
};
using Nptr = struct Node *;
//定义链表
struct List{
    Nptr sentry;//哨兵节点
    List(){
        sentry=new Node(-1);
    }
};

class dualListQ
{
private:
    List m_front;
    List m_rear;

    int balance();
    Nptr reverse_list(Nptr head);//递归翻转链表
public:
    bool is_empty() const;
    void push_back(const Key &k) ;
    void pop_front();
    Key front();
};
int dualListQ::balance()
{
    if(m_front.sentry->nxt!=nullptr) return 0;
    if(m_rear.sentry->nxt==nullptr){
        cerr<<"balance()-> queue is empty!"<<endl;
        return ERROR;
    }
    m_front.sentry->nxt=reverse_list(m_rear.sentry->nxt);
    m_rear.sentry->nxt=nullptr;
    return 0;
}
//haed 指向哨兵的下一个节点
Nptr dualListQ::reverse_list(Nptr head){
    //递归的出口:空链或只有一个结点,直接返回头指针
    if (head == nullptr || head->nxt == nullptr) 
    {
        return head;
    }
    else
    {
        //一直递归,找到链表中最后一个节点
        Nptr new_head = reverse_list(head->nxt);
        //当逐层退出时,new_head 的指向都不变,一直指向原链表中最后一个节点;
        //递归每退出一层,函数中 head 指针的指向都会发生改变,都指向上一个节点。
        //每退出一层,都需要改变 head->next 节点指针域的指向,同时令 head 所指节点的指针域为 NULL。
        head->nxt->nxt = head;
        head->nxt = nullptr;
        //每一层递归结束,都要将新的头指针返回给上一层。由此,即可保证整个递归过程中,能够一直找得到新链表的表头。
        return new_head;
    }

}

bool dualListQ::is_empty() const
{
    return m_front.sentry->nxt==nullptr&&nullptr==m_rear.sentry->nxt; 
}

void dualListQ::push_back(const Key &k)
{
    Nptr old_=m_rear.sentry->nxt;
    Nptr new_=new Node(k);
    m_rear.sentry->nxt=new_;
    new_->nxt=old_;
    if(ERROR==balance()) return;
}

void dualListQ::pop_front()
{   
    if(ERROR==balance()) return;
    Nptr tmp=m_front.sentry->nxt;
    m_front.sentry->nxt=tmp->nxt;
    delete tmp;
}

Key dualListQ::front()
{
    if(is_empty()){
        cerr<<"front()-> queue is empty!"<<endl;
        return ERROR;
    }
    if(m_front.sentry->nxt==nullptr){
        balance();
    }
    return m_front.sentry->nxt->key;
}



class dualVectorQ
{
private:
    vector<Key> m_front,m_rear;
    int balance();
public:
    bool is_empty() const;
    int push_back(const Key &k) ;
    void pop_front();
    Key front();
};
int dualVectorQ::balance()
{
    if(m_front.empty()){
        if(m_rear.empty()){
            cerr<<"balance()-> queue is empty."<<endl;
            return ERROR;
        }
        reverse(m_rear.begin(),m_rear.end());
        for(auto &k:m_rear){
            m_front.push_back(k);
        }
        m_rear.clear();
    }
    return 0;
}
bool dualVectorQ::is_empty() const
{
    return m_front.size()+m_rear.size()==0;
}

int dualVectorQ::push_back(const Key &k)
{
    m_rear.push_back(k);
    return 0;
}
void dualVectorQ::pop_front(){
    balance();
    m_front.pop_back();// 翻转后,队尾即对头
}
Key dualVectorQ::front(){
    if(is_empty()){
        cerr<<"front()-> Queue is empty..."<<endl;
        return ERROR;
    }
    balance();
    return m_front.back();// 翻转后,队尾即对头
}


#define see(x) cout<<x<<endl
void test(){
    // dualListQ que;//创建空的队列
    dualVectorQ que;
    int arr[]={1,3,5,7,9};
    for (size_t i = 0; i < 5; i++)
    {
        que.push_back(arr[i]);
    }
    see(que.front());//1
    que.pop_front();
    see(que.front());//3
    que.pop_front();
    que.pop_front();
    que.pop_front();
    if(que.is_empty())
        see("queue is empty");//无
    see(que.front());//9
    que.pop_front();
    if(que.is_empty())
        see("queue is empty");//有
    see(que.front());

}

int main(){
    test();
    return 0;
}


1 单链表与循环缓冲区实现队列

#include<iostream>
using namespace std;

#define ERROR -1
using Key=int;
class listQ
{
private:
    struct Node 
    {
        Key key;
        struct Node* nxt;
        Node(Key k,struct Node* ptr=nullptr):key(k),nxt(ptr){}
    };
    using Nptr=struct Node*; 
    Nptr m_sentry=nullptr;//头结点,指向真实的头一个数据
    Nptr m_tail=nullptr;//指向尾节点
public:
    listQ();//创建空队列
    ~listQ();
    bool is_empty() const;
    void push_back(const Key &k) ;
    void pop_front();
    Key front();
};
listQ::listQ()
{
    m_sentry=new Node(-1);
    m_tail=m_sentry;
}

listQ::~listQ()
{
    while(!is_empty()){
        pop_front();
    }

    delete m_sentry;
    cout<<"over   ..."<<endl;
}

bool listQ::is_empty() const
{
    if(m_sentry->nxt==nullptr) return true;
    return false;
}

void listQ::push_back(const Key &k)
{
    Nptr new_node=new Node(k);
    m_tail->nxt=new_node;
    m_tail=new_node;
}

void listQ::pop_front()
{
    Nptr tmp=m_sentry->nxt;
    m_sentry->nxt=tmp->nxt;
    delete tmp;
}

Key listQ::front()
{
    if(is_empty()){
        cerr<<"queue is empty!"<<endl;
        return ERROR;
    }
    return m_sentry->nxt->key;
}


//循环缓冲区
class arrayQ
{
private:
    static const size_t QSIZE=5;
    Key m_buf[100];
    int m_head=0;
    int m_tail=0;//指向队列末尾下一个空位
    int m_length=0;
public:
    bool is_empty() const{ return 0==m_length;}
    int push_back(const Key &k) ;
    void pop_front();
    Key front();

};
void arrayQ::pop_front(){
    --m_length;
    ++m_head;
    m_head -= (m_head < QSIZE) ?  0 : QSIZE;//模拟取余数,因为某些机器取余很慢
}
int arrayQ::push_back(const Key &k){
    if(m_length==QSIZE){
        cerr<<"push-> Queue is full!!"<<endl;
        return ERROR;
    }

    m_buf[m_tail++]=k;
    m_tail -=(m_tail < QSIZE) ?  0 : QSIZE;
    ++m_length;
    return 0;
}
Key arrayQ::front(){
    if(is_empty()){
        cerr<<"front()-> Queue is empty..."<<endl;
        return ERROR;
    }

    return m_buf[m_head];
}


#define see(x) cout<<x<<endl
void test(){
    // listQ que;//创建空的队列
    arrayQ que;
    int arr[]={1,3,5,7,9};
    for (size_t i = 0; i < 5; i++)
    {
        que.push_back(arr[i]);
    }

    // que.push_back(11);
    see(que.front());//1
    que.pop_front();
    see(que.front());//3
    que.pop_front();
    que.pop_front();
    que.pop_front();
    see(que.is_empty());
    see(que.front());//9
    que.pop_front();
    see(que.is_empty());
    see(que.front());

}

int main(){
    test();
    return 0;
}



3 参考资料

刘新宇 《算法新解》


http://www.kler.cn/news/18492.html

相关文章:

  • C++类和对象(上)
  • 华为OD机试真题 Java 实现【猜字谜】【2023Q2】
  • Adobe考试
  • 【MySQL】索引
  • 字节跳动发放年终奖,远超预期~
  • 将sublime中的自定义代码片段snippet 转为vscode可用的代码片段 (cursor可用)
  • Java笔记_17(异常、File)
  • uboot 启动内核代码分析
  • C++结构体分别在:栈空间、堆空间、静态存储区中初始化
  • 【计算机专业漫谈】【计算机系统基础学习笔记】W2-2-1 原码和移码表示
  • vue概述
  • Go数据机构----栈与队列
  • CANoe以太网配置 Network-Based Access Mode
  • 离散化(算法)
  • 卫星下行链路预算模型(未完待续)
  • JavaScript (七) -- JavaScript 事件(需要了解的事件的运用)
  • C++运算符重载
  • 可视化绘图技巧100篇分析篇(二)-生存曲线(LM曲线)(补充篇)
  • EMC VNX登录Unisphere错误 certificate has invalid date问题处理
  • DC-8通关详解
  • orin配置系统
  • api数据接口文档_接口文档示例(以1688平台API接口文档实例演示)
  • HID Relay, 有线键盘转蓝牙项目学习:记一次失败的尝试
  • 密码学:古典密码.
  • 创新驱动 共建生态|鲲鹏开发者峰会2023·GBASE南大通用技术论坛成功举办
  • Docker run命令
  • WebRTC源码目录结构
  • 欧几里得算法,辗转相除法的证明
  • 思科网络交换机配置命令(详细命令总结归纳)
  • 手把手带你进入爬虫的世界