第三章--栈与队列

栈(stack)

定义

一种后进先出(LIFO)的线性表,在表的一端插入和删除。

进栈:最先插入的元素放在栈的底部。

出栈:最后插入的元素最先出栈。

*栈顶元素先出栈栈底元素最后出栈

基本操作

1、InitStack (&S)

操作结果:构造一个空栈 S 。

2、DestroyStack (&S)

初始条件:栈 S 已存在 。

操作结果:栈 S 被销毁 。

3、StackEmpty (S)

初始条件:栈 S 已存在。

操作结果:若栈 S 为空栈,则返回 TRUE,否则 FALSE 。

4、StackLength (S)

初始条件:栈 S 已存在 。

操作结果:返回 S 的元素个数,即栈的长度 。

5、GetTop (S, &e)

查看栈顶元素,不同于 Pop ;

初始条件:栈 S 已存在且非空 。

操作结果:用 e 返回 S 的栈顶元素 。

6、ClearStack (&S)

初始条件:栈 S 已存在 。

操作结果:将 S 清为空栈 。

7、Push (&S, e)

初始条件:栈 S 已存在 。

操作结果:插入元素 e 为新的栈顶元素 。 

8、Pop(&S, &e)

初始条件:栈 S 已存在且非空 。

操作结果:删除 S 的栈顶元素,并用 e 返回其值 。

注意 GetTop 和 Pop 的区别! !

判断出栈顺序类题目的做题技巧:

先看头节点,看其后是否有比它更小的数,若有两个以上,只要这几个比它小的数是按照逆序排列就是合理的。

例:

1)4 3 5 6 1 2(错!!应该是 2 1 才可以)

2)1 3 5 4 2 6(正确)

栈的表示与实现
顺序栈

指栈的顺序结构,一组地址连续的存储单元依次存放自栈底到栈顶的数据元素。

1、栈底指针 base,总是指向栈底。

2、栈顶指针 top:栈空时,top 等于 base;栈非空时,总是指向栈顶元素 + 1 的位置。

        1)插入一个栈顶元素,指针 top 增 1;

        2)删除一个栈顶元素,指针 top 减 1;

栈的顺序表示:

#define MAXSIZE 100  // 存储空间初始分配量
typedef int ElemType;
typedef int Position;
typedef struct {
    ElemType *base;  // 栈构造前和销毁后,base的值为NULL
    Position top;    // 栈顶指针
    int size;        // 当前已分配的存储空间,以元素为单位
} SqStack;

当然也有另一种堆栈方式,我有详细分析其实现方式以及优势,博文链接我放在这了:

PTA:另类堆栈-优快云博客

链栈

栈的一种链式存储结构,链栈的存储空间是动态分配的,不需要预先确定最大容量。

1、插入和删除(进栈 / 出栈)只能在表头位置(栈顶)进行 。

2、链栈中的结点是动态产生的,可不考虑上溢问题 。

3、无需头结点栈顶指针就是链表(即链栈)头指针

栈的链式表示:

typedef struct node{
    LElemType data;
    struct node *link;
}StackNode, *LinkStack;

栈的应用

1、数制转换

#include <iostream>
#include <stack>
using namespace std;

void convert(int num, int base) // 余数放入栈
{
    stack<int> s;
    while (num)
    {
        int x = num % base;
        s.push(x);
        num /= base;
    }
    if (s.empty())
    {
        s.push(0);
    }
    while (!s.empty())
    {
        int digit = s.top();
        s.pop();
        if (digit < 10)
        {
            cout << digit;
        }
        else
        {
            cout << char(digit - 10 + 'A');
        }
    }
    cout << endl;
}

int main()
{
    int num, base;
    cin >> num >> base;
    convert(num, base);
    return 0;
}

时间复杂度:O(log num)
空间复杂度:O(log num)

栈在算法中的作用:暂存程序运行中的状态

2、括号匹配的检验

#include <iostream>
#include <stack>
#include <string>
using namespace std;

bool isOpeningBracket(char c) {
    return c == '(' || c == '[' || c == '{';
}

bool isMatchingPair(char opening, char closing) {
    return (opening == '(' && closing == ')') ||
           (opening == '[' && closing == ']') ||
           (opening == '{' && closing == '}');
}

