数据结构学习笔记(第三章:栈和队列)

3.1 栈的基本概念

栈的定义

  • 首先栈本身也是一种线性表
  • 但是这种线性表规定了只能在某一端进行插入和删除操作

栈的特点

  • 拥有栈顶和栈底
  • 栈底是固定的,是不允许进行插入和删除的那一端
  • 栈顶是变化的,是允许插入和删除的哪一端
  • 栈是一种后进先出的线性表(也叫LIFO结构)\mathrm{C}_n^r

n个不同元素进栈,出栈不同排列的个数为:
(Catalan公式) 1 n + 1 C 2 n n \frac{1}{n+1}\mathrm{C}_{2n}^{n} n+11C2nn

3.2 栈的顺序存储结构

因为本身栈也是线性表的一种,所以当然也会有两种存储结构。

顺序栈的实现

规定:我们的栈顶指针永远指向栈顶元素的下一个位置上

#include <iostream>    //C++头文件格式,如果需要可替换成C语言的头文件
using namespace std;

#define stack_init_size 100    //初始容量
#define stack_increment 10    //线性表存储空间
#define error -1
//因为C语言中没有true和false关键字,虽然C++里有但是这里还是额外定义一下
#define FALSE 0
#define TRUE 1
typedef int elemtype;    //定义数据元素类型,这样做的好处是想要修改类型时只需修改这句话就可以了
typedef int status;    //因为C语言编译器中一般不存在bool,所以这里的定义相当于C++的bool类型,代表函数返回的状态

typedef struct {
    elemtype *base; //用来申请连续的空间的指针,该空间存放栈的数据
    elemtype *top;  //栈顶指针
    int stacksize;  //记录当前栈的空间大小,用来方便以后空间不足时申请更多的空间
}sqstack;

status init_stack(sqstack &s){
    //初始化一个空栈
    s.base=(elemtype *)malloc(stack_init_size * sizeof(elemtype));//申请一段连续的空间
    if(!s.base) return(FALSE);//申请空间失败弹出错误信息
    s.top=s.base;
    s.stacksize=stack_init_size;
    return TRUE;
}

status get_top(sqstack &s,elemtype &e){
    //若栈不为空,则用e返回s的栈顶元素,并返回TRUE,否则返回FALSE
    if (s.top==s.base) {
        return FALSE;
    }
    e=*(s.top-1);//因为是连续的空间,所以栈顶指针所指向的数据地址-1即可得到我们的栈顶元素所在的地方
    return TRUE;
}

status push(sqstack &s,elemtype e){
    //插入元素e进入栈中,成为新的栈顶元素
    if(s.top-s.base>=s.stacksize){
        //s.top-s.bace即是栈现在已经存放有数据的空间量
        //如果现在栈申请的空间已经不够继续存放数据了,就要重新申请更大的连续空间了
        s.base=(elemtype*)realloc(s.base, (s.stacksize+stack_increment)*sizeof(elemtype));
        if (!s.base) {
            return FALSE;
        }
        s.top=s.base+s.stacksize;   //因为重新申请更大空间后,栈的地址发生了改变,所以栈顶指针所指向的地址也要做相应的更改
        s.stacksize+=stack_increment;
    }
    *s.top++=e; //先用后++
    return TRUE;
}

status pop(sqstack &s,elemtype &e){
    //如果栈不空,就删除s的栈顶元素,并用e返回栈顶元素的值
    if (s.top==s.base) {
        return FALSE;
    }
    e=*(--s.top);
    return TRUE;
}

int main() {
    sqstack s;
    init_stack(s);
    
    return 0;
}

共享栈的实现

共享栈:指的是两个顺序栈共享同一个连续空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。
在这里插入图片描述
共享栈能让存储空间的利用率提高。其存储数据的时间复杂度均为O(1),所以对存取效率没影响。

#include <iostream>    //C++头文件格式,如果需要可替换成C语言的头文件
using namespace std;

