数据结构栈和队列

目录

3.1 栈

3.1.1 基本概念

3.1.2 栈的顺序存储结构

3.1.3 栈的链式存储结构 

3.2 队列

3.2.1 基本概念

3.2.2 队列的顺序存储

3.2.3 队列的链式存储

3.3 栈和队列的应用

3.3.1 栈的括号匹配问题

3.3.2 栈的表达式求值

3.3.3 栈与递归

3.3.4 队列与层次遍历

3.3.5 队列与计算机系统


重点掌握栈的出入过程,出栈序列的合法性以及队列的操作和特征

3.1 栈

3.1.1 基本概念

·Def:栈是只允许在一端进行插入或删除操作的线性表

·栈顶Top:又称为表尾,进行插入删除操作的一端

·栈底Bottom:又称表头,固定端,不允许插入删除操作的一端

·特点:只能在栈顶进行运算,访问结点按后进先出last in first out原则

·逻辑结构:与线性表相同,仍为一对一的关系

·基本操作:

InitStack(&S):初始化一个空栈S

StackEmpty(S):判断一个栈是否为空,若栈S为空则返回true,否则返回false

Push(&S,x):进栈,若栈s未满,则将x加入使之成为新栈顶

Pop(&S,&x):出栈,若栈s非空,则弹出栈顶元素,并用x返回

GetTop(S,&x):读栈顶元素,若栈s非空,则用x返回栈顶元素

DestroyStack(&S):销毁栈,并释放栈s占用的存储空间(“ &”表示引用调用)

3.1.2 栈的顺序存储结构

·顺序栈:采用顺序存储的栈称为顺序栈,与一般线性表顺序存储结构完全相同,利用一组地址连续的存储单元依次存放自栈底到栈顶的元素,用top指针指向栈顶(或栈顶元素之上的下标地址)

·特点:使用数组作为顺序栈简单,方便,但是数组大小固定易产生溢出(上溢:栈满继续push;下溢:栈空还要pop,下溢可以作为一种结束条件使用)

·顺序栈的实现

1)初始化:初始化栈顶指针S.top=-1

2)判栈空:S.top=-1时栈为空,否则不空

3)进栈:栈不满时,栈顶指针先加1,再送值到栈顶

bool Push (SqStack &S, ElemType x) {
    if (S. tope=MaxSize-1)         //栈满, 报错
        return false;
    S.data[++S.top]=x;             //指针先加1,再入栈
    return true ;
}

4)出栈:栈非空时,先取栈顶元素值,再将栈顶指针减1

bool Pop (SqStack &S, ElemType &x) {
    if(S. top==-1)              //栈空,报错
      return false;
    x=S.data[S.top--];          //先出栈,指针再减1
    return true;
}

5)读栈顶元素

bool GetTop (SqStack S, ElemType &x) {
    if(S. top==-1)                //栈空,报错
      return false;
    x=S .data[S.top];             //x记录栈顶元素
    return true ;
}

·共享栈:利用栈底位置相对不变,可让两个顺序栈共享一个一维数组,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸

两个栈的栈顶指针都指向栈顶元素,top1=-1 时1号栈为空,top2=Maxsize时 2号栈为空;仅当两个栈顶指针相邻(top2-top1=1)时,判断为栈满。当1号栈进栈时top1先加1再赋值,2号栈进栈时top2先减1再赋值;出栈时则刚好相反。

共享栈是为了更有效地利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢。其存取数据的时间复杂度均为0(1),所以对存取效率没有什么影响。 

3.1.3 栈的链式存储结构 

·链栈:采用链式存储的栈称为链栈,通常采用单链表实现,且所有操作都是在表头进行的,因此链栈没有头结点,头指针直接指向栈顶元素

·特点:链栈便于多个栈共享存储空间提高了效率,且不存在栈满上溢的情况,便于结点的删除插入操作;链栈的操作与链表类似,出栈入栈都在表头进行

·链栈的基本操作

1)初始化:构造一个空栈,栈顶指针置为空

2)判空:Lhead=NULL?

3)入栈

4)出栈

5)取栈顶元素:直接return Lhead->data

【顺序栈与链栈】:顺序栈与链栈在时间复杂度上一样均为O(1);对于空间性能顺序栈需要事先确定一个固定的长度,可能会存在内存空间浪费的问题,但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些内存开销,但对于栈的长度无限制。所以如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,使用顺序栈会更好一些。

