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

xwzz.jpg

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

叨叨两句

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

什么是栈

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

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

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

子弹匣.png

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

内存结构图.png

栈的实现

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

public interface Stack {
  //压栈
  boolean push(T item);

  //弹栈
  T pop();
  
  // 返回栈顶数据,不弹栈
  T peek();

  //是否为空
  boolean isEmpty();

  //栈中数据的数量
  int size();

  //清空
  void clear();
}

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

  • 数组 ?
  • 链表 ?

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

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

public class ArrayStack implements Stack {

   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 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 numStack = new ArrayStack<>();
    ArrayStack 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 goStack, ArrayStack 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 goStack, ArrayStack backStack) {
    // 当点击后退时,将X出栈一个页面,并存入Y栈
    String page = goStack.pop();
    backStack.push(page);
    System.out.println("back:" + goStack.peek());
}

private static void open(String page, ArrayStack goStack, ArrayStack backStack) {
    // 当打开新页面时,入X栈,清空Y栈
    goStack.push(page);
    backStack.clear();
    System.out.println("open:" + goStack.peek());
}

测试:

    ArrayStack goStack = new ArrayStack<>();
    ArrayStack 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栈)