数据结构与算法二:栈 & 递归

一:栈

 1.1 栈的一个实际需求

 请输入一个表达式

 计算式:[7*2*2-5+1-5+3-3] 点击计算如下图

 

 请问: 计算机底层是如何运算得到结果的? 意不是简单的把算式列出运算,因为们看这个算式 7 * 2 * 2 - 5, 但是计算机怎么理解这个算式的(对计算机而言,它接收到的就是一个字符串),我们讨论的是这个问题->

1.2 栈的介绍

  1. 栈的英文为(stack)
  2. 栈是一先入后出(FILO-First In Last Out)的有序列表
  3. (stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)
  4. 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
  5. 出栈(pop)和入栈(push)的概念(如图所示)

1.3 栈的应用场景

  1. 程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中 
  2. 理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中
  3. 达式的转[中缀表达式转后缀表达式](实际解决)
  4. 叉树的遍历
  5. 形的深度优先(depthfirst)搜索法。

1.4 栈的快速入门

数组模拟栈的使用,由于栈是一种有序列表然可以使用数组的结构来储存栈的数据内容,下面我们就用数组模拟栈的出栈入栈等操作。

实现思路分析,并画出示意图

 ③用链表模拟栈 

代码实现:

使用数组模拟栈


/**
 * @author Wnlife
 * @create 2019-10-20 21:29
 * <p>
 * 用数组实现栈
 */
public class ArrayStackDemo {

    public static void main(String[] args) {

        ArrayStack stack = new ArrayStack(5);
        String key = "";
        boolean loop = true;//控制是否退出菜单
        Scanner scanner = new Scanner(System.in);
        while (loop) {

            System.out.println("show:表示显示栈内的数据");
            System.out.println("push:表示添加数据到栈");
            System.out.println("pop:将栈顶的数据弹出来");
            System.out.println("exit:退出程序");

            key = scanner.next();
            switch (key) {
                case "show":
                    stack.showStack();
                    break;
                case "push":
                    System.out.println("请输入要添加的数据~~");
                    int val = scanner.nextInt();
                    stack.push(val);
                    break;
                case "pop":
                    try {
                        System.out.printf("出栈的数据为%d\n", stack.pop());
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case "exit":
                    scanner.close();
                    loop = false;
                    break;
                default:
                    break;
            }
        }
        System.out.println("程序退出~~~");
    }
}


/**
 * 用数组实现的栈
 */
class ArrayStack {

    private int maxSize;//栈的大小
    private int[] stack;//数组模拟栈,存储数据的地方
    private int top = -1;//栈顶

    public ArrayStack(int maxSize) {
        this.maxSize = maxSize;
        stack = new int[maxSize];
    }

    //判断栈是否为空
    public boolean isEmpty() {
        return top == -1;
    }

    //判断栈是否满
    public boolean isFull() {
        return top == maxSize - 1;
    }

    //入栈-push
    public void push(int num) {

        if (isFull()) {
            System.out.println("栈是满的~~");
            return;
        }
        stack[++top] = num;
    }

    //出栈-pop
    public int pop() {

        if (isEmpty()) {
            throw new RuntimeException("栈是空的~~");
        }
        int val = stack[top];
        top--;
        return val;
    }

    //显示栈的情况[遍历栈], 遍历时,需要从栈顶开始显示数据
    public void showStack() {

        if (isEmpty()) {
            System.out.println("栈空,没有数据~~");
            return;
        }
        for (int i = top; i >= 0; i--) {
            System.out.printf("stack[%d]=%d\n", i, stack[i]);
        }
    }

}

使用链表模拟栈:

/**
 * @author Wnlife
 * @create 2019-10-21 15:23
 * <p>
 * 用链表实现一个栈
 */
public class LinkedListStackDemo {

    public static void main(String[] args) {

        LinkedListStack stack = new LinkedListStack(5);
        Scanner scanner = new Scanner(System.in);
        boolean loop = true;//循环控制变量
        String key = "";
        while (loop) {

            System.out.println("show:表示查看栈内的数据~~");
            System.out.println("push:表示将数据压入栈内~~");
            System.out.println("pop:表示将栈顶的数据弹出~~");
            System.out.println("exit:表示退出程序~~");
            key = scanner.next();
            switch (key) {
                case "show":
                    stack.show();
                    break;
                case "push":
                    System.out.println("请输入数据:");
                    int val = scanner.nextInt();
                    stack.push(val);
                    break;
                case "pop":
                    try {
                        int topVal = stack.pop();
                        System.out.printf("出栈的数据是%d\n", topVal);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case "exit":
                    scanner.close();
                    loop = false;
                    break;
                default:
                    break;
            }
        }
        System.out.println("程序退出~~");
    }

}


/**
 * 使用链表创建栈
 */
class LinkedListStack {