3.2 队列

3.2.1 基本概念

·Def:队列是只允许在一端(队尾)进行插入,而在另一端进行删除(队头)操作的线性表(想想排队做核酸的队列),即一种先进先出的线性表

·队列基本操作:

InitQueue(&Q):初始化队列,构造一个空队列Q

QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false

EnQueue(&Q,x):入队,若队列Q未满,将x加入,使之成为新的队尾

DeQueue (&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回

GetHead(Q, &x):读队头元素,若队列e非空,则将队头元素赋值给x

【注】:不可以随便读取栈或队列中间的某个数据

3.2.2 队列的顺序存储

·Def:队列的顺序存储是指分配一块连续的存储单元存放队列中的元素,并设两个指针:队头指针front指向队头元素以及队尾指针rear指向队尾元素的下一个位置(不同教材对front和rear的定义可能不同,可以让rear指向队尾元素、front 指向队头元素)

·顺序存储描述类型:

#define MaxSize 50         //定义队列中元素的最大个数
typedef struct{
ElemType data [MaxSize] ; //存放队列元素
int front, rear;          //队头指针和队尾指针
} SqQueue;

·队列的操作:

初始状态(队空): Q. front==Q. rear==0

进队操作:队不满时,先送值到队尾元素,再将队尾指针加1

出队操作:队不空时,先取队头元素值,再将队头指针加1

【注】:当rear==maxQsize时,发生的真溢出和假溢出

·循环队列:针对上述的真假溢出情况,解决方法是后面满了再从头开始,将上图的rear指针指向0的位置,这样头尾相接的循环,即把存储队列元素的表从逻辑上视为一个环,称为循环队列,也可抽象成一个圆形队列

·循环队列的实现:利用模运算,实现rear指针指向0,即插入元素时Q.rear = (Q.rear+1) % MAXQSIZE,这样rear指针就能从最后一个位置移动到第一个位置;删除元素时front指针同理,Q.front = (Q.front+1) % MAXQSIZE

·循环队列判空判满:对下图,此时队空队满都是rear=front,那如何判断此时到底是空还是满呢?

 1)设置一个标志flag,当rear=front且flag=0时队为空;rear=front且flag=1时队满

2)另设一个变量,记录队中的元素个数

3)少用一个元素空间。由于rear可能比front大也可能小,可能相差一个位置,也可能相差一圈,即下图两种情况,所以为了解决rear和front的大小问题,队满的条件就为 (rear+1) % MAXQSIZE=front;队空条件仍为rear=front

综上还可以计算出队列中的元素个数为(rear-front+ MAXQSIZE)% MAXQSIZE,有了以上分析,实现循环队列的基本思想就有了

·循环队列的基本操作

1)初始化:初始化队首队尾指针Q.rear =Q.front=0

2)判队空:Q.rear =Q.front

3)入队

bool EnQueue (SqQueue &Q,ElemType x) {
    if((Q. rear+1) %MaxSize==Q. front) return false;  //队满则报错
    Q.data[Q. rear]=x;
    Q. rear= (Q. rear+1) MaxSize;                    //队尾指针加1取模
    return true;
}

4)出队

bool DeQueue (SqQueue &Q, ElemType &X) {
    if(Q. rear==Q. front) return false;         //队空则报错
    x=Q. data[Q. front] ;
    Q. front= (Q. front+1)%MaxSize;            //队头指针加1取模
    return true;
}

3.2.3 队列的链式存储

·Def:队列的链式存储称为链队列,是一个同时带有队头指针和队尾指针的单链表,队头指针指向队列头结点,队尾指针指向终端结点,队空时front和rear都指向头结点

·链队列的基本操作

1)初始化:建立头结点,置队头指针为空Q.front->next=NULL

2)判空:Q.front=Q.rear

3)入队:将新结点插入到链尾

void EnQueue (LinkQueue &Q, ElemType x) {
    LinkNode *s= (LinkNode*) malloc (sizeof (LinkNode)) ;
    s->data=x; s->next=NULL;            //创建新结点,插入到链尾
    Q. rear->next=s;
    Q. rear=s;
}

 4)出队:删除链表第一个结点

bool DeQueue (LinkQueue &Q; ElemType &x) {
    if (Q. front--Q.rear) return false;         //空队
    LinkNode *p=Q. front->next;
    x=p->data;
    Q. front->next=p->next;
    if (Q. rear==p)
      Q. rear=Q. front;                //若原队列中只有一个结点,删除后变空
    free(p) ;
    return true;
}