#define stack_init_size 100    //初始容量
#define error -1
//因为C语言中没有true和false关键字,虽然C++里有但是这里还是额外定义一下
#define FALSE 0
#define TRUE 1
typedef int elemtype;    //定义数据元素类型,这样做的好处是想要修改类型时只需修改这句话就可以了
typedef int status;    //因为C语言编译器中一般不存在bool,所以这里的定义相当于C++的bool类型,代表函数返回的状态

typedef struct {
    elemtype *base;//用来申请数据空间的指针
    elemtype *top[2];//两个共享栈的栈顶指针
    int stacksize;
} sharestack;

status init_stack(sharestack &s){
    //初始化一个空栈
    s.base=(elemtype *)malloc(stack_init_size * sizeof(elemtype));//申请一段连续的空间
    if(!s.base) return(FALSE);//申请空间失败弹出错误信息
    s.top[0]=s.base;
    s.stacksize=stack_init_size;
    s.top[1]=s.base+s.stacksize-1;
    return TRUE;
}

status get_top(sharestack &s,elemtype &e,int number){
    //number是栈的编号,0号栈代表栈底在空间开头的栈,1号栈代表栈底在空间结尾的栈
    //若栈不为空,则用e返回s的栈顶元素,并返回TRUE,否则返回FALSE
    switch (number) {
        case 0:
            if (s.top[number]==s.base) {
                //0号栈是空的
                return FALSE;
            }
            e=*(s.top[number]-1);
            break;
        case 1:
            if (s.top[number]==s.base+s.stacksize-1) {
                //1号栈是空的
                return FALSE;
            }
            e=*(s.top[number]+1);
            break;
        default:
            printf("%s","编号错误,无法执行函数");
            return FALSE;
            break;
    }
    return TRUE;
}

status push(sharestack &s,elemtype e,int number){
    //插入元素e进入栈中,成为新的栈顶元素
    if(s.top[0]-s.top[1]==1){
        //在这里注意我们的栈顶指针规定指向的是下一个空间,所以栈满的时候s[0]-s[1]=1
        //如果现在栈申请的空间已经不够继续存放数据了,就要重新申请更大的连续空间了
        //这里要注意不能像正常的顺序栈一样,直接用realloc增加空间,因为1号栈的数值不是简单的复制
        //搬运时应该注意1号栈的栈底与多出的空间距离,将1号栈的所有数值集体挪动才行,在这里不再贴出代码
        printf("%s","申请的栈空间不足");
        return FALSE;
    }
    switch (number) {
        case 0:
            *(s.top[number]++)=e; //先用后++
            break;
        case 1:
            *(s.top[number]--)=e;
            break;
        default:
            printf("%s","编号错误,无法执行函数");
            return FALSE;
            break;
    }
    return TRUE;
}

status pop(sharestack &s,elemtype &e,int number){
    //如果栈不空,就删除s的栈顶元素,并用e返回栈顶元素的值
    switch (number) {
        case 0:
            if (s.top[number]==s.base) {
                //0号栈是空的
                return FALSE;
            }
            e=*(--s.top[number]);
            
            break;
        case 1:
            if (s.top[number]==s.base+s.stacksize-1) {
                //1号栈是空的
                return FALSE;
            }
            e=*(++s.top[number]);
            break;
        default:
            printf("%s","编号错误,无法执行函数");
            return FALSE;
            break;
    }
    return TRUE;
}

status stack_empty(sharestack s,int number){
    //若栈S为空栈,返回TRUE,否则返回FALSE
    switch (number) {
        case 0:
            if (s.top[number]==s.base) {
                //0号栈是空的
                return TRUE;
            }
            break;
        case 1:
            if (s.top[number]==s.base+s.stacksize-1) {
                //1号栈是空的
                return TRUE;
            }
            break;
        default:
            printf("%s","编号错误,无法执行函数");
            return FALSE;
            break;
    }
    return FALSE;
}

int main() {
    sharestack s;
    init_stack(s);
    return 0;
}

3.3 栈的链式存储结构

链栈的实现

注意:因为栈的后进先出的特性,所以为了方便使用链栈和链表的起点指针有一点点不一样。