    private Node top;
    private int maxSize;//栈的大小
    private int size;

    public LinkedListStack(int maxSize) {
        this.maxSize = maxSize;
        create(maxSize);
    }

    //创建栈
    public void create(int num) {
        Node pre = null;
        for (int i = 0; i < num; i++) {
            Node node = new Node();
            if (i == 0) {
                top = node;
            } else {
                pre.next = node;
            }
            pre = node;
        }
    }

    //判断栈是否是满的
    public boolean isFull() {
        return size == maxSize;
    }

    //判断栈是不是为空
    public boolean isEmpty() {
        return size == 0;
    }

    //将数据压入栈
    public void push(int num) {
        if (isFull()) {
            System.out.println("栈是满的~~");
            return;
        }
        Node node = new Node(num);
        node.next = top;
        top = node;
        size++;
    }

    //将栈顶的数据弹出
    public int pop() {
        if (isEmpty()) {
            throw new RuntimeException("栈内的数据为空~~");
        }
        int val = top.val;
        top = top.next;
        size--;
        return val;
    }

    //显示栈内的数据
    public void show() {
        if (isEmpty()) {
            System.out.println("栈为空~~");
            return;
        }

        Node temp = top;
        for (int i = 0; i < size; i++) {
            System.out.printf("stack[%d]=%d\n",i,temp.val);
            temp = temp.next;
        }
    }
}


/**
 * 链表节点
 */
class Node {

    public int val;
    public Node next;

    public Node() {
    }

    public Node(int val) {
        this.val = val;
    }

    @Override
    public String toString() {
        return "Node{" +
                "val=" + val +
                '}';
    }
}

1.5 栈实现综合计算器

  • 使用栈来实现综合计算器-自定义优先[priority]

  • 思路图解

  • 代码实现
/**
 * @author Wnlife
 * @create 2019-10-21 19:03
 * <p>
 * 使用数组模拟的栈实现计算器功能(加减乘除)
 */
public class Calculator {


    //实现表达式的计算++
    public static void main(String[] args) {

        String expression="7*2*2-5+1-5+3-4";

        //创建两个栈,一个是操作数栈,一个是操作符栈
        ArrayStack2 numStack=new ArrayStack2(10);
        ArrayStack2 operStack=new ArrayStack2(10);

        //定义需要的相关变量
        char ch=' ';//将每次扫描得到char保存到ch
        String keepNum="";
        int res = 0;//计算结果

        //循环取出每个数字和字符
        for (int i = 0; i < expression.length(); i++) {
            //取出每个字符
            ch = expression.charAt(i);
            //判断ch是什么,分别处理
            if(numStack.isOper(ch)){//是操作符
                //判断当前操作符栈是否为空
                if(operStack.isEmpty()){//如果为空直接压入操作符栈
                    operStack.push(ch);
                }else {//如果栈不为空
                    //当前字符的优先级大于栈顶字符的优先级,直接压入操作符栈
                    if(operStack.priority(ch)>operStack.priority(operStack.peek())){
                        operStack.push(ch);
                    }else {//如果当前字符的优先级小于或者等于栈顶的操作符
                        //从操作数栈取出两个数,从操作符栈中取出一个操作符,进行计算,
                        // 把计算结果放到操作数栈,把当前字符压入操作数栈
                        int num1 = numStack.pop();
                        int num2 = numStack.pop();
                        int opr = operStack.pop();
                        res = operStack.cal(num1, num2, opr);
                        //把运算的结果如数栈
                        numStack.push(res);
                        //然后将当前的操作符入符号栈
                        operStack.push(ch);
                    }
                }
            }else{//当前字符是个数
                //分析思路
                //1. 当处理多位数时,不能发现是一个数就立即入栈,因为他可能是多位数
                //2. 在处理数,需要向expression的表达式的index 后再看一位,如果是数就进行扫描,如果是符号才入栈
                //3. 因此我们需要定义一个变量 字符串,用于拼接
                keepNum+=ch;
                //如果是最后一位,直接入栈
                if(i==expression.length()-1){
                    numStack.push(Integer.parseInt(keepNum));
                }else{//如果不是最后一位,则判断后一位是不是数字
                    if(operStack.isOper(expression.charAt(i+1))){
                        //如果下一位是运算符,则把当前的keepNum直接压入操作数栈
                        numStack.push(Integer.parseInt(keepNum));
                        //重要的!!!!!!, keepNum清空
                        keepNum="";
                    }
                }
            }
        }

        //当表达式扫描完毕后,就顺序的从操作数栈和操作符栈里面取出数据进行运算,把最终结果放入到操作数栈
        while (!operStack.isEmpty()){
            int num1 = numStack.pop();
            int num2 = numStack.pop();
            int opr = operStack.pop();
            res = operStack.cal(num1, num2, opr);
            //把运算的结果如数栈
            numStack.push(res);
        }
        //操将数栈的最后数,pop出,就是结果
        int r = numStack.pop();
        System.out.printf("表达式 %s = %d",expression,r);

    }

}


/**
 * 用数组实现的栈,对原有的功能进行了扩展
 */
class ArrayStack2 {