3.2.4 双端队列

·Def:双端队列指允许两端都可以进行入队和出队操作的队列,其逻辑结构仍为线性结构

·输出受限的双端队列:一端允许入队和出队,但另一端只允许入队的双端(两入一出),针对两入一出的双端队列,在进行入出队顺序时抓住“两边进依次出”的准则

·输入受限的双端队列:一端允许入队和出队,但另一端只允许出队的双端(两出一入),针对两出一入的双端队列,在进行入出队顺序时抓住“依次进两边出出”的准则

3.3 栈和队列的应用

3.3.1 栈的括号匹配问题

·基本思想:依次扫描所有字符,遇到左括号就入栈,遇到右括号就将栈顶的左括号出栈进行匹配检查,此时出现以下四种情况:

1)如果匹配成功,则进行下一轮匹配,直到全部括号匹配成功

2)若有括号匹配不成功,则说明这些括号不合法匹配将会失败

3)若有右括号匹配时,栈已空,此时右括号单身,括号匹配失败

4)当处理完所有右括号是,此时栈中还有括号,即左括号单身,同样匹配失败

·算法实现:

3.3.2 栈的表达式求值

·三种算数表达式

1)中缀表达式:运算符在两个操作数中间(a+b-c*d)

2)前缀表达式:运算符在两个操作数前面(-+ab*cd)

3)后缀表达式:运算符在两个操作数后面(ab+cd*-)

·表达式间的转换

1)中缀→后缀

①确定表达式中各个运算符的运算顺序(运算顺序不唯一,所对应的表达式也不唯一)

②依顺序选择运算符,按照各种表达式的顺序组成新的操作数

③若还有运算符未处理,继续执行②

【注】:转换过程中,最好遵循左优先的原则,保证手算与计算的一致,即只要左边的运算符可以先计算,则优先算左边的

栈实现:初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右处理各个元素可能遇到三种情况:

①遇到操作数。直接加入后缀表达式。

②遇到界限符。遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式中,直到弹出“(” 为止。注意:“(” 不加入后缀表达式。

③遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式中,若碰到“(”或栈空则停止。之后再把当前运算符入栈。

2)中缀→前缀

方法与1)同理,但注意此时遵循的为右优先原则,即只要右边的运算符能先计算就优先算右边的

·表达式的计算及实现

1)后缀表达式:从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应的运算,并合并成一个操作数

栈实现:

①从左往右扫描下一个元素,直到处理完所有元素

②若扫描到操作数则压入栈,并回到①;否则执行③

③若扫描到运算,则弹出两个栈顶元素,执行相应运算运算结果压回栈顶,回到①

2)前缀表达式:从右往左扫描,每遇到一个运算符,就让运算后面最近的两个操作数执行对应的运算,并合并成一个操作数

栈实现:对于前缀表达式,只需要在①过程中从右往左扫描即可

3)中缀表达式:

栈实现:中缀转为后缀,再用后缀表达式求值

初始化两个栈,操作数栈和运算符栈

若扫描到操作数,压入操作数栈

若扫描到运算符或界限符,则按照“中缀转后缀”的逻辑压入运算符栈(期间也会弹出运算符,每弹出一个运算符时,就要再弹出两个操作数栈的栈顶元素执行相应运算,运算结果再压回操作数栈)

3.3.3 栈与递归

·函数调用:在函数调用过程中,最后被调用的函数总是最先执行,用栈来实现的就是最后存入栈的最先被调用,在这个过程中,栈会存储函数调用返回地址、实参和局部变量

·递归调用:递归调用时,函数调用栈可称为递归工作栈,每进入一层递归,就将递归调用所需的信息压入栈顶,每退出一层递归,就从栈顶弹出相应信息

·缺点:效率低,且太多层递归会导致栈溢出;也可能会包含很多重复计算

3.3.4 队列与层次遍历

【二叉树的层次遍历】:

【图的广度优先遍历】:同理,略

3.3.5 队列与计算机系统

多个进程争抢使用有限的系统资源时,先来先服务是一种常用的方法,而这种方法恰好可以用队列实现

·CPU资源的分配:就绪进程队列,进程按先来后到的顺序依次等待CPU

·打印数据缓冲区:这里的缓冲区就可用队列实现,存放要准备打印的数据,这样可以缓解主机与打印机速度不匹配的问题

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值