美文网首页
重温:数据结构与算法 - 06栈

重温:数据结构与算法 - 06栈

作者: 雷小歪 | 来源:发表于2021-06-28 00:12 被阅读0次
    xwzz.jpg

    重温:数据结构与算法 - 开篇
    重温:数据结构与算法 - 复杂度分析(一)
    重温:数据结构与算法 - 复杂度分析(二)
    重温:数据结构与算法 - 数组
    重温:数据结构与算法 - 链表(一)
    重温:数据结构与算法 - 链表(二)

    叨叨两句

    说好的尽量日更o(╥﹏╥)o~,太懒了,最后两个练习题一直没敲,拖了两天才更,啪 啪~ 打脸!

    什么是栈

    本章介绍一种“操作受限”的数据结构,它仅支持:

    • 添加操作(入栈 - push)
    • 删除操作(出栈 - pop)

    并具备“先进后出,后进先出”特点:

    子弹匣.png

    图中,正在装填的弹匣就是典型的栈结构,先装入的子弹被压入了底部,后装入的在顶部,当使用时优先使用顶部子弹。那么,在内存中给予这种模式存储数据的容器,称之为:

    内存结构图.png

    栈的实现

    根据定义,很容抽象出一个只能新增、删除的操作接口来:

    public interface Stack<T> {
      //压栈
      boolean push(T item);
    
      //弹栈
      T pop();
      
      // 返回栈顶数据,不弹栈
      T peek();
    
      //是否为空
      boolean isEmpty();
    
      //栈中数据的数量
      int size();
    
      //清空
      void clear();
    }
    

    有了操作标准,拿什么来存储实际数据?

    • 数组 ?
    • 链表 ?

    细想一下,无论是使用数组还是链表存储数据,只要出栈、入栈规则满足“先进后出、后进先出”,那它就是我们想要的栈型结构,当然不同数据结构实现的栈,底层肯定还是具备其本身的数据结构特性的,例如数组实现的栈型结构内存是连续的,链表则是不连续的,我们把数组实现的栈,称之:顺序栈 ; 链表实现则为:链式栈

    下面以数组为例,实现一个栈容器:

    public class ArrayStack<T> implements Stack<T> {
    
       private static final int DEFAULT_SIZE = 10;
    
      // 栈中数据数量
      private int count;
    
      // 栈中数据(数组实现)
      private Object[] items;
      private Object curItem;
    
      // 当前栈能存储的最大容量
      private int maxSize;
    
      public ArrayStack() {
        this(DEFAULT_SIZE);
      }
    
      public ArrayStack(int initSize) {
        this.items = new Object[initSize];
        this.maxSize = initSize;
      }
    
      @Override
      public boolean push(T item) {
        // 入栈
        if (count == maxSize) {
            //扩容
            grow();
        }
        items[count] = item;
        ++count;
        curItem = item;
        return true;
      }
    
      private void grow() {
        Object[] temp = items;
        items = new Object[temp.length * 2];
        for (int i = 0; i < temp.length; i++) {
            items[i] = temp[i];
        }
        maxSize = items.length;
      }
    
      @Override
      public T pop() {
        // 出栈
        if (count == 0) return null;
        T item = (T) items[count - 1];
        items[count - 1] = null;
        --count;
        if (count == 0) {
            curItem = null;
        } else {
            curItem = items[count - 1];
        }
        return item;
      }
    
      @Override
      public T peek() {
        return (T) curItem;
      }
    
      @Override
      public boolean isEmpty() {
        return count == 0;
      }
    
      @Override
      public int size() {
        return count;
      }
    
      @Override
      public void clear() {
        if (isEmpty()) return;
        for (int i = 0; i < size(); i++) {
            items[i] = null;
        }
        count = 0;
      }
    
      public void print() {
         for (int i = 0; i < count; i++) {
            System.out.print(items[i] + "\t");
        }
        System.out.println();
      }
    }
    

    测试:

        ArrayStack<String> stack = new ArrayStack<>();
        stack.push("1");
        stack.push("2");
        stack.push("3");
        stack.push("4");
        stack.push("5");
        stack.print();
    
        String item1 = stack.pop();
        System.out.println(item1);
    
        String item2 = stack.pop();
        System.out.println(item2);
    
        stack.print();
    

    输出:

    1   2   3   4   5   
    5
    4
    1   2   3
    
    • 时间复杂度:

      • 最好时间复杂度:空间够用,直接入栈,复杂度 O(1)

      • 最坏时间复杂度:空间不足,动态扩容,复杂度 O(n)

      • 平均时间复杂度:还记得复杂度分析(二)中提到的平摊分析法吗?随着数据的改变,算法出现某种固定规律,这里明显当执行完n次O(1)插入操作后,都需要进行一次O(n)的数据位移,正好可以把这n次搬运平摊给前面各自1次的插入操作,得到平摊时间复杂度:O(1)

    • 空间复杂度:

      注意,这里存储数据需要一个大小为 n 的数组,并不是说空间复杂度就是 O(n)。因为,这 n 个空间是必须的,无法省掉。我们说空间复杂度的时候,是指除了原本的数据存储空间外,算法运行还需要额外的存储空间。所以栈的插入操作空间复杂度是:O(1)

    栈 - 练习1:运算表达式

    要求给出一个运算表达式字符串,计算出表达式结果。
    输入字符串: 3 + 2 * 5 - 4 / 2
    输出 :11

    思路:

    • 1、利用两个栈,一个用来保存数字,另一个用来保存运算符,从左向右遍历表达式;
    • 2、当遇到数字,我们就直接压入数字栈;
    • 3、当遇到运算符,就与运算符栈的栈顶元素进行比较,若比运算符栈顶元素优先级高,就将当前运算符压入栈;
    • 4、若比运算符栈顶元素的优先级低或者相同,从运算符栈中取出栈顶运算符,从操作数栈顶取出2个操作数,然后进行计算,把计算完的结果压入数字栈;
    • 5、重复第3步的操作,继续比较栈顶操作符。
    private static void computeExp(String exp) {
        ArrayStack<String> numStack = new ArrayStack<>();
        ArrayStack<String> opStack = new ArrayStack<>();
        char[] chars = exp.toCharArray();
        String preItem = null;
        for (int i = 0; i < chars.length; i++) {
            String item = String.valueOf(chars[i]);
    
            if (isOp(item)) {
                // 操作符
                while (true) {  
                    if (opStack.isEmpty()) {
                        // 直接入栈
                        opStack.push(item);
                        break;
                    }
                    // 比较优先级
                    String preOp = opStack.pop();
                    if (opLv(item) > opLv(preOp)) {
                        // 大于
                        opStack.push(preOp);
                        opStack.push(item);
                        break;
                    } else {
                        // 小于等于 , 取出2个数,和栈顶操作符进行运算
                        String num1 = numStack.pop();
                        String num2 = numStack.pop();
                        int result = compute(num2, num1, preOp);
                        numStack.push(String.valueOf(result));
                    }
                }
            } else {
                // 数字
                if (!numStack.isEmpty() && !isOp(preItem)) {
                    // 前一个也是数字,拼接后入数字栈
                    preItem = numStack.pop();
                    item = preItem + item;
                }
                // 入数字栈
                numStack.push(item);
            }
            preItem = item;
        }
    
        // 清空栈,计算最终结果
        int len =  opStack.size();
        for(int i = 0 ; i < len ; i++){
           String num1 =   numStack.pop();
           String num2 = numStack.pop();
           String op = opStack.pop();
           int result = compute(num2 , num1 , op);
           numStack.push(String.valueOf(result));
        }
        System.out.println( exp + " = " +  numStack.pop());
    }
    
    private static int compute(String num2, String num1, String op) {
        int n2 = Integer.parseInt(num2);
        int n1 = Integer.parseInt(num1);
        switch (op) {
            case "+":
                return n2 + n1;
            case "-":
                return n2 - n1;
            case "*":
                return n2 * n1;
            case "/":
                return n2 / n1;
            default:
                throw new RuntimeException("no op");
        }
    
    }
    
    private static boolean isOp(String item) {
        if (item == null) return false;
        return item.matches("[+\\-*/]");
    }
    
    private static int opLv(String op) {
        switch (op) {
            case "+":
            case "-":
                return 1;
            case "*":
            case "/":
                return 2;
            default:
                throw new RuntimeException("no op");
        }
    }
    

    测试:

        computeExp("3+10*5-8/2");
        computeExp("4+8*5/4-7");
        computeExp("50/2-3*7+1");
    

    输出:

      3+10*5-8/2 = 49
      4+8*5/4-7 = 7
      50/2-3*7+1 = 5
    

    时间复杂度:O(n²)
    空间复杂度:O(1)

    栈 - 练习2:模拟浏览器前进后退功能

    思路:
    利用两个栈,X栈用于存储新打开的页面,Y栈存储后退页面
    当点击后退时,将X出栈一个页面,并存入Y栈
    当点击前进时,将Y出栈一个页面,并存入X栈
    当打开新页面时,入X栈,清空Y栈

     private static void go(ArrayStack<String> goStack, ArrayStack<String> backStack) {
        // 当点击前进时,将Y出栈一个页面,并存入X栈
        if(backStack.isEmpty()) {
            System.out.println("go:" + goStack.peek());
            return;
        }
        String page = backStack.pop();
        goStack.push(page);
        System.out.println("go:" + goStack.peek());
    }
    
    private static void back(ArrayStack<String> goStack, ArrayStack<String> backStack) {
        // 当点击后退时,将X出栈一个页面,并存入Y栈
        String page = goStack.pop();
        backStack.push(page);
        System.out.println("back:" + goStack.peek());
    }
    
    private static void open(String page, ArrayStack<String> goStack, ArrayStack<String> backStack) {
        // 当打开新页面时,入X栈,清空Y栈
        goStack.push(page);
        backStack.clear();
        System.out.println("open:" + goStack.peek());
    }
    

    测试:

        ArrayStack<String> goStack = new ArrayStack<>();
        ArrayStack<String> backStack = new ArrayStack<>();
        open("页面1", goStack, backStack);
        open("页面2", goStack, backStack);
        open("页面3", goStack, backStack);
        open("页面4", goStack, backStack);
        open("页面5", goStack, backStack);
        back(goStack, backStack);
        back(goStack , backStack);
        go(goStack, backStack);
        go(goStack, backStack);
        go(goStack, backStack);
    

    输出:

    open:页面1
    open:页面2
    open:页面3
    open:页面4
    open:页面5
    back:页面4
    back:页面3
    go:页面4
    go:页面5
    go:页面5
    

    时间复杂度:O(1)
    空间复杂度:O(1)

    相关文章

      网友评论

          本文标题:重温:数据结构与算法 - 06栈

          本文链接:https://www.haomeiwen.com/subject/tvbnyltx.html