为了方便顺序读取数据,链表中的起点指针需要一直待在头结点。

而由于栈读取一般只能读栈顶,所以考虑到读取效率,链表的起点指针应该作为栈顶指针使用,也就是需要随着栈顶的位置变化而变化。
在这里插入图片描述

#include <iostream>    //C++头文件格式,如果需要可替换成C语言的头文件
using namespace std;
#include <string.h>
#include <cstring>
#include <string>

#define stack_init_size 100    //初始容量
#define stack_increment 10    //线性表存储空间
#define error -1
//因为C语言中没有true和false关键字,虽然C++里有但是这里还是额外定义一下
#define FALSE 0
#define TRUE 1
typedef char elemtype;    //定义数据元素类型,这样做的好处是想要修改类型时只需修改这句话就可以了
typedef int status;    //因为C语言编译器中一般不存在bool,所以这里的定义相当于C++的bool类型,代表函数返回的状态

typedef struct stacknode{
    elemtype data;//结点数据
    struct stacknode *next;//记录下个结点地址的指针
} stacknode,*linkstack;

status init_stack(linkstack &s){
    s=(linkstack)malloc(sizeof(stacknode));//申请头结点
    s->next=NULL;
    return TRUE;
}

status get_top(linkstack &s,elemtype &e){
    //若栈不为空,则用e返回s的栈顶元素,并返回TRUE,否则返回FALSE
    if (s->next==NULL) {
        //s里面没有存放如何数据
        return FALSE;
    }
    //注意i最好跳过头结点
    e=s->data;
    return TRUE;
}
status push(linkstack &s,elemtype e){
    //插入元素e进入栈中,成为新的栈顶元素
    stacknode *node=(linkstack)malloc(sizeof(stacknode));//申请一个新的结点
    node->data=e;
    node->next=s;
    s=node;
    return TRUE;
}
status pop(linkstack &s,elemtype &e){
    //如果栈不空,就删除s的栈顶元素,并用e返回栈顶元素的值
    if (s->next==NULL) {
        //s里面没有存放如何数据
        return FALSE;
    }
    e=s->data;
    stacknode *t=s;
    s=t->next;
    free(t);
    return TRUE;
}

status stack_empty(linkstack s){
    if(s->next==NULL)
        return TRUE;
    else
        return FALSE;
}

status destroy_stack(linkstack s){
    stacknode *p=s;
    while (p!=NULL) {
        s=s->next;
        free(p);
        p=s;
    }
    return TRUE;
}

int main() {
    linkstack s;
    init_stack(s);
    return 0;
}

3.4 队列的基本概念

队列的定义

  • 队列也是一种操作受限的线性表
  • 队列只允许在表的一端进行插入,而在表的另一端进行删除。

队列的特点

  • 先进先出。
  • 队头是允许删除的那一端,又称为队首
  • 队尾是允许插入的一端。

3.5 队列的顺序存储结构

队列的顺序存储结构照例可以参照线性表的顺序存储结构,都是分配一块连续的存储单元来存放队列中的元素,不过多增加了两个指针:
队头指针front:指向队头元素
队尾指针rear:指向队尾元素的下一个位置

注意:在顺序队列中,一般我们都是讨论的顺序循环队列

原因:如下图,如果不做循环队列,会出现“上溢出”的情况
在这里插入图片描述
循环队列:把顺序队列在逻辑结构上构成一个环状的空间,以此来解决“上溢出”的问题。当队首指针q.front=maxsize-1后,再进一个位置就自动回0。如下图所示:
在这里插入图片描述
从上图也可以知道,循环队列虽然可以完美解决“上溢出”问题,但也衍生出了一个新问题,就是判断队空和队满的问题,为了解决这个问题,我们有三种处理方式:

1)牺牲一个单元来区分队空和队满,入队时少用一个队列单元,约定以“队头指针在队尾指针的下一个位置作为队满的标志”,这也是一个较为普遍的做法。

2)在结构体中增加一个表示元素个数的数据成员。

