Skip to content

2 栈

目录

[2] 栈 (stack)

定义

  • 栈是特殊的线性表:只允许在一端进行插入或删- 除操作, 其逻辑结构与普通线性表相同;
  • 栈顶(Top):允许进行插入和删除的一端 (最上面的为栈顶元素);
  • 栈底(Bottom):固定的,不允许进行插入和删除的一端 (最下面的为栈底元素);
  • 空栈:不含任何元素的空表;
  • 特点:后进先出(后进栈的元素先出栈);
  • 缺点:栈的大小不可变,解决方法——共享栈

基本操作

创建&销毁

  • InitStack(&S) 初始化栈:构造一个空栈 S,分配内存空间;
  • DestroyStack(&S) 销毁栈:销毁并释放栈 S 所占用的内存空间;

增&删

  • Push(&S, x) 进栈:若栈 S 未满,则将 x 加入使其成为新栈顶;
  • Pop(&S, &x) 出栈:若栈 S 非空,则弹出(删除)栈顶元素,并用 x 返回;

查&其他

  • GetTop(S, &x) 读取栈顶元素

    • 若栈 S 非空,则用 x-返回栈顶元素

    • 栈的使用场景大多只访问栈顶元素

  • StackEmpty(S) 判空

    • 断一个栈 S 是否为空,若 S 为空,则返回 true,否则返回 false;

[2-1-1] 静态栈

{0}

c
#define MaxSize 10         //定义栈中元素的最大个数

typedef struct{
    ElemType data[MaxSize];       //静态数组存放栈中元素
    int top;                      //栈顶元素
}SqStack;

void testStack(){
    SqStack S;       //声明一个顺序栈(分配空间)
                     //连续的存储空间大小为 MaxSize*sizeof(ElemType)
}

{0-1}

c
//初始化栈
void InitStack(SqStack &S){
    S.top = -1;                   //初始化栈顶指针
}

{0-1-1}

c
void testStack(){
    SqStack S;       //声明一个顺序栈(分配空间)
    InitStack(S);
    //...
}

{1} 入栈

c
//新元素进栈
bool Push(SqStack &S, ElemType x){
    if(S.top == MaxSize - 1)        //栈满
        return false;

    S.top = S.top + 1;    //指针先加1
    S.data[S.top] = x;    //新元素入栈

    /*
    S.data[++S.top] = x;
    */
    return true;
}

{2} 出栈

c
//出栈
bool Pop(SqStack &x, ElemType &x){
    if(S.top == -1)          //栈空
        return false;

    x = S.data[S.top];       //先出栈
    S.top = S.top - 1;       //栈顶指针减1
    // x = S.data[S.top--];
    return true;
    //只是逻辑上的删除,数据依然残留在内存里
}

{3} 读栈顶

c
//读栈顶元素
bool GetTop(SqStack S, ElemType &x){
    if(S.top == -1)
        return false;

    x = S.data[S.top];      //x记录栈顶元素
    return true;
}

{5-2}

c
//判栈空
bool StackEmpty(SqStack S){
    if(S.top == -1)      //栈空
        return true;
    else                 //栈不空
        return false;
}

top 指针指向下一个位置

  • 也可以初始化时定义 S.top = 0 :top 指针指向下一个可以插入元素的位置(栈顶元素的后一个位置)
    • 进栈操作 :栈不满时,栈顶指针先加 1,再送值到栈顶元素。S.data[S.top++] = x;
    • 出栈操作:栈非空时,先取栈顶元素值,再将栈顶指针减 1。x = S.data[–S.top];
    • 栈空条件:S.top==-1
    • 栈满条件:S.top==MaxSize-1
    • 栈长:S.top+1

[2-1-1-1] 共享栈

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

{0/0-1} 结构体定义以及初始化

  • 存取数据的时间复杂度均为 O(1)
c
#define MaxSize 10         //定义栈中元素的最大个数

typedef struct{
    ElemType data[MaxSize];       //静态数组存放栈中元素
    int top0;                     //0号栈栈顶指针
    int top1;                     //1号栈栈顶指针
}ShStack;

//初始化栈
void InitSqStack(ShStack &S){
    S.top0 = -1;        //初始化栈顶指针
    S.top1 = MaxSize;
}

栈满条件:top1-top0=1

[2-2] 链栈

特性

  • 定义:采用链式存储的栈称为链栈。

  • 优点:链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。

  • 特点:

    • 进栈和出栈都只能在栈顶一端进行(链头作为栈顶)
    • 链表的头部作为栈顶,意味着
      • 在实现数据"入栈"操作时,需要将数据从链表的头部插入
      • 在实现数据"出栈"操作时,需要删除链表头部的首元节点
      • 因此,链栈实际上就是一个只能采用头插法插入或删除数据的链表