    private int maxSize;//栈的大小
    private int[] stack;//数组模拟栈,存储数据的地方
    private int top = -1;//栈顶

    public ArrayStack2(int maxSize) {
        this.maxSize = maxSize;
        stack = new int[maxSize];
    }

    //判断栈是否为空
    public boolean isEmpty() {
        return top == -1;
    }

    //判断栈是否满
    public boolean isFull() {
        return top == maxSize - 1;
    }

    //查看栈顶的值
    public int peek() {
        return stack[top];
    }

    //入栈-push
    public void push(int num) {

        if (isFull()) {
            System.out.println("栈是满的~~");
            return;
        }
        stack[++top] = num;
    }

    //出栈-pop
    public int pop() {

        if (isEmpty()) {
            throw new RuntimeException("栈是空的~~");
        }
        int val = stack[top];
        top--;
        return val;
    }

    //显示栈的情况[遍历栈], 遍历时,需要从栈顶开始显示数据
    public void showStack() {

        if (isEmpty()) {
            System.out.println("栈空,没有数据~~");
            return;
        }
        for (int i = top; i >= 0; i--) {
            System.out.printf("stack[%d]=%d\n", i, stack[i]);
        }
    }

    //返回运算符的优先级,优先级是程序员来确定, 优先级使用数字表示
    //数字越大,则优先级就越高.
    public int priority(int oper) {
        if (oper == '*' || oper == '/') {
            return 1;
        } else if (oper == '+' || oper == '-') {
            return 0;
        }
        return -1;
    }

    //判断是不是运算符
    public boolean isOper(int oper){
        return oper=='+'||oper=='-'||oper=='*'||oper=='/';
    }

    //计算两个数的方法
    public int cal(int num1, int num2, int oper) {
        int req = 0;
        switch (oper) {
            case '*':
                req = num2 * num1;
                break;
            case '/':
                req = num2 / num1;
                break;
            case '+':
                req = num2 + num1;
                break;
            case '-':
                req = num2 - num1;
                break;
            default:
                break;
        }
        return req;
    }

}

1.6 前缀、中缀、后缀表达式(逆波兰表达式)

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

1)缀表达式又称波兰式,前缀表达式的运算符位于操作数之

2)例说明: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6

  • 前缀表达式的计算机求值

从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结

例如: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤:

1)右至左扫描,将6543压入堆

2)+运算符,因此弹出343为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7

3)下来是×运算符,因此弹出75,计算出7×5=35,将35

4)后是-运算符,计算出35-6的值,即29,由此得出最终结果

1.6.2 中缀表达式

1)缀表达式就是常见的运算表达式,如(3+4)×5-6

2)中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(前面我们讲的案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式.)

1.6.3 后缀表达式

1)缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后

2)中举例说明: (3+4)×5-6 对应表达式就 3 4 + 5 × 6

3)比如:

  • 后缀表达式的计算机求值

左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果

例如: (3+4)×5-6 对应表达式就是 3 4 + 5 × 6 - , 针对缀表达式求值步骤:

1)左至右扫描,将34压入堆栈

2)+运算符,因此弹出434为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈

3)5入栈

4)下来是×运算符,因此弹出57,计算出7×5=35,将35入栈

5)6入栈

6)后是-运算符,计算出35-6的值,即29,由此得出最终结 

1.6.4  逆波兰计算器

我们完成一个逆波兰计算器,要求完成如下任务:

1)输入一个逆波兰表达式(后缀表达式),使用栈(Stack), 计算其结果

2)小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。

3)思路分析

例如: (3+4)×5-6 对应表达式就是 3 4 + 5 × 6 - , 针对缀表达式求值步骤:

