栈的应用

Posted KLeonard

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了栈的应用相关的知识,希望对你有一定的参考价值。

由于栈结构具有后进先出的固有特性,致使栈称为程序设计中的有用工具。

1.数制转换

十进制数N和其他d进制数的转换是计算机实现计算的基本问题,其解决方法很多,其中一个简单算法基于下列原理:

N = (N div d) * d + N mod d(其中:div为整除运算,mod为求余运算)

例如,(2007)10 = (3727)8,其运算过程如下:

可以看到上述过程是从低位到高位产生8进制的各个数位,然后从高位到低位进行输出,结果数位的使用具有后出现先使用的特点,因此生成的结果数位可以使用一个栈来存储,然后从栈顶开始依次输出即可得到相应的转换结果。

下述算法实现了十进制数转八进制数,并打印结果。其中,对于栈,我们使用上一节中实现的ArrayStack

public class Conversion 
    public static void conversion(int N)
        ArrayStack<Integer> stack = new ArrayStack<>();
        while( N != 0)
            stack.push(N % 8);
            N /= 8;
        
        while(!stack.isEmpty())
            System.out.print(stack.pop());
        
    

    public static void main(String[] args) 
        conversion(2007);
    

运行该程序,输出结果:

3727

2.括号匹配检验

假设表达式中包含三种括号:圆括号、方括号和花括号,并且它们可以任意嵌套。例如[()]()[][()([])]等为正确格式,而[()][()]为不正确的格式。那么怎么检测表达式是否正确呢?

这个问题可以用“期待的急迫程度”这个概念来描述。对表达式中的每一个左括号都期待一个相应的右括号与之匹配,表达式中越迟出现并且没有得到匹配的左括号期待匹配的程度越高。不是期待出现的右括号则是不正确的。它具有天然的后进先出的特点。

于是我们可以设计算法:算法需要一个栈,在读入字符的过程中,如果是左括号,则直接入栈,等待相匹配的同类右括号;如果是右括号,且与当前栈顶左括号匹配,则将栈顶左括号出栈,如果不匹配则属于不合法的情况。另外,如果碰到一个右括号,而堆栈为空,说明没有左括号与之匹配,则非法。那么,当字符读完的时候,如果是表达式合法,栈应该是空的,如果栈非空,那么则说明存在左括号没有相应的右括号与之匹配,也是非法的情况。

下述算法实现了对该表达式的括号匹配检验:

public class Match 
    public static boolean match(String s)
        ArrayStack<Character> stack = new ArrayStack<>();
        for(int i = 0;i<s.length();i++)
            char c = s.charAt(i);
            switch (c) 
            case ')':
                if(!stack.isEmpty() && stack.pop() == '(')
                    break;
                else
                    return false;
                
            case ']':
                if(!stack.isEmpty() && stack.pop() == '[')
                    break;
                else
                    return false;
                
            case '':
                if(!stack.isEmpty() && stack.pop() == '')
                    break;
                else
                    return false;
                
            default:
                stack.push(c);
                break;
            
        
        return stack.isEmpty();
    

    public static void main(String[] args) 
        System.out.println(match("[()]()[]"));
        System.out.println(match("[()]"));
    

输出结果:

true
false

3.迷宫求解

求迷宫从入口到出口的所有路径是一个经典的程序设计问题。由于计算机解迷宫时,通常用的是“穷举求解”的方法,即从入口出发,顺某一方向向前探索,若能走通,则继续往前走;否则沿原路退回,换一个方向再继续探索,直至所有可能的通路都探索到为止。

为了保证在任何位置都能沿原路退回,显然需要用一个后进先出的结构来保存从入口到当前位置的路径。因此在迷宫求解时应用“栈”也就是自然而然的事情了。

在计算机中,我们可以用一个二维数组来表示一个迷宫,如下图所示:

