3.1 栈及其基本应用

基本概念

栈的定义及特点

  • 栈(stack)是一种只能在一端插入和删除的线性表。允许插入删除的一端称为栈顶(top),反之的一端称为栈底(bottom)。
  • 栈满足特性:后进先出(Last-in First-Out, LIFO)。

栈的基本操作

名称作用
create()创建一个长度为定值的新栈
destory(s)销毁栈s,释放内存空间
empty(s)返回栈s是否为空
push(s, v)将元素v插入栈s
pop(s)将栈s弹出一个元素
top(s)返回栈s栈顶元素

  • 所用的教材,和《算法》直接在出栈的同时返回栈顶的值。而《数据结构与算法分析C语言描述》书中的stack和C++的STL的stack的实现中,pop()操作并不返回任何值,而依靠独立操作top()返回栈顶元素。3.2节的队列,《算法》的描述,也有一些区别。
  • x86指令集也具有PUSHPOP指令,其中,PUSH指令将寄存器的值压栈,而POP指令将栈顶元素放入寄存器,并出栈。在IA32架构体系中,栈帧结构是实现函数调用的基本模型(可以参考《深入理解计算机系统》)。因此对于递归程序,栈这一数据结构非常重要。利用栈可以将递归程序非递归化处理。

栈的实现

栈与线性表

因为栈本质上是一种特殊的,操作受限的线性表,因此可以直接在线性表的基础上加以扩充,就得到栈这一数据结构了。在STL中,栈、队列并不是容器,而是容器配接器(container adapter)。
在STL的stack的类定义如下:

template<class T, class Sequence = deque<T> >
class stack{
protected: 
    Sequence c; //底层容器
public:
    //其他内容从略
}

因此,在实现stack的数据结构,我们可以直接利用线性表的相关数据结构进行操作,比如顺序表和链表。STL的Sequence使用的是deque容器实现,deque实际上也可以用双向循环链表简单地实现(当然STL为了使deque兼有vector的随机迭代器(random access iterator),实现起来复杂得多)。

具体的实现层面,实现一个栈,只需要原始容器具备在一端插入和删除的操作就可以了,更进一步,他们的操作时间都必须是 O(1)

因此用顺序表实现栈的过程中,只需要一个栈顶指针,指向顺序表的最后一个元素;用链表实现时,因为在头结点之后无论是插入和删除都是常数时间,因此均可以用在头部插入和删除完成操作。

顺序栈

首先利用2.1节的顺序表SeqList<_Ty>实现一个有容量限制的顺序栈。因为顺序表的栈只有在末尾插入的时间是 O(1) ,因此入栈、出栈都需要利用_list.length()获取其最后一个元素的逻辑序号。

/**
* 顺序栈类
*/
template<typename _Ty, int capacity = 64>
class SeqStack{
private:
    SeqList<_Ty, capacity> _list;
public:
    /**
    * 构造函数,创建一个空栈
    */
    SeqStack(){}
    /**
    * 析构函数
    */
    ~SeqStack(){}
    /**
    * 判断栈是否为空
    * @return bool 空栈返回true,否则false
    */
    bool empty(){
        return _list.empty();
    }
    /**
    * 返回栈顶元素
    * @return _Ty & 栈顶元素
    */
    _Ty & top(){
        assert(!_list.empty());
        return _list[_list.length()];
    }
    /**
    * 将元素入栈
    * @param _Ty & value 需要入栈的元素
    */
    void push(_Ty value){
        _list.insert(_list.length() + 1, value);
    }
    /**
    * 将栈顶元素出栈
    */
    void pop(){
        assert(!_list.empty());
        _list.remove(_list.length());
    }
    /**
    * 返回栈的实际长度
    * @return int 栈的实际长度
    */
    int length(){
        return _list.length();
    }
};

链栈

下面是利用2.3节的单链表类SinglyLList<_Ty>实现的一个栈。使用类的内联函数写法,因为操作比较简单。链式栈的优点是不存在栈的容量上限。其实如果用vector向量容器而不是定长顺序表array容器或者直接C风格数组作为顺序存储结构的底层容器,vector支持自动扩容,且摊还时间为 O(1) ,也是可以实现一个高效的栈。