1)左至右扫描,将34压入堆栈

2)+运算符,因此弹出434为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈

3)5入栈

4)下来是×运算符,因此弹出57,计算出7×5=35,将35入栈

5)6入栈

6)后是-运算符,计算出35-6的值,即29,由此得出最终结 

4)代码完成

/**
 * @author Wnlife
 * @create 2019-10-27 19:16
 * <p>
 * 逆波兰计算器:根据输入的波兰表达式求出最终的值
 */
public class PolandNotation {

    public static void main(String[] args) {

        //先定义给逆波兰表达式
        //(30+4)×5-6  => 30 4 + 5 × 6 - => 164
        // 4 * 5 - 8 + 60 + 8 / 2 => 4 5 * 8 - 60 + 8 2 / +
        //测试
        //说明为了方便,逆波兰表达式 的数字和符号使用空格隔开
        String suffixExpression = "30 4 + 5 * 6 -";
//        String suffixExpression = "4 5 * 8 - 60 + 8 2 / +"; // 76

        List<String> list = getListString(suffixExpression);
        int req = calculate(list);
        System.out.println("req = " + req);


    }

    //将一个逆波兰表达式, 依次将数据和运算符 放入到 ArrayList中
    public static List<String> getListString(String suffixExpression){
        //将suffixExpression分割
        String[] strings = suffixExpression.split(" ");
        List<String> list = Arrays.asList(strings);
        return list;
    }

    //完成对逆波兰表达式的运算
	/*
	    1)从左至右扫描,将3和4压入堆栈;
		2)遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
		3)将5入栈;
		4)接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
		5)将6入栈;
		6)最后是-运算符,计算出35-6的值,即29,由此得出最终结果
	 */
    public static int calculate(List<String> list) {
        Stack<String>stack=new Stack<>();
        for (String s : list) {
            //如果当前数据是一个数
            if(s.matches("\\d+")){
                stack.push(s);
            }else {//当前元素是一个符号
                int num2 = Integer.parseInt(stack.pop());
                int num1 = Integer.parseInt(stack.pop());
                int req=0;
                switch (s){
                    case "+":
                        req=num1+num2;
                        break;
                    case "-":
                        req=num1-num2;
                        break;
                    case "*":
                        req=num1*num2;
                        break;
                    case "/":
                        req=num1/num2;
                        break;
                    default:
                        throw new RuntimeException("符号有问题");
                }
                stack.push(req+"");
            }
        }
        return Integer.parseInt(stack.pop());
    }
}

1.6.5  中缀表达式转换为后缀表达式

大家看到,后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,我们需要将 缀表达式转成后缀表达式

  • 具体步骤如下:

1)始化两个栈:运算符栈s1和储存中间结果的栈s2

2)左至右扫描中缀表达式

3)到操作数时,将其压s2

4)到运算符时,比较其与s1栈顶运算符的优先级:

        (1)s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈

        (2)则,若优先级比栈顶运算符的高,也将运算符压入s1

        (3)否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)s1中新的栈顶运算符相比较

5)到括号时
        (1) 果是左括号“(”,则直接压入s1
        (2)
果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃

6)复步骤25,直到表达式的最右

7)s1中剩余的运算符依次弹出并压入s2

8)依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达

  • 举例说明:

中缀表1+((2+3)×4)-5”为后缀表达式的如下:因此结果"1 2 3 + 4 × + 5 –"

  • 思路分析:

  • 代码实现:
/**
 * @author Wnlife
 * @create 2019-10-27 21:05
 * <p>
 * 中缀表达式 -> 后缀表达式(逆波兰表达式)
 */
public class InfixToSuffixExpression {

    public static void main(String[] args) {
        /*
        完成将一个中缀表达式转成后缀表达式的功能
        说明:
        1. 1+((2+3)*4)-5 => 转成  1 2 3 + 4 * + 5 –
        2. 因为直接对str 进行操作,不方便,因此 先将  "1+((2+3)*4)-5" -> 中缀的表达式对应的List
        即 "1+((2+3)×4)-5" -> ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]
        3. 将得到的中缀表达式对应的List -> 后缀表达式对应的List
           即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]  -> ArrayList [1,2,3,+,4,*,+,5,–]
        */

        String infixExpression = "1+((2+3)*4)-5";
        List<String> infixExpressionList = InfixToList(infixExpression);
        System.out.println("中缀表达式对应的list = " + infixExpressionList);//[1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]
        List<String> suffixExpreesionList = parseSuffixExpreesionList(infixExpressionList);
        System.out.println("后缀表达式对应的list = "+suffixExpreesionList);//[1, 2, 3, +, 4, *, +, 5, -]

    }