我们使用字符1来表示迷宫中的墙体,即灰色的方块;使用字符0来表示可以通过的道路,即白色的方块。

求解迷宫的算法思想可以描述为:

初始化,将起点加入堆栈;
while(堆栈不为空)
    取出栈顶位置为当前位置;
    如果 当前位置是终点,
    则 使用堆栈记录的路径标记从起点至终点的路径;
    否则
        按照从下、右、上、左的顺序将当前位置下一个可以探索的位置入栈;
        如果 当前位置的四周均不通
        则 当前位置出栈;
    

所谓当前位置可通,指的是未曾走到过的通道块,即要求该位置不但是通道块,而且不在当前路径上(否则所求的路径就是简单路径),也不是曾经纳入过路径的通道块(否则只能在死胡同内转悠)。

下述代码实现了对该迷宫的求解:

/**
 * 迷宫通道类,一个Cell代表地图上的一个方块
 * @author Gavin
 *
 */
class Cell 
    private int x; // 单元所在行
    private int y; // 单元所在列
    private char c; // 字符,通道对应'0',墙对应'1'
    private boolean isVisited;// 是否访问过

    public Cell(int x, int y, char c, boolean isVisited) 
        super();
        this.x = x;
        this.y = y;
        this.c = c;
        this.isVisited = isVisited;
    

    public char getC() 
        return c;
    

    public void setC(char c) 
        this.c = c;
    

    public boolean isVisited() 
        return isVisited;
    

    public void setVisited(boolean isVisited) 
        this.isVisited = isVisited;
    

    public int getX() 
        return x;
    

    public int getY() 
        return y;
    

    @Override
    public boolean equals(Object obj) 
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Cell other = (Cell) obj;
        if (x != other.x)
            return false;
        if (y != other.y)
            return false;
        return true;
    

    @Override
    public String toString() 
        return this.getC() + "("+this.getX()+", "+this.getY() + ")";
    


/**
 * 迷宫求解类
 * @author Gavin
 *
 */