3)在结构体中增设tag数据成员,tag用来标记你上一次所进行的操作,如果tag==0,代表上一次的操作是删除,则这时候的q.front == q.rear所代表的就必是队空了。如果tag ==1,代表上一次的操作是插入,则这时候的q.front ==q.rear就必为队满。

顺序队列的实现(顺序循环队列)

#include <iostream>    //C++头文件格式,如果需要可替换成C语言的头文件
using namespace std;

#define MAXQSIZE 10    //初始容量
#define error -1
//因为C语言中没有true和false关键字,虽然C++里有但是这里还是额外定义一下
#define FALSE 0
#define TRUE 1
typedef int elemtype;    //定义数据元素类型,这样做的好处是想要修改类型时只需修改这句话就可以了
typedef int status;    //因为C语言编译器中一般不存在bool,所以这里的定义相当于C++的bool类型,代表函数返回的状态

typedef struct {
    elemtype *base;//用来申请数据空间的指针
    int front;//指向队头元素的指针
    int rear;//指向队尾元素的下一个位置
} SqQueue;

status init_queue(SqQueue &q){
    //初始化一个空栈
    q.base=(elemtype *)malloc(MAXQSIZE * sizeof(elemtype));//申请一段连续的空间
    if(!q.base) return(FALSE);//申请空间失败弹出错误信息
    q.front=q.rear=0;
    return TRUE;
}

status queue_empty(SqQueue &q){
    //判断队列是否为空,如果为空返回true,否则返回false
    if(q.front==q.rear){
        return true;
    }
    return false;
}
status enqueue(SqQueue &q,elemtype e){
    //插入元素e为q的新的队列元素
    if ((q.rear+1)%MAXQSIZE==q.front) {
        //如果队满了
        return error;
    }
    q.base[q.rear]=e;
    q.rear=(q.rear+1)%MAXQSIZE;
    return TRUE;
}
status dequeue(SqQueue &q,elemtype &e){
    if (q.rear==q.front) {
        //如果队空
        return error;
    }
    e=q.base[q.front];
    q.front=(q.front+1)%MAXQSIZE;
    return TRUE;
}

int main() {
    SqQueue q;
    init_queue(q);
    return 0;
}

3.6 队列的链式存储结构

链队列实际上是一个同时带有队头指针和队尾指针的单链表。头指针指向队头结点,不过尾指针和顺序队列有一点点不同,它直接指向队尾结点。
在这里插入图片描述

链队列的实现

主要我们在这里实现的链表都是带头结点的,为啥要带头结点可以看一下下面网友们的解释:
为何链表要有头结点?

#include <iostream>    //C++头文件格式,如果需要可替换成C语言的头文件
using namespace std;

#define error -1
//因为C语言中没有true和false关键字,虽然C++里有但是这里还是额外定义一下
#define FALSE 0
#define TRUE 1
typedef int elemtype;    //定义数据元素类型,这样做的好处是想要修改类型时只需修改这句话就可以了
typedef int status;    //因为C语言编译器中一般不存在bool,所以这里的定义相当于C++的bool类型,代表函数返回的状态

typedef struct QNode{
    elemtype data;
    struct QNode *next;
} QNode,*QueuePtr;
typedef struct{
    QueuePtr front,rear;
} LinkQueue;

status init_queue(LinkQueue &q){
    //带头结点的初始化
    q.front=q.rear=(QueuePtr)malloc(sizeof(sizeof(QNode)));
    if (!q.front) {
        return error;
    }
    q.front->next=NULL;
    return TRUE;
}
status queue_empty(LinkQueue q){
    if(q.front==q.rear){
        return TRUE;
    }
    return FALSE;
}
status destroy_queue(LinkQueue &q){
    //销毁队列
    while (q.front) {
        q.rear=q.front->next;
        free(q.front);
        q.front=q.rear;
    }
    return TRUE;
}
status enqueue(LinkQueue &q,elemtype e){
    //插入元素e为q的新队尾元素
    QNode *p=(QueuePtr)malloc(sizeof(QNode));
    if(!p) return error;
    p->next=NULL;
    p->data=e;
    q.rear->next=p;
    q.rear=p;
    return TRUE;
}
status dequeue(LinkQueue &q,elemtype &e){
    //如果队列不空,则删除q的队头元素,用e返回其值
    if (q.front==q.rear) {
        //空队列
        return error;
    }
    QNode *p=q.front->next;
    e=p->data;
    q.front->next=p->next;
    if (q.rear==p) {
        //如果要释放的是这个元素是队尾元素,还要额外的更改队尾指针;
        q.rear=q.front;
    }
    free(p);
    return TRUE;
}