    //方法:将得到的中缀表达式对应的List -> 后缀表达式对应的List
    //即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]  -> ArrayList [1,2,3,+,4,*,+,5,–]
    public static List<String> parseSuffixExpreesionList(List<String>infixList){

        //初始化两个栈
        Stack<String>s1=new Stack<>();//代表运算符栈
        List<String>s2=new ArrayList<>();//代表中间结果栈   (注意:此处使用)

        for (String s : infixList) {
            if(s.matches("\\d+")){//如果是操作数,直接压入栈s2
                s2.add(s);
            }else if (s.equals("(")){//如果是左括号直接入栈是s1
                s1.push(s);
            }else if (s.equals(")")){//如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
                while (!s1.isEmpty()&&!s1.peek().equals("(")){
                    s2.add(s1.pop());
                }
                s1.pop();//丢弃左括号
            }else {//如果是运算符
                if(s1.isEmpty()||s1.peek().equals("(")){//如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
                    s1.push(s);
                }else if (Operation.getPriority(s)>Operation.getPriority(s1.peek())){//否则,若优先级比栈顶运算符的高,也将运算符压入s1;
                    s1.push(s);
                }else {//否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶运算符相比较;
                    while (!s1.isEmpty()&&Operation.getPriority(s)<=Operation.getPriority(s1.peek())){
                        s2.add(s1.pop());
                    }
                    s1.push(s);//还需要将s压入栈
                }
            }
        }
        //将s1中剩余的运算符依次弹出并加入s2
        while (!s1.isEmpty()){
            s2.add(s1.pop());

        }
        return s2;//注意因为是存放到List, 因此按顺序输出就是对应的后缀表达式对应的List
    }


    //将中缀表达式转成List
    public static List<String> InfixToList(String str) {

        ArrayList<String> list = new ArrayList<>();
        int i = 0;//这时是一个指针,用于遍历 中缀表达式字符串
        String keepNum ;// 对多位数的拼接
        char c;// 每遍历到一个字符,就放入到c
        do {
            c = str.charAt(i);
            if (c < 48 || c > 57) {//说明当前是一个操作符,直接入集合
                list.add(c + "");
                i++;
            } else {//如果是个数,考虑【多位数】的情况
                keepNum = "";
                while (i < str.length() && (c = str.charAt(i)) >= 48 && (c = str.charAt(i)) <= 57) {
                    keepNum += c;
                    i++;
                }
                list.add(keepNum);
            }
        } while (i < str.length());
        return list;
    }


}

//编写一个类 Operation 可以返回一个运算符 对应的优先级
class Operation{

    private static int ADD=1;
    private static int SUB=1;
    private static int MUL=2;
    private static int DIV=2;

    //返回操作符的优先级
    public static int getPriority(String operation){
        int req=0;
        switch (operation){
            case "+":
                req=ADD;
                break;
            case "-":
                req=SUB;
                break;
            case "*":
                req=MUL;
                break;
            case "/":
                req=DIV;
                break;
            default:
                System.out.println("不存在的操作符:"+operation);
                break;
        }
        return req;
    }
}

1.6.6 逆波兰计算器完整版

完整版的逆波兰计算器,功能包

1)+ - * / ( )

2) 多位数,支持小数,

3)容处, 滤任何空白字符,包括空格、制表符、换页符

/**
 * 完整版的逆波兰计算器,功能包括
 * 支持 + - * / ( )
 * 多位数,支持小数,
 * 兼容处理, 过滤任何空白字符,包括空格、制表符、换页符
 */
public class ReversePolishMultiCalc {

	 /**
     * 匹配 + - * / ( ) 运算符
     */
    static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";

    static final String LEFT = "(";
    static final String RIGHT = ")";
    static final String ADD = "+";
    static final String MINUS= "-";
    static final String TIMES = "*";
    static final String DIVISION = "/";

    /**
     * 加減 + -
     */
    static final int LEVEL_01 = 1;
    /**
     * 乘除 * /
     */
    static final int LEVEL_02 = 2;

    /**
     * 括号
     */
    static final int LEVEL_HIGH = Integer.MAX_VALUE;


    static Stack<String> stack = new Stack<>();
    static List<String> data = Collections.synchronizedList(new ArrayList<String>());

    /**
     * 去除所有空白符
     * @param s
     * @return
     */
    public static String replaceAllBlank(String s ){
        // \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]
        return s.replaceAll("\\s+","");
    }