int main() {
    string expression;
    getline(cin, expression);
    stack<char> bracketStack;

    for (char c : expression) {
        if (isOpeningBracket(c)) {
            bracketStack.push(c);
        } else if (c == ')' || c == ']' || c == '}') {
            if (bracketStack.empty()) {
                cout << "no" << endl;
                return 0;
            }
            char top = bracketStack.top();
            if (isMatchingPair(top, c)) {
                bracketStack.pop();
            } else {
                cout << top << endl;
                cout << "no" << endl;
                return 0;
            }
        }
    }

    if (bracketStack.empty()) {
        cout << "yes" << endl;
    } else {
        cout << bracketStack.top() << endl;
        cout << "no" << endl;
    }
    return 0;
}

3、表达式求值 

表达式的三种表示方法

设 Exp = S1 + OP + S2 (这里 S1 和 S2 是操作数,OP 是运算符 ):

1)前缀表示法:OP + S1 + S2 ,运算符在操作数之前。例如 2 + 3 ,前缀表示为 + 2  3 。

2)中缀表示法:S1 + OP + S2 ,这是最常用的表示形式,运算符在两个操作数中间,如 2 + 3 。

3)后缀表示法:S1 + S2 + OP ,运算符在操作数之后。对于 2 + 3 ,后缀表示为 2  3  + 。

结论:

1)操作数相对次序不变:无论前缀、中缀还是后缀表示法,操作数(如 a、b 等)在表达式中的相对先后顺序是一样的 ,只是运算符位置和组合方式改变。

2)运算符相对次序不同:三种表示法中,运算符的排列顺序差异明显,如前缀式中运算符在操作数前,后缀式中在操作数后,中缀式在操作数中间。

3)中缀式运算次序问题:中缀式去掉括号后,像有多种优先级运算符时(如加减乘除混合 ),无法明确运算先后,必须依赖括号辅助确定运算顺序。

4)前缀式运算规则:比如前缀式 + × a b × - c / d e f 里,× a b 就是连续两个操作数 a、b 和之前紧邻的运算符 × 构成最小表达式,先进行 a 乘 b 的运算。

5)后缀式运算规则:以后缀式 a b × c d e / - f × + 为例,运算符出现顺序就是计算顺序,如先算 a b × ,再算 c d e / - 等,每个运算符和之前紧邻两个操作数构成最小表达式。

中缀表达式转后缀表达式的方法:

1)当扫描到一个运算符,若它的优先级比栈顶运算符高,就将其压入栈

2)若扫描到的运算符优先级比栈顶运算符低,则将栈顶运算符出栈,写入后缀表达式。

4、栈走迷宫

求解思想:回溯法

1)从入口出发,按某一方向向未走过的前方探索。

2)若能走通,则到达新点,否则试探下一方向。

3)若所有的方向均没有通路,则沿原路返回前一点,换下一个方向再继续试探(沿原路返回实际上是出栈) 。

4)直到所有可能的通路都探索到,或找到一条通路,或无路可走又返回到入口点。

5、函数调用过程

调用前,系统完成:

1)将实参,返回地址等传递给被调用函数。

2)为被调用函数的局部变量分配存储区。

3)将控制转移到被调用函数的入口

调用后,系统完成:

1)保存被调用函数的计算结果

2)释放被调用函数的数据区

3)依照被调用函数保存的返回地址将控制转移到调用函数。

6、递归与栈

递归:

1)自我调用。

2)必须有结束条件。

7、在一个数组中实现两个堆栈:

PTA:在一个数组中实现两个堆栈-优快云博客

队列(queue)

定义

一种先进先出(FIFO)的线性表。在表的一端插入,在另一端删除

基本操作

1、InitQueue(&Q)

功能:构造一个空队列 Q 。通过调用这个函数,创建一个没有元素的队列,为后续在队列上进行插入(入队 )、删除(出队 )等操作做准备。这里的 &Q 表示传递队列 Q 的地址,以便函数内部能对其进行初始化操作。

2、DestroyQueue(&Q)

功能:销毁队列 Q ,使队列不再存在。调用该函数后,队列占用的内存等资源被释放,不能再对其进行与队列相关的操作。