int main() {
    LinkQueue q;
    init_queue(q);
    return 0;
}

3.7 双端队列

双端队列:是指允许两端都可以进行入队和出队操作的队列,将队列的两端称为前端和后端。
在这里插入图片描述
输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入的双端队列称为输出受限的双端队列。
在这里插入图片描述

输入受限的双端队列:允许在一端进行插入和删除,但在另一端只允许删除的双端队列称为输入受限的双端队列。
在这里插入图片描述

这里是引用

3.8 栈和队列的应用

栈在括号匹配中的应用

假设表达式中允许包含两种括号:圆括号和方括号。现在将括号的左部和右部匹配起来。
考虑下列的括号序列:

[([][])]
12345678

解决这个问题的思路是:
1)设置一个空栈,并开始按顺序遍历序列。
2)如果是左括号则直接入栈中。
3)如果是右括号,则检查栈顶的左括号是否匹配,如果不匹配则格式错误;如果匹配则,出栈一次;如果匹配到最后栈仍存在左括号,则格式出错。
具体代码如下:

status myfunction(string str){
    //匹配输入的str中的括号
    elemtype c,e;
    linkstack s;
    init_stack(s);
    for (int i=0; i<str.length(); i++) {
        c=str[i];
        if (c=='['||c=='(') {
            push(s, c);
        }
        if (c==']') {
            get_top(s, e);
            if(e!='['){
                printf("格式错误!\n");
                return error;
            }
            pop(s, e);
        }
        if (c==')') {
            get_top(s, e);
            if(e!='('){
                printf("格式错误!\n");
                return error;
            }
            pop(s, e);
        }
    }
    if(stack_empty(s)){
        destroy_stack(s);
        printf("匹配成功!\n");
        return TRUE;
    }
    else{
        destroy_stack(s);
        printf("匹配失败!\n");
        return FALSE;
    }
}

栈在表达式求值中的应用

用栈处理表达式求值问题。例如表达式:
(中缀表达式) 12 + 3 - 4 * ( ( 6 + 16 ) / 2 - 4 ) + 32
我们需要利用栈处理这个表达式获得求得其值。
思路如下:

我们需要将中缀表达式化成后缀表达式
(后缀表达式)12 3 + 4 6 16 + 2 / 4 - * - 32 +
我们仔细观察这个后缀表达式,发现顺序扫描后缀表达式的每一项:
1、如果该项是操作数,就将其压入栈中;
2、如果该项是操作符,就从栈中取出两个操作数,进行相应的运算后,将结果重新压入栈中。
3、最终结束,栈顶所存放的就是最后的计算结果。

现在还有一个问题,我们要怎么将这个中缀表达式转化成后缀表达式?

在这里我们要引入一张操作符优先级表,在读取到操作符时,我们需要查表,然后根据表的优先级序列,来进行入栈或者出栈输入的操作。
1、如果读取到操作符,我们需要将栈顶操作符和读取到的操作符优先级比较;
2、如果读取到操作数,直接输出,并读取下一个字符

优先级比较规则如下:
2、如果栈内的操作符优先级高,则直接输出栈内操作符,并退栈;
3、如果栈外的操作符优先级高,则将栈外操作符入栈,读取下一个字符;
4、如果优先级相等,则直接不输出,退栈并读取下一个字符;

在这里规定,表示式的输入由#号开始,#号结束