public class Maze 

    public static void main(String[] args) 
        char maze[][] =   '1', '1', '1', '1', '1', '1', '1', '1', '1', '1' ,
                 '1', '0', '0', '1', '1', '1', '0', '0', '1', '1' ,
                 '1', '0', '0', '1', '1', '0', '0', '1', '0', '1' ,
                 '1', '0', '0', '0', '0', '0', '0', '1', '0', '1' ,
                 '1', '0', '0', '0', '0', '1', '1', '0', '0', '1' ,
                 '1', '0', '0', '1', '1', '1', '0', '0', '0', '1' ,
                 '1', '0', '0', '0', '0', '1', '0', '1', '0', '1' ,
                 '1', '0', '1', '1', '0', '0', '0', '1', '0', '1' ,
                 '1', '1', '0', '0', '0', '0', '1', '0', '0', '1' ,
                 '1', '1', '1', '1', '1', '1', '1', '1', '1', '1' , ;
        mazeExit(maze, 8, 8, 1, 7);
    

    /**
     * 求解迷宫
     * @param maze 迷宫的字符数组
     * @param in_x 起点x坐标
     * @param in_y 起点y坐标
     * @param out_x 终点x坐标
     * @param out_y 终点y坐标
     */
    public static void mazeExit(char maze[][], int in_x, int in_y, int out_x, int out_y) 
        Cell[][] cells = createMaze(maze);
        printMaze(cells);
        Cell start = cells[in_x][in_y];
        Cell end = cells[out_x][out_y];
        ArrayStack<Cell> stack = new ArrayStack<>();
        stack.push(start);
        start.setVisited(true);
        while (!stack.isEmpty()) 
            Cell now = stack.peek();
            if (now.equals(end)) 
                // 找到路径
                int x = out_x;
                int y = out_y;
                while(!stack.isEmpty())
                    Cell cell = stack.pop();
                    // 要注意的是,只有和上一次的cell相邻的cell才是路径上的通道
                    if(Math.abs(cell.getX()-x) + Math.abs(cell.getY()- y) <= 1)
                        cell.setC('*');
                    
                    x = cell.getX();
                    y = cell.getY();
                
                System.out.println("找到路径:");
                printMaze(cells);
                return;
             else 
                // 向四个方向探索
                boolean isDead = true;
                for (int i = 0; i < 4; i++) 
                    Cell next_cell = getCell(cells, now, i);
                    if (isValid(next_cell)) 
                        next_cell.setVisited(true);
                        stack.push(next_cell);
                        isDead = false;
                    
                
                // 四个方向都不能走,则该点为死胡同,出栈
                if(isDead)
                    stack.pop();
                
            
        
        System.out.println("找不到路径");
    

    /**
     * 判断一个cell是否是通道
     * @param cell
     * @return
     */
    public static boolean isValid(Cell cell) 
        return cell.getC() == '0' && !cell.isVisited();
    

    /**
     * 根据方向得到下一个cell
     * @param cells
     * @param now
     * @param direction
     * @return
     */
    public static Cell getCell(Cell[][] cells, Cell now, int direction) 
        int x = now.getX();
        int y = now.getY();
        Cell cell = null;
        switch (direction) 
        case 0:
            // 向下
            cell =  cells[x + 1][y];
            break;
        case 1:
            // 向右
            cell =  cells[x][y + 1];
            break;
        case 2:
            // 向上
            cell =  cells[x - 1][y];
            break;
        case 3:
            // 向左
            cell =  cells[x][y - 1];
            break;
        
        return cell;
    

    /**
     * 根据输入的二维char数组创建二维Cell数组
     * 
     * @param maze
     *            二维char数组
     * @return
     */
    private static Cell[][] createMaze(char[][] maze) 
        Cell[][] cells = new Cell[maze.length][maze[0].length];
        for (int i = 0; i < maze.length; i++) 
            for (int j = 0; j < maze[i].length; j++) 
                char c = maze[i][j];
                Cell cell = new Cell(i, j, c, false);
                cells[i][j] = cell;
            
        
        return cells;
    

    /**
     * 打印迷宫
     * 
     * @param cells
     */
    private static void printMaze(Cell[][] cells) 
        for (int i = 0; i < cells.length; i++) 
            for (int j = 0; j < cells[i].length; j++) 
                System.out.print(cells[i][j].getC() + " ");
            
            System.out.println();
        
    

程序用星号*标记路径,输出结果如下:

1 1 1 1 1 1 1 1 1 1 
1 0 0 1 1 1 0 0 1 1 
1 0 0 1 1 0 0 1 0 1 
1 0 0 0 0 0 0 1 0 1 
1 0 0 0 0 1 1 0 0 1 
1 0 0 1 1 1 0 0 0 1 
1 0 0 0 0 1 0 1 0 1 
1 0 1 1 0 0 0 1 0 1 
1 1 0 0 0 0 1 0 0 1 
1 1 1 1 1 1 1 1 1 1 

找到路径:
1 1 1 1 1 1 1 1 1 1 
1 0 0 1 1 1 * * 1 1 
1 0 0 1 1 * * 1 0 1 
1 * * * * * 0 1 0 1 
1 * 0 0 0 1 1 0 0 1 
1 * 0 1 1 1 * * * 1 
1 * * * * 1 * 1 * 1 
1 0 1 1 * * * 1 * 1 
1 1 0 0 0 0 1 0 * 1 
1 1 1 1 1 1 1 1 1 1 

上述代码还需要注意的是,因为迷宫四周有墙,因此从当前位置向四周探索时,数组下标不会越界;如果迷宫四周没有墙,则在向四周探索时,要验证位置的下标是否越界。

4.表达式求值 & 中缀表达式转后缀表达式

表达式求值是程序设计语言中的一个最基本问题。它的实现是栈应用的又一个典型例子。这里介绍一种简单直观、广为使用的算法,通常称为“算符优先法”。