    /**
     * 判断是不是数字 int double long float
     * @param s
     * @return
     */
    public static boolean isNumber(String s){
        Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
        return pattern.matcher(s).matches();
    }

    /**
     * 判断是不是运算符
     * @param s
     * @return
     */
    public static boolean isSymbol(String s){
        return s.matches(SYMBOL);
    }

    /**
     * 匹配运算等级
     * @param s
     * @return
     */
    public static int calcLevel(String s){
        if("+".equals(s) || "-".equals(s)){
            return LEVEL_01;
        } else if("*".equals(s) || "/".equals(s)){
            return LEVEL_02;
        }
        return LEVEL_HIGH;
    }

    /**
     * 匹配
     * @param s
     * @throws Exception
     */
    public static List<String> doMatch (String s) throws Exception{
        if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
        if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number");

        s = replaceAllBlank(s);

        String each;
        int start = 0;

        for (int i = 0; i < s.length(); i++) {
            if(isSymbol(s.charAt(i)+"")){
                each = s.charAt(i)+"";
                //栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈
                if(stack.isEmpty() || LEFT.equals(each)
                        || ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){
                    stack.push(each);
                }else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){
                    //栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈
                    while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){
                        if(calcLevel(stack.peek()) == LEVEL_HIGH){
                            break;
                        }
                        data.add(stack.pop());
                    }
                    stack.push(each);
                }else if(RIGHT.equals(each)){
                    // ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈
                    while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){
                        if(LEVEL_HIGH == calcLevel(stack.peek())){
                            stack.pop();
                            break;
                        }
                        data.add(stack.pop());
                    }
                }
                start = i ;    //前一个运算符的位置
            }else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
                each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
                if(isNumber(each)) {
                    data.add(each);
                    continue;
                }
                throw new RuntimeException("data not match number");
            }
        }
        //如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列
        Collections.reverse(stack);
        data.addAll(new ArrayList<>(stack));

        System.out.println(data);
        return data;
    }

    /**
     * 算出结果
     * @param list
     * @return
     */
    public static Double doCalc(List<String> list){
        Double d = 0d;
        if(list == null || list.isEmpty()){
            return null;
        }
        if (list.size() == 1){
            System.out.println(list);
            d = Double.valueOf(list.get(0));
            return d;
        }
        ArrayList<String> list1 = new ArrayList<>();
        for (int i = 0; i < list.size(); i++) {
            list1.add(list.get(i));
            if(isSymbol(list.get(i))){
                Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
                list1.remove(i);
                list1.remove(i-1);
                list1.set(i-2,d1+"");
                list1.addAll(list.subList(i+1,list.size()));
                break;
            }
        }
        doCalc(list1);
        return d;
    }

    /**
     * 运算
     * @param s1
     * @param s2
     * @param symbol
     * @return
     */
    public static Double doTheMath(String s1,String s2,String symbol){
        Double result ;
        switch (symbol){
            case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
            case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
            case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
            case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
            default : result = null;
        }
        return result;

    }

    public static void main(String[] args) {
        //String math = "9+(3-1)*3+10/2";
        String math = "12.8 + (2 - 3.55)*4+10/5.0";
        try {
            doCalc(doMatch(math));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

二 :递归

2.1 递归适用场景

看个实际应用场景,迷宫问题(回溯)(Recursion)

2.2 递归的概念

简单的说: 递归就是方法自己调用自己,每次调用时传入不同的变量.递归助于编程者解决复杂的问题,同时可以让代码变得简洁

2.3 递归的调用机制

列举两个小案例,来帮助大家理递归分学员已经学习过递归了,这里在给大家回顾一下递归调用机制

  1. 打印问题
  2. 阶乘
  3. 使用图解方式说明了递归的调用机制

代码演示:

/**
 * @author Wnlife
 * @create 2019-10-29 16:01
 *
 * 递归小测试
 */
public class RecursionTest {

    public static void main(String[] args) {

//        test1(5);
        factorial(5);
    }

    public static void test1(int n){
        if(n>2){
            test1(n-1);
        }
        System.out.println("n = " + n);
    }

    //阶乘
    public static int factorial(int n){

        if(n==1)
            return 1;
        else
            return factorial(n-1)*n;
    }
}

2.4 递归能解决什么样的问题

递归用于解决什么样的问题

1)种数学问题如: 8后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google编程大赛)

2)种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等.

3)用栈解决的问题-->第归代码比较简洁

2.5 递归需要遵守的重要规则