/**
* 链式栈类
*/
template<typename _Ty>
class lstack{
private:
    SinglyLList<_Ty> _list;
public:
    /**
    * 构造函数,创建一个空栈
    */
    lstack(){}
    /**
    * 析构函数
    */
    ~lstack(){}
    /**
    * 判断栈是否为空
    * @return bool 空栈返回true,否则false
    */
    bool empty(){
        return _list.empty();
    }
    /**
    * 返回栈顶元素
    * @return _Ty & 栈顶元素
    */
    _Ty & top(){
        assert(!_list.empty());
        return _list[1];  //首元素的逻辑序为1
    }
    /**
    * 将元素入栈
    * @param _Ty & value 需要入栈的元素
    */
    void push(_Ty value){
        _list.insert(1, value);
    }
    /**
    * 将栈顶元素出栈
    */
    void pop(){
        assert(!_list.empty());
        _list.remove(1);
    }
    /**
    * 返回栈的实际长度
    * @return int 栈的实际长度
    */
    int length(){
        return _list.length();
    }
};

实际上,直接手写一个新的链表,或者直接用数组表示一个栈也是很容易的,这里只是为了演示栈是一种特殊的线性表,可以利用线性表的操作来完成而已。

共享栈

共享栈的实现实际上并不是那么重要,在《数据结构与算法分析C语言描述》只是作为一个练习题呈现。在一个数组中实现两个栈,只需要维护两个栈顶指针就可以了。判断这两个栈是否相等,实际是就是判断这两个指针相等。这种做法几乎没有什么实际用途,因此具体实现就不写了。

栈的应用

标记配对问题

对于标记配对问题,用栈可以很简单的实现。从文件开头读取到文件末尾,当扫描到开始标记时,将其入栈;扫描到结束标记,需要判断其是否与栈顶的开始标记匹配。当扫描到末尾栈非空,或者扫描到结束标记时栈空,则标记不配对。只有当扫描到文件结束,且栈空,标记才能算是完全匹配。

下面是一个简单的实现,只检查包括(),[],{}三种标记的配对情况,很容易扩展到更多标记的情况。至于需要实现具体语言中的标记配对,在编译原理中有着更好的解决办法。

/**
* 检查一个字符串(只包含圆括号,中括号,大括号)是否配对
* @char * str 需要检查的 C-Style 字符串
* @return bool 返回标记是否匹配,匹配则true,反之false
*/
bool check(char * str){
    std::stack<char> s;
    //使用stl的stack,并没有使用自制的栈
    for (char * p = str; *p != 0; ++p){
        if (*p == '[' || *p == '(' || *p == '{'){
            s.push(*p);
        }else if(* p == ']' || *p == ')' || *p == '}'){
            if (s.empty()){
                return false;
            } else {
                if (*p == ']'){
                    if (s.top() != '[')
                        return false;
                } else if (*p == '}'){
                    if (s.top() != '{')
                        return false;
                } else if (*p == ')'){
                    if (s.top() != '(')
                        return false;
                }
                s.pop();
            }
        }
    }
    return s.empty();
}

表达式转换与求值

给定一个只包含数字、四则运算符、圆括号的简单表达式,对其进行求值。
对于日常中的中缀表达式(infix expression),要先将其转化为后缀表达式(postfix expression,又叫逆波兰表达式),然后对其进行求值。

将中缀表达式转化为后缀表达式

(1)扫描到优先级低于栈顶元素的符号,需要将栈清空或者栈顶元素的优先级低于当前符号,并将当前符号入栈;
(2)扫描到优先级高于栈顶元素的符号,直接将运算符入栈;
(3)扫描到优先级相同的符号,将栈顶元素出栈,然后将该元素入栈。
(4)扫描到左圆括号,将左圆括号入栈。
(5)扫描到右圆括号,将栈进行弹出,弹出到左圆括号被弹出为止。(6)如果序列扫描结束,应该将栈中的所有元素进行弹出。

