数据结构-栈

发布时间:2024年01月24日

栈概述

  • 概述:栈是数据结构中的线性结构,栈可以利用数组和链表两种底层的数据结构实现,用数组实现的栈叫作顺序栈,用链表实现的栈叫作链栈
  • 栈的特点
    • 先进后出,后进先出
    • 栈只在一端进行元素的插入和删除操作,可以进行插入和删除的一端叫作 栈顶,而另一端叫作 栈底
  • 栈的用途
    • 内存管理:栈用于存储函数调用的参数、局部变量和返回地址,以及管理函数的调用和返回过程。这个如果相应学习过一些语言就可以更加理解这句话。
    • 表达式求值:栈可以用于计算中缀表达式、后缀表达式和前缀表达式,以及处理括号匹配。这个可去参考下我的栈实现前缀表达式的计算、栈实现后缀表达式的计算。
    • 编译器和解释器:栈用于存储编译器和解释器中的符号表、中间代码和执行环境。
    • 数据结构:栈可以用于实现其他数据结构,如队列、树、图等。
    • 算法:栈可以用于实现递归算法,深度优先搜索算法和回溯算法。

顺序栈

  • 顺序栈的形状

  • 问题?

    • 顺序栈是用数组实现的,但是我们是用数组的头部还是尾部做栈顶或者栈底呢?
    • 答:用数组的尾部做栈顶,数组的头部做栈底。
    • 为什么呢:首先,栈只在栈顶进行元素的插入和删除。因为我们知道数组的尾部进行元素的插入和删除的时间复杂度都是 O(1),但是头部做栈顶的话时间复杂度插入和删除的时间复杂度都是 O(n),所以选择时间效率更高的一种。(时间复杂度不太懂的可以去参考下这篇文章:复杂度分析-时间复杂度和空间复杂度
顺序栈的结构体
typedef struct SqStack{
    int data[maxSize];				// 顺序栈实现数组,maxSize 定义存储的最大个数
    int top;						// top 栈顶指针
}SqStack;
顺序栈的基本操作
初始化栈
void initStack(SqStack &st){
    st.top = -1;					// 栈顶指针默认初始值为 -1
}
判断栈空
int isEmpty(SqStack st){
    if(st.top == -1){
        return 1;					// 栈空
    }else{		
        return 0;					// 栈不空
    }
}

注意:栈空返回 1 ,栈不空返回 0

入栈操作
int push(SqStack &st , int x){
    if(st.top == maxSize-1){                // 栈满,上溢,入栈失败
        return 0;
    }
    ++(st.top);                             // 先移动栈顶指针,因为初始值为 -1
    st.data[st.top] = x;                    // 元素入栈
    return 1;
}

注意:入栈成功返回 1, 失败返回 0。入栈需要先判断栈是否已经满,因为数组存储是固定的

出栈操作
int pop(SqStack &st , int &x){
    if(st.top == -1){                      // 栈空,下溢,出栈失败
        return 0;
    }
    x = st.data[st.top];                   // 元素出栈
    --(st.top);                            // 后移动栈顶指针,因为初始值为 -1
    return 1;
}

注意:出栈成功返回 1, 失败返回 0 ,出栈需要先判断栈空,否则栈空出栈没有元素产生错误

链栈

  • 链栈的形状

  • 问题?

    • 链栈是用链表实现的,但是我们是用链表的头部还是尾部做栈顶或者栈底呢?
    • 答:用链表的头部做栈顶,链表的尾部做栈底。
    • 为什么呢:首先,栈只在栈顶进行元素的插入和删除。因为我们知道链表的头部进行元素的插入和删除时间复杂度都是 O(1),而链表的尾部进行元素的插入和删除时间复杂度都是 O(n),所以选择时间效率更高的一种。
链栈的结构体
typedef struct LNode{
    int data;                                // 数据域
    struct LNode *next;                      // 指针域,指向下一个结点
}LNode;  
初始化栈
void initStack(LNode *&lst){
    lst = (LNode *)malloc(sizeof(LNode));             // 制造一个头结点
    lst->next = NULL;
}
判断栈空
int isEmpty(LNode *lst){
    if(lst->next == NULL){                            // 判断栈空
        return 1;
    }else{
        return 0;
    }
}

注意:栈空返回 1 ,栈不空返回 0

入栈操作
void push(LNode *lst , int x){
    LNode *p;
    p = (LNode *)malloc(sizeof(LNode));                      // 创建一个新结点
    
    /*结点初始化*/
    p->next = NULL;
    p->data = x;
    
   	/*头插法,将结点插入栈中*/ 
    p->next = lst->next;
    lst->next = p;
}

注意:入栈操作不需要判断栈空,因为链表是动态增长的,只要内存够大,就可以一直创建节点

出栈操作
int pop(LNode *lst , int &x){
    if(lst->next == NULL){                          // 栈空
        return 0;                                         
    }
    LNode *q;
    /*将指针指向要删除的结点*/
    q = lst->next;
    x = q->data;
    /*将删除结点从链表中删除*/
    lst->next = q->next;
    /*释放结点,不要忘了*/
    free(q);
    return 1;
}

注意:出栈操作需要判断栈空,出栈记得将节点释放掉


栈的应用

十进制转二进制
  • 案例:编写一个算法,将一个非负的十进制整数N转换为一个二进制数?

  • 代码分析

    • 十进制整数得到二进制整数,可以一直模2得到余数,将余数存入栈中
    • 待 N除2为0的时候结束循环,然后依次将栈顶元素弹出,就是二进制整数
  • 代码实现

    int tenTotwo(int N , int r){                    // r 为 2
        int t[maxSize];                             // maxSize 为已定义常量
        int top = -1;								// 初始化栈顶指针
        int n = N;
        int result = 0;
        
        while(n != 0){							 
            t[++top] = n%r;							// 将余数放入栈中
            n = n/r;						
        }
        
        while(top != -1){
            result = result*10 + t[top--];
        }
        return result;
    }
    
括号匹配
  • 案例:试编写一个算法,检查一个程序中的花括号、方括号和圆括号是否配对,若全部配对,则返回 1 ,否则返回 0。对于程序中出现的一对单引号或双引号内的字符不进行括号配对检查。 39 为单引号的 ASCII 值,34 为双引号的 ASCII 值,单引号和双引号如果出现则必成对出现。假设 stack 是已定义的顺序栈结构体,可以直接调用的元素进展/出栈、取栈顶元素、判断栈空的函数定义如下:

    void push(stack &S , char ch);
    void pop(stack &S , char &ch);
    void getTop(stack S , char &ch);
    int isEmpty(stack S);					// 若栈 S 空,则返回 1,否则返回 0
    
  • 代码分析

    • 从头到尾扫描字符数组,结束条件是 等于 ‘\0’
    • 当扫描遇到左花括号、左方括号、左圆括号,令其进栈
    • 当扫描遇到右花括号、右方括号、右圆括号,则检查栈顶是否为对应的左括号,若是则,出栈,若不是,则出现语法错误,返回0
    • 当扫描到结尾,栈空,则表明括号匹配无误,返回 1,否则表明匹配失败,返回 0
    • 对单引号和双引号内不进行配对检查
  • 代码实现

    int match(char ch[]){
        int i = 0;                            // 循环变量
        
        /*定义栈*/
        stack S;
        char c;                         		  // 接收字符
        
        while(ch[i] != '\0'){
            if(ch[i] == 39){                      	// 单引号内不检查配对
                ++i;
                while(ch[i] != 39)
                    ++i;
                ++i;
            }else if(ch[i] == 34){					// 双引号内不检查配对
                ++i;
                while(ch[i] != 34)
                    ++i;
                ++i;
            }else{
                switch(ch[i]){
                    case '{':
                    case '[':
                    case '(': 
                        push(S,ch[i]);
                        break;
                    case '}':
                        getTop(S,c);               	// 获取栈顶元素
                        if(c == '{')				// 配对
                            pop(S,c);
                        else
                            return 0;
                        break;
                    case ']':
                        getTop(S,c);
                        if(c == '[')				// 配对
                            pop(S,c);
                        else
                            return 0;
                        break;
                    case ')':
                        getTop(S,c);
                        if(c == '(')				// 配对
                            pop(S,c);
                        else
                            return 0;
                }
                ++i;
            }
        }
        if(isEmpty(S) == 1){					// 栈空,则表明括号匹配无误,返回 1		
            return 1;
        }else{
            return 0;
        }
    }
    
共享栈
  • 共享栈图形

    栈S1 从低到高,栈S2从高到低,当两个栈顶在中间相遇时,栈满

  • 案例:用两个顺序栈实现共享栈?

  • 代码分析

    • 根据图形我们可以知道,在一片空间中,我们定义两个顺序栈,栈S1栈底是在 0 位置,栈顶初始值为 -1,入栈每次增加 1 ,出栈每次减少 1。栈S2 栈底是在 n-1 位置,栈顶初始值为 n, 入栈每次减少 1 ,出栈每次增加 1。
  • 代码实现

    // 定义共享栈的结构体
    typedef struct SqStack{
        int elem[maxSize];             // 定义共享栈空间
        int top[2];                    // top[0] 为 s0 的栈顶,top[1] 为 s1 的栈顶
    }SqStack;
    
    // 入栈操作
    int push(SqStack &st, int stNo , int x){     // st共享栈,stNo 顺序栈序号,x 入栈元素
        // 入栈,先判断栈是否满
        if(st.top[0]+1 < st.top[1]){
            // 栈不满,再判断 顺序栈的序号
            if(stNo == 0){
                ++(st.top[0]);              // 先移动 s0 栈顶指针
                st.elem[st.top[0]] = x;     // 元素入栈
                return 1;
            }
            if(stNo == 1){
                --(st.top[1]);              // 先移动 s1 栈顶指针
                st.elem[st.top[1]] = x;     // 元素入栈
                return 1;
            }
            return -1;                      // 序号不对
        }
        return 0;                           // 栈满
    }
    
    // 出栈操作
    int pop(SqStack &st , int stNo , int &x){
        // 先判断出栈序号
        if(stNo == 0){
            // 再判断栈是否空
            if(st.top[0] != -1){
                // 栈不空,元素出栈
                x = st.elem[st.top[0]];
                --(st.top[0]);
                return 1;
            }else{
                return 0;                // 栈空
            }
        }else if(stNo == 1){
            // 判断栈是否空
            if(st.top[1] != maxSize){
                // 栈不空,元素出栈
                x = st.elem[st.top[1]];
                ++(st.top[1]);
                return 1;
            }else{
                return 0;              // 栈空
            }
        }else{
            return -1;                // 序号不对
        }   
    }
    
栈模拟队列
  • 案例:栈模拟队列实现?

  • 代码分析

    • 栈的特点是先进后出,队列的特点是先进先出
    • 用两个栈(s1、s2)模拟队列,s1作输入栈,s2 作输出栈
    • 首先将元素逐个压入 s1 中用来模拟入队列,然后逐个将其退出并压入 s2 中,这样 s2 栈顶元素即是第一个进入 s1 的元素,这样 s2 出栈就可以模拟出队列。
    • 只有当 s1 和 s2 都空时 才算队列为空
  • 代码实现

    // 入队列
    int enQueue(SqStack &s1 , SqStack &s2 , int x){
        int y;
        if(s1.top == maxSize-1){             // 判断 s1 栈是否已满,则看 s2 是否空
            if(!isEmpty(s2)){
                return 0;                    // s1 满,s2 非空
            }else if(isEmpty(s2)){           // s2 空,先将 s1 中元素进入 s2 中
                while(!isEmpty(s1)){
                    pop(s1,y);
                    push(s2,y);
                }
                push(s1,x);                   // 将 x 进入栈 s1 中
                return 1;
            }
        }else{                                // s1 栈不满,直接入栈
            push(s1,x);
            return 1;
        }
    }
    // 出队列
    int deQueue(SqStack &s1 , SqStack &s2 , int &x){
        int y;
        if(!isEmpty(s2)){                    // s2 非空,出队列(本质,s2 元素出栈)
            pop(s2,x);
            return 1;
        }else{
            if(isEmpty(s1)){                 // s1 空,直接结束,不空,将 s1 中元素压入 s2 中
                return 0;
            }else{
                while(!isEmpty(s1)){
                    pop(s1,y);
                    push(s2,y);
                }
                pop(s2,x);
                return 1;
            }
        }
    }
    // 判断队列是否空
    int isQueueEmpty(SqStack s1 , SqStack s2){
        if(isEmpty(s1) && isEmpty(s2)){
            return 1;                     // 队列空
        }else{
            return 0;                     // 队列不空
        }
    }
    
二叉树的非递归先序遍历
  • 代码分析

    • 非递归遍历用到了栈(自定义栈)
    • 首先访问根结点,并让其入栈,再出栈(并输出根节点值)
    • 出栈后,访问当前结点 ,让其右孩子先入栈,左孩子再入栈(因为栈先进后出,而我们先访问的是左孩子)
    • 依次重复执行 2)和 3) 步骤,直到栈空为止
  • 代码实现

    void preOrder(BTNode *bt){
    	/*根结点不空,执行遍历*/
        if(bt != NULL){
            // 1)
            BTNode *stack[maxSize];						// 定义一个栈(存的都是BTNode类型指针)
            int top = -1;
            BTNode *p;
            // 2) 
            stack[++top] = bt;							// 根节点入栈
            while(top != -1){							// 栈不空
                p = stack[top--];						// p 指向根结点,并执行出栈
                visit(p);								// visit 访问结点
                if(p->rChild != NULL){
                    stack[++top] = p->rChild;
                }
                if(p->lChild != NULL){
                    stack[++top] = p->lChild;
                }
            }
        }
    }
    
文章来源:https://blog.csdn.net/weixin_45754463/article/details/135832501
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。