归需要遵守的重规则

1)行一方法,就创建一个新的受保护的独立空间(空间)

2)方法局部变量是独立的,不会相互影, 比如n

3)果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据.

4)归必须向退出递归的条件逼近,否则就是无限递,出现StackOverflowError死龟了:)

5)方法行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时方法行完毕或者返回时,就执行完毕。

2.6 迷宫问题

代码实现:

/**
 * @author Wnlife
 * @create 2019-10-29 17:41
 * <p>
 * 迷宫问题
 */
public class MiGong {

    public static void main(String[] args) {

        int[][] map = new int[8][7];
        //让左右墙全部为1
        for (int i = 0; i < 8; i++) {
            map[i][0] = 1;
            map[i][6] = 1;
        }
        //让上下墙全部为1
        for (int i = 1; i < 6; i++) {
            map[0][i] = 1;
            map[7][i] = 1;
        }
        //设置中间的墙
        map[3][1] = 1;
        map[3][2] = 1;
        //输出地图
        System.out.println("地图布局");
        for (int i = 0; i < 8; i++) {
            for (int j = 0; j < 7; j++) {
                System.out.print(map[i][j] + "  ");
            }
            System.out.println();
        }

        //使用递归回溯给小球找路
//        setWay(map, 1, 1);
        setWay2(map, 1, 1);

        //输出小球走过的路径,用2标记
        System.out.println("小球走过的路径");
        for (int i = 0; i < 8; i++) {
            for (int j = 0; j < 7; j++) {
                System.out.print(map[i][j] + "  ");
            }
            System.out.println();
        }

    }

    /**
     * 使用递归回溯来给小球找路
     * 说明:
     * 1. map 表示地图
     * 2. i,j 表示从地图的哪个位置开始出发 (1,1)
     * 3. 如果小球能到 map[6][5] 位置,则说明通路找到.
     * 4. 约定: 当map[i][j] 为 0 表示该点没有走过 当为 1 表示墙  ; 2 表示通路可以走 ; 3 表示该点已经走过,但是走不通
     * 5. 在走迷宫时,需要确定一个策略(方法) 下->右->上->左 , 如果该点走不通,再回溯
     *
     * @param map 地图
     * @param i   起始点横坐标
     * @param j   起始点纵坐标
     * @return 如果找到通路,就返回true, 否则返回false
     */
    public static boolean setWay(int[][] map, int i, int j) {

        if (map[6][5] == 2) {//通路已经找到
            return true;
        } else if (map[i][j] == 0) {//当前点没有走过
            map[i][j] = 2;
            if (setWay(map, i + 1, j)) {//向下走
                return true;
            } else if (setWay(map, i, j + 1)) {//向右走
                return true;
            } else if (setWay(map, i - 1, j)) {//向上走
                return true;
            } else if (setWay(map, i, j - 1)) {//向左走
                return true;
            } else {//说明该点上下左右都走不通,是死路
                map[i][j] = 3;
                return false;
            }
        } else {// 如果map[i][j] != 0 , 可能是 1, 2, 3
            return false;
        }

    }

    //改变一种行走策略:右->下->左->上
    public static boolean setWay2(int[][]map,int i,int j){
        if (map[6][5] == 2) {//通路已经找到
            return true;
        } else if (map[i][j] == 0) {//当前点没有走过
            map[i][j] = 2;
            if (setWay2(map, i, j+1)) {//向右走
                return true;
            } else if (setWay2(map, i+1, j)) {//向下走
                return true;
            } else if (setWay2(map, i, j-1)) {//向左走
                return true;
            } else if (setWay2(map, i-1,j)) {//向上走
                return true;
            } else {//说明该点上下左右都走不通,是死路,设置为已走过
                map[i][j] = 3;
                return false;
            }
        } else {// 如果map[i][j] != 0 , 可能是 1, 2, 3
            return false;
        }
    }

}

2.7 递归-八皇后问题(回溯算法)

2.7.1 八皇后问题介绍

皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法(92)

2.7.2 八皇后问题算法思路分析

1)一个皇后先放第一行第一列

2)二个皇后放在第二行第一列、然后判断是否OK果不OK续放在第二列、第三列、依次把所有列都放完,找到一个合

3)继续第三个皇后,还是第一列、第二列……直到第8个皇后也能放在一个不冲突的位置,算是找到了一个正确

4)当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到.

然后回头继续第一个皇后放第二列,后面继续循环执行 1,2,3,4的步骤