3、QueueEmpty(Q)

功能:若 Q 为空队列,则返回 TRUE,否则返回 FALSE。

4、QueueLength(Q)

功能:返回 Q 的元素个数,即队列的长度。

5、GetHead(Q, &e)

功能:用 e 返回 Q 的队头元素。

队列的表现与实现
顺序队列

队列的顺序表示:

#define MAXQSIZE 100 // 最大队列长度
typedef struct
{
    QElemType *base; //初始化动态分配存储空间
    int front, rear ; //队首、队尾
}SqQueue;
SqQueue Q;

1)rear 指向队尾元素; front 指向队头元素 。初始化 front=rear=0 。

2)空队列条件:Q.front==Q.rear 。

3)队列满:Q.rear-Q.front=m 。

4)入队:Q.base [rear++]=x 。

5)出队:x=Q.base [front++] 。

!!但是这种表现方式存在假溢出的问题:

设数组维数为 m,则:

  • 当 rear-front==m 时,再有元素入队发生溢出(真溢出)  
  • 当 rear 指向队尾,但队列前端仍有空位置时,再有元素入队发生溢出(假溢出)

解决方式是使用循环队列:

将数组首尾相接(即:base [0] 连在 base [m-1] 之后) 。

出入队运用模运算:

  • 入队:Q.rear=(Q.rear+1)% m
  • 出队:Q.front=(Q.front+1)% m

解决方案 :

法一:另外设一个标志以区别队空、队满 。

法二:少用一个元素空间:

  • 队空:Q.rear = Q.front
  • 队满:(Q.rear+1) % m = Q.front

队列的循环顺序存储可以解决空间浪费的问题。

链队列

队列的链式表示:

typedef int QElemType;
typedef struct QNode { // 结点类型
    QElemType data;
    struct QNode *next;
} QNode, *QueuePtr;
typedef struct { // 链队列类型
    QueuePtr front; // 队头指针
    QueuePtr rear;  // 队尾指针
} LinkQueue;

在链队列中删除元素(出队 )时,一般情况下只需修改头指针

但当删除队头元素后,若此时队列变为空(即原队头元素是队列中唯一元素 ),此时不仅要修改头指针 -- 将头指针指向头节点 ,还要修改尾指针也指向头节点 ,以头尾指针一致来表示空队列状态。

队列的应用

1、舞伴问题

我之前写的关于这个问题的博文链接放这里了:

PTA: jmu-ds-舞伴问题-优快云博客

2、扑克牌排序

这道题的链接在此(里面我写的很详细):

专题讨论:1-算法时空分析与线性表-优快云博客

拓展(STL--stack/queue的使用方法)

1、stack

stack 模板类的定义在<stack>头文件中。

stack 模板类需要两个模板参数,一个是元素类型,一个容器类型,但只有元素类型是必要的,在不指定容器类型时,默认的容器类型为 deque。

定义 stack 对象的方法:

1)stack<int> s1;

2)stack<string> s2;

stack 的基本操作有:

  • 入栈:s1.push (x)。
  • 出栈:s1.pop ()。 !!出栈操作只是删除栈顶元素,并不返回该元素的值。
  • 访问栈顶:s1.top ()。
  • 判断栈是否为空:s1.empty (),当栈空时,返回 true。
  • 访问栈中的元素个数:s1.size ()。

2、queue

queue 模板类的定义在<queue>头文件中。

与 stack 模板类很相似,queue 模板类也需要两个模板参数,一个是元素类型,一个容器类型,元素类型是必要的,容器类型是可选的,默认为 deque 类型。

定义 queue 对象的方法:

1)queue<int> q1;

2)queue<double> q2;

queue 的基本操作有:

  • 入队:q1.push (x), 将 x 接到队列的末端。
  • 出队:q1.pop (),弹出队列的第一个元素。!!出队操作只是弹出队列的第一个元素,并不返回该元素的值。
  • 访问队首元素:q1.front (),访问最早被压入队列的元素。
  • 访问队尾元素:q1.back (),访问最后被压入队列的元素。
  • 判断队列是否为空:q1.empty (),当队列空时,返回 true。
  • 访问队列中的元素个数:q1.size ()。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值