栈的链式存储结构可描述为:

c
typedef struct Linknode{
    ElemType data;              //数据域
    struct Linknode *next;      //指针域
}*LiStack;                      //栈类型的定义

基本操作

  • 初始化
  • 进栈
  • 出栈
  • 获取栈顶元素
  • 判空、判满

[2-2-1-1] 有头节点的栈

{0}

c
#include<stdio.h>

struct Linknode{
    int data;             //数据域
    Linknode *next;       //指针域
}Linknode,*LiStack;

typedef Linknode *Node;   //结点结构体指针变量
typedef Node List;        //结点结构体头指针变量

{0-1}

c
//1. 初始化
void InitStack(LiStack &L){   //L为头指针
    L = new Linknode;
    L->next = NULL;
}

{1-3-1} 头插法进栈

c
//3. 进栈(:链栈基本上不会出现栈满的情况)
void pushStack(LiStack &L, int x){
    Linknode s;          //创建存储新元素的结点
    s = new Linknode;
    s->data = x;

    //头插法
    s->next = L->next;
    L->next = s;
}

{2-3-1} 头出栈

c
//4.出栈
bool popStack(LiStack &L, int &x){
    Linknode s;
    if(L->next == NULL) //栈空不能出栈
        return false;

    s = L->next;
    x = s->data;
    L->next = L->next->next;
    delete(s);

    return true;
}

{5-2}

c
//2.判栈空
bool isEmpty(LiStack &L){
    if(L->next == NULL){
        return true;
    }
    else
        return false;
}

[2-2-1-0] 无头节点的栈

{0}

c
#include<stdio.h>

struct Linknode{
    int data;             //数据域
    Linknode *next;       //指针域
}Linknode,*LiStack;

typedef Linknode *Node;   //结点结构体指针变量
typedef Node List;        //结点结构体头指针变量

{0-1}

c
//1.初始化
void initStack(LiStack &L){
    L=NULL;
}

{1-3-1}

c
//3.进栈
void pushStack(LiStack &L, int x){
    Linknode s;          //创建存储新元素的结点
    s = new Linknode;

    s->next = L;
    L = s;
}

{2-3-1}

c
//4.出栈
bool popStack(LiStack &L, int &x){
    Linknode s;
    if(L = NULL)     //栈空不出栈
        return false;

    s = L;
    x = s->data;
    L = L->next;
    delete(s);

    return true;
}

{5-2}

c
//2.判栈空
bool isEmpty(LiStack &L){
    if(L == NULL)
        return true;
    else
        teturn false;
}

AP>栈的应用--用栈实现括号匹配

  • ((())) 最后出现的左括号最先被匹配 (栈的特性—LIFO);
  • 遇到左括号就入栈;
  • 遇到右括号,就“消耗”一个左括号 (出栈); 匹配失败情况:
  • 扫描到右括号且栈空,则该右括号单身;
  • 扫描完所有括号后,栈非空,则该左括号单身;
  • 左右括号不匹配;

代码:

c
#define MaxSize 10

typedef struct{
    char data[MaxSize];
    int top;
} SqStack;

//初始化栈
InitStack(SqStack &S)

//判断栈是否为空
bool StackEmpty(SqStack &S)

//新元素入栈
bool Push(SqStack &S, char x)

//栈顶元素出栈,用x返回
bool Pop(SqStack &S, char &x)



bool bracketCheck(char str[], int length){
    SqStack S;      //声明
    InitStack(S);   //初始化栈

    for(int i=0; i<length; i++){
        if(str[i] == '(' || str[i] == '[' || str[i] == '{'){
            Push(S, str[i]);       //扫描到左括号,入栈
        }else{
            if(StackEmpty(S))      //扫描到右括号,且当前栈空
                return false;      //匹配失败

            char topElem;          //存储栈顶元素
            Pop(S, topElem);       //栈顶元素出栈

            if(str[i] == ')' && topElem != '(' )
                return false;

            if(str[i] == ']' && topElem != '[' )
                return false;

            if(str[i] == '}' && topElem != '{' )
                return false;
        }
    }

    StackEmpty(S);                //栈空说明匹配成功
}

【前/中/后缀表达式】

中缀表达式 (需要界限符)

运算符在两个操作数中间:

c
a + b
a + b - c
a + b - c * d
((15 ÷ (7-(1+1)))×3)-(2+(1+1))
A + B × (C - D) - E ÷ F

后缀表达式 (逆波兰表达式)

运算符在两个操作数后面:

c
a b +
a b + c - / a b c- +
a b + c d * -
15 7 1 1 + - ÷ 3 × 2 1 1 + + -
A B C D - × + E F ÷ - (机算结果)
A B C D - × E F ÷ - + (不选择)