说明论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题. arr[8] = {0 , 4, 7, 5, 2, 6, 1, 3} //对应arr 下标 表示第几行,即第几个皇后,arr[i] = val , val 表示第i+1个皇后,放在第i+1行的第val+1列。

代码演示:

/**
 * @author Wnlife
 * @create 2019-10-29 20:45
 * <p>
 * 8皇后问题解法:
 * 一共有92中摆放方法,一共判断了15720次冲突
 */
public class Queue8 {

    //定义一个max表示共有多少个皇后
    int max = 8;
    //定义一个数组,存储皇后摆放位置的结果,比如:array={0, 4, 7, 5, 2, 6, 1, 3}
    int[] array = new int[max];
    static int count = 0;//摆放方法个数
    static int judgeCount = 0;//判断冲突次数

    public static void main(String[] args) {

        Queue8 queue8 = new Queue8();
        //从第0个皇后开始摆放
        queue8.check(0);
        System.out.printf("一共有%d中摆放方法,", count);
        System.out.printf("一共判断了%d次冲突", judgeCount);
    }

    /**
     * 放置第n个皇后
     *
     * @param n 代表第n个皇后
     */
    private void check(int n) {
        //当前方案的所有皇后已经放置好,可以输出一个这个摆放方式
        if (n == 8) {
            print();
            return;
        }
        //依次放入皇后,判断是否冲突  ,max代表棋盘最大的列数,每个皇后从每次从棋盘的第一列开始判断
        for (int i = 0; i < max; i++) {
            array[n] = i;//将第n个皇后摆放到第n列
            if (judge(n)) {//如果不冲突,则继续摆放下一个皇后
                check(n + 1);//递归摆放第n+1个皇后
            }
            //如果冲突,摆放到下一列,如果 i 循环到最大列数这个皇后还冲突,
            // 则跳转到上一个皇后,让上一个皇后后移下一列,然后判断是否冲突,
            // 上一个皇后不冲突后则继续向后,到下一个皇后,依次循环和递归判断
        }
    }

    /**
     * 判断第n个皇后排放的位置和前面几个皇后摆放的位置是否冲突
     * array[i]==array[n] 用来判断第n个皇后和前面的皇后是否在同一列
     * Math.abs(n-i)==Math.abs(array[n]-array[i] 用来判断是否处于同一对角线
     * 判断同一行没有必要,因为行数一直在递增
     *
     * @param n 第n个皇后
     * @return 是否冲突
     */
    private boolean judge(int n) {
        judgeCount++;
        for (int i = 0; i < n; i++) {
            if (array[i] == array[n] || Math.abs(n - i) == Math.abs(array[n] - array[i]))
                return false;
        }
        return true;
    }

    /**
     * 输出每一种皇后摆放方式的位置
     */
    private void print() {
        count++;
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
        }
        System.out.println();
    }

}

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数据结构与算法分析:C语言描述清晰版》是由美国著名计算机科学家Mark Allen Weiss所著,是一本介绍数据结构和算法的经典教材之一。该书内容丰富、深入浅出,既有基础知识的讲解,也有高级算法的探讨,适合计算机科学及其他相关专业的学生、计算机编程爱好者、程序员等人群。 本书主要内容包括线性结构、递归、树、排序和搜索算法、散列表、图等多个章节。其中,线性结构是数据结构的基础,包括线性表、、队列等数据结构的实现和操作。递归是一种特殊的函数调用方式,在算法设计中应用广泛,本书详细讲解了递归的原理和应用。树是一种重要的非线性数据结构,本书介绍了叉树、堆、AVL树等多种树形结构的实现和应用。排序和搜索算法是解决各种实际问题的重要工具,本书详细讲解了冒泡排序、插入排序、归并排序、快速排序等多种排序算法及分查找、哈希表查找等多种搜索算法。散列表是一种实现高效查找的数据结构,本书深入浅出地讲解了散列表的实现原理和应用。图是一种复杂的数据结构,在算法设计中具有很高的应用价值,本书介绍了图的表示方法、遍历方法和最短路径算法等多个方面的内容。 总的来说,《数据结构与算法分析:C语言描述清晰版》是一本综合性的计算机科学教材,既适合初学者入门学习,也适合中高级程序员深入研究。本书内容翔实,全面而深入,不仅介绍了数据结构和算法的基本知识,还讲解了实践中的应用技巧和注意事项,对于从事计算机科学相关领域的人员来说,具有很高的实用价值和参考意义。如果你想在数据结构和算法方面有更深入的了解,那么这本书绝对是你不可错过的参考资料。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值