/**
* 将中缀表达式转换为后缀表达式
* @param const string & infix_exp 需要转换的中缀表达式
* @return string 转换后的后缀表达式
*/
std::string infix_to_postfix(const std::string & infix_exp){
    using std::string;
    using std::stack;
    stack<char> s;
    string result;
    for (int i = 0; i < infix_exp.length(); ++i){
        if (infix_exp[i] == '+' || infix_exp[i] == '-'){
            while (!s.empty()){
                if (s.top() == '(')
                    break;
                result.push_back(s.top());
                s.pop();
            }
            s.push(infix_exp[i]);
        } else if (infix_exp[i] == '*' || infix_exp[i] == '/'){
            while (!s.empty()){
                if (s.top() != '*' && s.top() != '/')
                    break;
                result.push_back(s.top());
                s.pop();
            }
            s.push(infix_exp[i]);
        } else if (infix_exp[i] == '('){
            s.push('(');
        } else if (infix_exp[i] == ')'){
            while (!s.empty() && s.top() != '('){
                result.push_back(s.top());
                s.pop();
            }
            s.pop();    //弹出多余的'('
        } else{
            while (infix_exp[i] >= '0' && infix_exp[i] <= '9'){
                result.push_back(infix_exp[i++]);
            }
            i--;        //修正多加了一次i
            result.push_back('#');  //约定以'#'作为数字结束符
        }
    }
    while (!s.empty()){
        result.push_back(s.top());
        s.pop();
    }
    return result;
}
对后缀表达式求值

对后缀表达式求值十分容易,只需要一个操作数栈,每扫描到一个操作符,就从操作数栈上依次取两个数,运算完成后将结果放在操作数栈上就可以了。最终,栈顶元素(实际上也只有1个元素)就是表达式的值。

/**
* 计算后缀表达式的值
* @param string & postfix_exp 后缀表达式
* @return double 返回表达式的值
*/
double eval_postfix(const std::string & postfix_exp){
    std::stack<double> s;
    double operand1, operand2;
    for (int i = 0; i < postfix_exp.length(); i++){
        if (postfix_exp[i] >= '0' && postfix_exp[i] <= '9'){
            int val = 0;
            while (postfix_exp[i] != '#'){  //前面定义的结束符
                val = val * 10 + postfix_exp[i++] - '0';
            }
            s.push((double)val);
        } else if (postfix_exp[i] == '+' || postfix_exp[i] == '-' ||
                   postfix_exp[i] == '*' || postfix_exp[i] == '/'){
            //注意减法、除法不满足交换律
            //需要注意操作数顺序(因为栈是LIFO的)
            operand1 = s.top();
            s.pop();
            operand2 = s.top();
            s.pop();
            if (postfix_exp[i] == '+'){
                s.push(operand2 + operand1);
            } else if (postfix_exp[i] == '-'){
                s.push(operand2 - operand1);
            } else if (postfix_exp[i] == '*'){
                s.push(operand2 * operand1);
            } else if (postfix_exp[i] == '/'){
                s.push(operand2 / operand1);
            }
        }
    }
    return s.top();
}
使用两个栈直接求值(Dijkstra 2-Stack Algorithm)

迷宫问题

迷宫问题是指,把迷宫抽象成一个二维数组,用0表示通路,1表示墙壁,求从某一点到另一点的路径。
迷宫问题可以用栈(DFS)解决,也可以用后面的队列(BFS)解决。
用栈解决迷宫问题属于一种回溯法的思想,即一步步深入问题,如果遇到不可能的情况就立刻返回上一种情况,在此基础上继续寻找可能的情况。栈这一数据结构LIFO的特性恰好是满足要求的。
下面简要给出迷宫问题算法的循环不变式:

【循环起始条件】
将起点进栈,设置起点已经走过。
【用栈解决维持的循环不变式】
维持栈不空的一个状态,取栈顶元素表示的格子,向四周探查可以继续行走的格子。如果找到了可以行走的格子,需要将这个格子设置为已经走过,并将其入栈。如果没有可以行走的格子,则将当前元素的格子出栈,并清除走过的标记。
【循环终止】
栈空,或者下一个元素就是终点。

Summary


  • 栈的基本特性和操作

栈的本质是一个操作受限制的线性表,只能在一头进行插入和删除操作,分别称为pushpop。因此在使用栈进行数据操作时,往往会导致逆序的情况出现。

  • 栈的基本实现

    栈可以用数组或者链表进行实现,实现的过程中应该保证其最坏时间都是 O(1) ,也就是说,要充分利用底层容器(存储结构)的特点。对于数组实现,应该将其放在尾部进行操作;对于单链表,应该放在头部进行操作。

  • 栈的应用

    这里列举了一些栈的应用:标记对配对、表达式转换与求值、求解迷宫问题。这些问题都利用了栈的LIFO特性,即后面要处理的内容,跟最后处理的元素有关联。
    栈也是实现递归程序非递归化的一个重要手段。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值