操作符#(* , /+ , -)
isp(栈内优先级)01536
icp(栈外优先级)06421

在这里的代码实现就不用真正的表达式了,因为带数字的字符串处理成数值还是有点麻烦的,用字母代替数字,并且在这里不在讨论从后缀表达式再转化到计算步骤,实现也比较简单,通过前文所讲到的思路实现即可。

status myfunction(string str){
    //输入的中缀表达式str,输出后缀表达式
    cout<<"后缀表达式:";
    linkstack s;
    init_stack(s);
    push(s,str[0]);//把表达式开头的#号输入
    for (int i=1; i<str.length(); ) {
        int isp=-1;
        int icp=-1;
        elemtype c=str[i];
        elemtype e=c;
        for(int j=0;j<7;j++){
            if (priority_sign[0][j]==c) {
                //记录读取到的操作符的优先级
                icp=priority_sign[2][j];
                break;
            }
        }
        if(icp==-1){
            //如果读取到操作数,则直接输出
            cout<<c;
            i++;
            continue;
        }
        get_top(s, e);
        for(int j=0;j<7;j++){
            if (priority_sign[0][j]==e) {
                //记录栈顶的操作符的优先级
                isp=priority_sign[1][j];
                break;
            }
        }
        
        if(isp>icp){
            //栈内比栈外的优先级高,则输出栈内操作符
            pop(s,e);
            cout<<e;
            //当前读取到的字符还需要进行一下轮的比较
        }
        else if(isp<icp){
            //栈外比栈内的优先级高,则将读到的操作符入栈
            push(s, c);
            i++;
        }
        else{
            //如果优先级相等,则直接不输出,退栈读取下一个字符
            pop(s, e);
            i++;
        }
    }
    cout<<endl;
    return TRUE;
}

栈在递归中的应用

总所周知,其实C语言中有函数调用函数的写法,其实就是系统开了一个栈来存放当前的函数A状态,然后在这个基础上新开一个函数B,等新开的函数B结束时,系统再读取栈中存放的函数A的状态,进行一个状态恢复,然后再继续执行函数A。

递归也是这样的。无非就是函数A和B都是同一套代码而已。所以我们在想要使用递归的思想,但又不想采用递归的算法,可以利用栈来实现非递归算法的转变,它的算法思想的实质其实也是保存当前函数状态。

值得注意的是由于递归算法的栈是由系统自己开的,所以会存下很多没有必要的状态,所以如果用自己开设的栈,将递归算法转成非递归算法,效率通常会高一些。

举个例题:利用栈来实现以下递归算法的非递归运算
在这里插入图片描述

struct P{
    int n;
    double value;
};
typedef P elemtype;    //定义数据元素类型,这样做的好处是想要修改类型时只需修改这句话就可以了

double Myfunction(double x,int n){
    linkstack s;
    init_stack(s);
    for(int i=n;i-2>=0;i--)
    {
        elemtype e;
        e.n=i;
        push(s,e);//将需要递归出结果的Pn放入栈中
    }
    double val1,val2;
    val2=2*x;
    val1=1;
    while(!stack_empty(s)){
    	//将递归最底层的Pn开始计算
        elemtype e;
        get_top(s, e);
        val1=val1*2*(e.n-1);
        val2=val2*2*x;
        e.value=val2-val1;
        val1=val2;//因为公式的第一项是需要二次使用的,逐一留值
        val2=e.value;
        pop(s,e);//将已经不需要使用的Pn剔除栈
    }
    if(n==0)return val1;
    return val2;
}

int main() {
    cout<<Myfunction(5, 3);
    return 0;
}

队列在层次遍历中的应用

这在学到二叉树的时候再详细讨论。

队列在计算机系统中的应用

队列其实在计算机系统应用非常广泛,我们在这里举两个方面的例子:第一个方面是解决主机与外部设备之间速度不匹配的问题,第二个方面是解决由多用户引起得资源竞争问题。
这两个方面在以后学习计算机系统的时候会有详细解释,现在只是做一个提及。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值