要把一个表达式翻译成正确求值的一个机器指令序列,或者直接对表达式求值,首先要能够正确解释表达式。例如要对下述表达式求值:

4+(6-10+2*2*2

首先,要了解算术四则运算的规则。即:

  • (1)先乘除,后加减;
  • (2)从左算到右
  • (3)先括号内,后括号外

由此,这个算术表达式的计算顺序应为:

4+(6-10+2*2*2 = 4+(-4+2*2*2 = 4+(-4+4*2 = 4+0*2 = 4+0 = 4

任何一个表达式都是由操作数(operand)、运算符(operator)和界限符(delimiter)组成。界限符也就是括号等,运算符包括加减乘除,以及更复杂的求余、三角函数等等。这里我们只讨论比较简单的算术表达式,只包括加减乘除四种运算符。

我们把运算符和界限符统称为算符,根据上述3条运算规则,在运算每一步中,任意两个相继出现的算符θ1和θ2之间的优先关系至多是下面三种关系之一:

  • θ1 < θ2,θ1的优先权低于θ2
  • θ1 = θ2,θ1的优先权等于θ2
  • θ1 > θ2,θ1的优先权大于θ2

下表定义了这种优先关系:

θ1 \\ θ2+-*/()
+>><<<>
->><<<>
*<<>><>
/<<>><>
(<<<<<=
)>>>>>

由规则(3),+、-、*和/为θ1时的优先级均低于“(”但高于右括号”)”,由规则(2),当θ1 = θ2时,令θ1 > θ2。

基于上述的论述,首先,我们来讨论使用算符优先算法来实现表达式的求值。

为实现该算法,我们需要使用两个工作栈。一个称作OPTR,用以寄存运算符;另一个称作OPND,用以寄存操作数或运算结果。算法的基本思想是:

  • (1)首先置操作数栈和操作符栈为空栈
  • (2)依次读入表达式中的每个字符,若是操作数则进OPND栈,若是运算符则和OPTR栈的栈顶运算符比较优先权后作相应操作。

    1.若该运算符优先权大于栈顶运算符优先权则该运算符直接进OPTR栈;反之若运算符优先权小于栈顶运算符的优先权,则弹出栈顶运算符,并从操作数OPND栈弹出两个数进行该栈顶运算符的运算,运算结果再加入OPND操作数栈。
    2.循环往复地进行第1步判断。直到将该操作符加入OPTR操作符栈

  • (3)表达式读取结束,若两个栈都不为空,则依次弹出OPTR栈中的运算符和OPND栈中的两个操作数,进行运算后,将运算结果在加入OPND栈。直到OPTR栈为空,OPND栈只剩下一个元素,则该元素就是运算结果。

  • (4)中间出现差错,比如最后OPND栈剩下不止一个数,则视为表达式出错。

下述算法实现了该表达式求值的算法:

import java.util.EmptyStackException;

public class Evaluate 

    public static void main(String[] args) 
        System.out.println(new Evaluate().evaluate("4+(6-10+2*2)*2"));
    

    /**
     * 表达式求值
     * 
     * @param expression
     *            表达式
     * @return 返回double型的计算结果
     */
    public double evaluate(String expression) 
        ArrayStack<Double> OPND_stack = new ArrayStack<>();// 操作数栈
        ArrayStack<Character> OPTR_stack = new ArrayStack<>(); // 操作符栈
        // 遍历这个表达式
        for (int i = 0; i < expression.length(); i++) 
            char c = expression.charAt(i);
            // 如果当前字符是数字,也就是操作数
            if (Character.isDigit(c) || c == '.') 
                StringBuilder stringBulider = new StringBuilder();
                // 操作数的拼接,包括小数点
                while (i < expression.length() && (Character.isDigit(c = expression.charAt(i)) || c == '.')) 
                    stringBulider.append(c);
                    i++;
                
                // 操作数入栈
                OPND_stack.push(Double.valueOf(stringBulider.toString()));
                // 跳过本次循环,i的值已经增加过,所以要减去
                i--;
                continue;
             else 
                // 当前的字符是操作符
                outer: while (!OPTR_stack.isEmpty()) 
                    switch (precede(OPTR_stack.peek(), c)) 
                    case '<':
                        // 栈顶运算符小于该运算符,该运算符直接入栈
                        OPTR_stack.push(c);
                        break outer;
                    case '=':
                        // 栈顶运算符等于该运算符,只有一种情况,左右括号匹配,弹出左括号
                        OPTR_stack.pop();
                        break outer;
                    case '>':
                        // 栈顶运算符大小该运算符
                        char optr = OPTR_stack.pop();
                        // 如果有多余的操作符却没有操作数可以计算了,那么说明表达式错误
                        try 
                            double opnd2 = OPND_stack.pop();
                            double opnd1 = OPND_stack.pop();
                            OPND_stack.push(operate(opnd1, optr, opnd2));
                         catch (EmptyStackException e) 
                            System.err.println("表达式有误0!");
                            System.exit(0);
                        
                        break;
                    
                
                // 第一次栈为空的情况,直接入栈。还有退栈直至栈为空的情况,当前操作符也需要入栈
                if (OPTR_stack.isEmpty()) 
                    OPTR_stack.push(c);
                
            
        
        while (!OPTR_stack.isEmpty()) 
            char optr = OPTR_stack.pop();
            // 如果有多余的操作符却没有操作数可以计算了,那么说明表达式错误
            try 
                double opnd2 = OPND_stack.pop();
                double opnd1 = OPND_stack.pop();
                OPND_stack.push(operate(opnd1, optr, opnd2));
             catch (EmptyStackException e) 
                System.err.println("表达式有误!");
                System.exit(0);
            
        
        if (OPND_stack.size() == 1) 
            return OPND_stack.pop();
         else 
            System.err.println("表达式有误!");
            System.exit(0);
        
        return 0;
    

    /**
     * 运算
     * 
     * @param opnd1
     *            操作数1
     * @param optr
     *            操作符
     * @param opnd2
     *            操作符2
     * @return 运算结果
     */
    private double operate(double opnd1, char optr, double opnd2) 
        switch (optr) 
        case '+':
            return opnd1 + opnd2;
        case '-':
            return opnd1 - opnd2;
        case '*':
            return opnd1 * opnd2;
        case '/':
            return opnd1 / opnd2;
        
        return 0;
    

    /**
     * 比较两个运算符的优先权大小
     * 
     * @param θ1
     * @param θ2
     * @return
     */
    public char precede(char θ1, char θ2) 
        if1 == '+' || θ1 == '-') 
            if2 == '+' || θ2 == '-' || θ2 == ')') 
                return '>';
             else 
                return '<';
            
         else if1 == '*' || θ1 == '/') 
            if2 == '(') 
                return '<';
             else 
                return '>';
            
         else if1 == '(') 
            if2 == ')') 
                return '=';
             else 
                return '<';
            
        else if1 == ')')
            return '>';
        
        return '>';
    