中缀表达式转后缀表达式-手算 步骤 1: 确定中缀表达式中各个运算符的运算顺序

步骤 2: 选择下一个运算符,按照[左操作数 右操作数 运算符]的方式组合成一个新的操作数

步骤 3: 如果还有运算符没被处理,继续步骤 2

“左优先”原则: 只要左边的运算符能先计算,就优先算左边的 (保证运算顺序唯一);

中缀:A + B - C * D / E + F
后缀:A B + C D * E / - F +

重点:中缀表达式转后缀表达式-机算 初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右处理各个元素,直到末尾。可能遇到三种情况:

遇到操作数: 直接加入后缀表达式。 遇到界限符: 遇到 ‘(’ 直接入栈; 遇到 ‘)’ 则依次弹出栈内运算符并加入后缀表达式,直到弹出 ‘(’ 为止。注意: '(' 不加入后缀表达式。 遇到运算符: 依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到 ‘(’ 或栈空则停止。之后再把当前运算符入栈。 按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。

c
// 设置了2个栈,n用来存放结果,ysf用来存过程中的运算符
stack<char> n,ysf;
// 获取运算符优先级
int getr(char i) {
	if (i == '+' || i == '-')
		return 1;
	else if (i == '*' || i == '/')
		return 2;
	else
		return 0;
}
///////////////////////////
// 从头遍历字符串
for (;;)
{
	// 如果为运算符号
	if (getr(str[i]) > 0)
	{
		// 如果栈里还没有任意运算符
		if (ysf.empty())
		{
            // 将运算符压入栈
			ysf.push(str[i]);
		}
        // 如果运算符优先级大于栈顶的运算符
		else if (getr(ysf.top()) < getr(str[i]))
		{
            // 将运算符压入栈
			ysf.push(str[i]);
		}
        // 同级或次级运算符
		else if (getr(ysf.top()) >= getr(str[i]))
		{
            // 将栈顶的运算符弹出,并将该运算符放入栈
			n.push(ysf.top());
			ysf.pop();
			ysf.push(str[i]);
		}
	}
    // 左括号直接入栈
	else if (str[i] == '(')
	{
		ysf.push('(');
	}
    // 如果是右括号,弹出运算符栈的栈顶元素并输出,直到遇到左括号
	else if (str[i] == ')')
	{
		while (ysf.top() != '(')
		{
			n.push(ysf.top());
			ysf.pop();
		}
		ysf.pop();
	}
    // 如果是数字,直接放入
	else
	{
		n.push(str[i]);
	}
}

后缀表达式的计算—手算: 从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应的运算,合体为一个操作数;

注意: 两个操作数的左右顺序

重点:后缀表达式的计算—机算

用栈实现后缀表达式的计算(栈用来存放当前暂时不能确定运算次序的操作数)

步骤 1: 从左往后扫描下一个元素,直到处理完所有元素;

步骤 2: 若扫描到操作数,则压入栈,并回到步骤 1;否则执行步骤 3;

步骤 3: 若扫描到运算符,则弹出两个栈顶元素,执行相应的运算,运算结果压回栈顶,回到步骤 1;

注意: 先出栈的是“右操作数”

前缀表达式 (波兰表达式)

运算符在两个操作数前面:

+ a b
- + a b  c
- + a b * c d

中缀表达式转前缀表达式—手算 步骤 1: 确定中缀表达式中各个运算符的运算顺序

步骤 2: 选择下一个运算符,按照[运算符 左操作数 右操作数]的方式组合成一个新的操作数

步骤 3: 如果还有运算符没被处理,就继续执行步骤 2

“右优先”原则: 只要右边的运算符能先计算,就优先算右边的;

中缀:A + B * (C - D) - E / F
前缀:+ A - * B - C D / E F

前缀表达式的计算—机算 用栈实现前缀表达式的计算

步骤 1: 从右往左扫描下一个元素,直到处理完所有元素;

步骤 2: 若扫描到操作数则压入栈,并回到步骤 1,否则执行步骤 3

步骤 3: 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到步骤 1;

注意: 先出栈的是“左操作数”

中缀表达式的计算(用栈实现)

两个算法的结合: 中缀转后缀 + 后缀表达式的求值

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

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

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

AP>栈在递归中的应用

函数调用的特点:最后被调用的函数最先执行结束(LIFO)

函数调用时,需要用一个栈存储:

  • 调用返回地址
  • 实参
  • 局部变量

递归调用时,函数调用栈称为 “递归工作栈”:

  • 每进入一层递归,就将递归调用所需信息压入栈顶
  • 每退出一层递归,就从栈顶弹出相应信息

**缺点:**太多层递归可能回导致栈溢出

适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题

Copyright © 2022 田园幻想乡 浙ICP备2021038778号-1