程序运行正确。输出:

4.0

要注意的是,上述程序支持小数的输入,但暂不支持负数,对负数要进一步处理,这里不作赘述。


上述算法即为表达式求值的过程,用到了两个栈,一个操作数栈OPND,一个操作符栈OPRT。

那么还有一种程序设计需求是先将中缀表达式转换成后缀表达式,再对后缀表达式进行求值。所谓中缀表达式就是人类能正常阅读的算术表达式,就像我们上述例子中的输入一样:4+(6-10+2*2)*2。而后缀表达式是让计算机读的,有了后缀表达式之后再求值就非常简单。

而中缀表达式转后缀表达式的过程与上类似,只不过在依次读入字符的过程中遇到操作符先不求值,对于优先级高的栈顶操作符,将之弹出加入操作数队列(队列也是一种数据结构)即可,最后输出整个队列,就是后缀表达式。

这里我们用StringBuilder代替队列,对上述算法稍作修改,便可实现中缀表达式转后缀表达式:

public class Suffix 
    public static void main(String[] args) 
        System.out.println(new Suffix().infixToSuffix("4+(6-10+2*2)*2"));
    

    /**
     * 中缀表达式转后缀表达式
     * 
     * @param expression
     *            表达式
     * @return 返回后缀表达式
     */
    public String infixToSuffix(String expression) 
        ArrayStack<Character> OPTR_stack = new ArrayStack<>();// 操作符栈
        StringBuilder suffix = new StringBuilder();
        // 遍历这个表达式
        for (int i = 0; i < expression.length(); i++) 
            char c = expression.charAt(i);
            // 如果当前字符是数字,也就是操作数
            if (Character.isDigit(c) || c == '.') 
                StringBuilder stringBulider = new StringBuilder();
                // 操作数的拼接,包括小数点
                while (i < expression.length() && (Character.isDigit(c = expression.charAt(i)) || c == '.')) 
                    stringBulider.append(c);
                    i++;
                
                suffix.append(stringBulider.toString()+" ");
                // 跳过本次循环,i的值已经增加过,所以要减去
                i--;
                continue;
             else 
                // 当前的字符是操作符
                outer: while (!OPTR_stack.isEmpty()) 
                    switch (precede(OPTR_stack.peek(), c)) 
                    case '<':
                        // 栈顶运算符小于该运算符,该运算符直接入栈
                        OPTR_stack.push(c);
                        break outer;
                    case '=':
                        // 栈顶运算符等于该运算符,只有一种情况,左右括号匹配,弹出左括号
                        OPTR_stack.pop();
                        break outer;
                    case '>':
                        // 栈顶运算符大小该运算符
                        char optr = OPTR_stack.pop();
                        suffix.append(optr);
                        suffix.append(" ");
                        break;
                    
                
                // 第一次栈为空的情况,直接入栈。还有退栈直至栈为空的情况,当前操作符也需要入栈
                if (OPTR_stack.isEmpty()) 
                    OPTR_stack.push(c);
                
            
        
        while (!OPTR_stack.isEmpty()) 
            char optr = OPTR_stack.pop();
            suffix.append(optr);
            suffix.append(" ");
        
        return suffix.toString();
    

    /**
     * 比较两个运算符的优先权大小
     * 
     * @param θ1
     * @param θ2
     * @return
     */
    public static char precede(char θ1, char θ2) 
        if1 == '+' || θ1 == '-') 
            if2 == '+' || θ2 == '-' || θ2 == ')') 
                return '>';
             else 
                return '<';
            
         else if1 == '*' || θ1 == '/') 
            if2 == '(') 
                return '<';
             else 
                return '>';
            
         else if1 == '(') 
            if2 == ')') 
                return '=';
             else 
                return '<';
            
        else if1 == ')')
            return '>';
        
        return '>';
    

程序输出:

4 6 10 - 2 2 * + 2 * + 

这便是输入的中缀表达式所对应的后缀表达式,后缀表达式是没有括号的。

之后再对后缀表达式求值的话就比较简单了,只需要再用一个栈,依次读入后缀表达式的每个字符,如果是操作数则入栈,如果是操作符则弹出栈顶两个元素进行运算,将运算结果再入栈,最后的栈顶元素就是运算结果。其实也就是表达式求值的计算部分。

以上是关于栈的应用的主要内容,如果未能解决你的问题,请参考以下文章

单调栈的实现和应用

单调栈的实现和应用

单调栈的实现和应用

linux应用之vim的常用命令

栈的应用:每日温度

栈的应用之行编辑程序