数据结构基础----栈

栈的概括

首先,相对栈这种数据结构而言,我们可以理解成,它本质就是一个数组,我们把数据排开来放,但是我们规定添加元素的时候,我们只能从栈的一端添加元素,而取出元素的时候,也只能从同一端来取出元素 (取出一端通常我们称为栈顶)。在形成了栈这种数据结构之后,在我们的计算机世界中,对于组建逻辑有着非常重要的作用。

在这里我进行了一个简单的图示,我可以理解成,如图示:

这样一个栈,栈顶就在上方。我们向栈中添加一个数据(即把它放进栈中),这个过程通常称之为是入栈

栈的应用

在我们学习无论是经典的算法也好,还是算法设计也好,都将不可避免再次接触到栈这种看似很简单,但其实应用非常广泛的数据结构,在这里我向大家讲两个栈相关的应用。

第一个栈的应用

首先说的第一个栈的应用就是大家都是用过编辑器,无论是word,还是写代码用的IDE,都会有一个叫做undo的操作 (即撤销),比如在文本中输入一句话,或者修改一个内容,修改后发现之前的修改可能有问题,我想撤销到修改前怎么办呢?就需要执行下undo操作了

对于编辑器来说,undo操作的原理是什么呢?

其实就是靠一个来进行维护的。
有这样一个过程,比如现在输入一句话,

  1. 输入第一个词沉迷,编辑器就会记录这个动作。这个记录的方式,其实就是把这个动作放进一个栈中,记录了沉迷两个字一下
  2. 然后,我在输入第二个词学习,编辑的栈就会再做一次记录,以同样的方式压入这个栈中
  3. 下面我可能要输入无法,但不小心输成无天,栈中也记录了这个动作。但你意识到输错,需要撤销这个操作。这个操作无论是点击撤销按钮还是快捷键ctrl+z
这里做的是什么事情呢?

其实是从编辑器的这个栈中拿出栈顶的元素,而栈顶元素记录的操作是输入无天,执行了撤销就删除了两个字,这即便是撤消的原理
如图所示:

通过这个例子,我们向栈顶推入新的元素从栈顶中取出元素,就可以作出撤销这样一个看起来很高级的逻辑

第二个栈的应用

这个例子便是,程序调用所使用的系统栈。如图所示:

事实上,在我们程序调用的过程中,经常会出现在一个逻辑的中间,先终止然后跳到另外的一个逻辑去执行。也就是所谓的子函数调用的这个过程。本质是使用一个这样的数据结构来记录我们程序的调用过程。而这个调用过程如图所示:

  1. 首先,程序执行A这个函数,开始执行a的第1行第2行,在第2行的时候,程序要跳转去执行B函数而暂时中断A这个函数。此时在我们的系统栈中就会记录一个信息,这个信息暂时叫做A2(之前程序执行到A函数的第2行,下同),在这里进行了中断
  2. 现在开始执行B函数了,依然是执行第1行第2行。在第2行要调用C这个子函数,同样在系统栈中压入一个信息并中断B这个函数,这个信息暂时叫做B2
  3. 现在开始执行C这个函数,C函数顺着它第1行第2行第3行,就直接执行完成了
  4. 下一步要执行谁?此时要看一下系统栈,对于系统栈来说,栈顶的元素是B2,计算机就知道了刚才是执行B函数的第2行时中断了,就跳回到B2这个位置继续执行
  5. 这样通过这个系统栈,就成功的帮助我们的计算机找到了上一次中断的位置。当然了,当回到B2这个位置继续执行时,这个记录的B2也就没用了,它就可以出栈
  6. 下一步就是执行B3,然后B函数也执行完成了。再次查看系统栈了,栈顶元素还有一个A2,就回到A这个函数的第2行去继续执行,整个计算机的程序逻辑就正确的跳到了这里,而这个记录的A2也就没用了,它就可以出栈
  7. 紧接着继续执行A的第3行,然后A执行完成,最后再次查看系统栈已为空,没有可执行的了。继而这整个过程都已经执行完了

这就是在子函数进行调用的过程,当一个子过程执行完成之后,可以自动的回到上层,调用中断的位置,继续执行下去。而背后的原因,是因为有这样的一个系统栈,来记录每一次调用过程中所中断的那个点

栈应用的总结

通过这个例子可以看到,我们使用栈这样的一个看似非常简单的数据结构,解释清楚了一个在计算机领域非常复杂的问题,就是这种子过程子逻辑的调用,在我的编译器内部运行实现的这个机制是什么。

栈的实现

针对栈这种数据结构来说,基本上写了只涉及这5个操作,分别是

  1. 向栈中添加一个元素,也都是入栈,通常叫做push
  2. 从栈中拿出栈顶的元素,也叫做出栈,通常叫做pop
  3. 看一下栈顶的元素是谁,这个动作通常叫做peek,有些命名叫做top
  4. 看一下里一共有多少个元素getSize
  5. 判断一下栈是否为空isEmpty

在这里,为了让我们整个程序的逻辑更加的清晰,同时也是为了支持面向对象的一些特性,比如说支持多态性。那么,我在这个代码设计上,我设计一个Stack的接口,这个接口中定义了这5种操作。现在,基于我们动态数组来实现的一个栈,这个栈叫做ArrayStack(实际上是实现了我们设计的这个Stack这个接口)。
Stack接口设计如下:

public interface Stack<E> {
    void push(E e);
    E pop();
    E peek();
    int getSize();
    boolean isEmpty();
}

ArrayStack实现代码如下:

public class ArrayStack<E> implements Stack<E>{

    Array<E> array;

    public ArrayStack(){
        array = new Array();
    }

    public ArrayStack(int capacity){
        array = new Array(capacity);
    }

    @Override
    public void push(E e) {
        array.addLast(e);
    }

    @Override
    public E pop() {
        return array.removeLast();
    }

    @Override
    public E peek() {
        return array.getLast();
    }

    @Override
    public int getSize() {
        return array.getSize();
    }

    @Override
    public boolean isEmpty() {
        return array.isEmpty();
    }

    public int getCapacity(){
        return array.getCapacity();
    }
}

栈的复杂度分析

基于动态数组,真正的实现栈是非常方便的。最后我们还可以再看一下,ArrayStack所有操作对应的时间复杂度,它们都是O(1)的,但是在这里关键就是,对于push(),pop()操作来说,每一次我只向栈的最后一个位置去推入元素,删除元素也只从最后一个位置拿出元素,所以这个时间复杂度是O(1)的。而如果一旦触发了resize的话,经过均摊复杂度分析之后,它的时间复杂度依然是O(1) ,所以对于我们这个栈来说,在时间性能上是非常的好的。

案例实现:

  • Array类的业务逻辑如下:
  • public class Array<E> {
    
        private E[] data;  //设置为private,不希望用户从外部直接获取这些信息,防止用户篡改数据
        private int size;
    
        //构造函数,传入数组的容量capacity构造Array
        public Array(int capacity) {
            data = (E[]) new Object[capacity];
            size = 0;
        }
    
        //无参数构造函数,默认数组容量capacity=10
        public Array() {
            this(10);    //这里的capacity是IDE自动添加的提示信息,实际不存在
        }
    
        //获取数组中的元素个数
        public int getSize() {
            return size;
        }
    
        //获取数组的容量
        public int getCapacity() {
            return data.length;
        }
    
        //判断数组是否为空
        public boolean isEmpty() {
            return size == 0;
        }
    
        //向数组末尾添加一个新元素e
        public void addLast(E e) {
            add(size, e);
        }
    
        //向数组开头添加一个新元素e
        public void addFirst(E e) {
            add(0, e);
        }
    
        //在index位置插入一个新元素e
        public void add(int index, E e) {
    
            if (index < 0 || index > size) {
                throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size");
            }
            if (size == data.length) {
                resize(2 * size); //扩大为原容量的2倍
            }
            for (int i = size - 1; i >= index; i--) {
                data[i + 1] = data[i];
            }
            data[index] = e;
            size++;
        }
    
        //获取index位置的元素
        public E get(int index) {
            if (index < 0 || index >= size) {
                throw new IllegalArgumentException("Get failed. Index is illegal.");
            }
            return data[index];
        }
    
        //获取最后一个元素
        public E getLast() {
            return get(size - 1);
        }
    
        //获取开头的元素
        public E getFirst() {
            return get(0);
        }
    
        //修改index位置的元素为e
        public void set(int index, E e) {
            if (index < 0 || index >= size) {
                throw new IllegalArgumentException("Set failed. Index is illegal.");
            }
            data[index] = e;
        }
    
        //查找数组中是否存在元素e
        public boolean contains(E e) {
            for (int i = 0; i < size; i++) {
                if (data[i].equals(e)) {
                    return true;
                }
            }
            return false;
        }
    
        //查看数组中元素e的索引,若找不到元素e,返回-1
        public int find(E e) {
            for (int i = 0; i < size; i++) {
                if (data[i].equals(e)) {
                    return i;
                }
            }
            return -1;
        }
    
        //删除掉index位置的元素,并且返回删除的元素
        public E remove(int index) {
    
            if (index < 0 || index >= size) {
                throw new IllegalArgumentException("Remove failed. Index is illegal.");
            }
    
            E ret = data[index];
    
            for (int i = index + 1; i < size; i++) {
                data[i - 1] = data[i];
            }
            size--;   //data[size]会指向一个类对象,这部分空间不会被释放loitering objects
            data[size] = null;
    
            if (size == data.length / 4 && data.length / 2 != 0) {
                resize(data.length / 2);  //被利用的空间等于总空间的一半时,将数组容量减少一半
            }
            return ret;
        }
    
        //删除掉数组开头的元素,并返回删除的元素
        public E removeFirst() {
            return remove(0);
        }
    
        //删除掉数组末尾的元素,并返回删除的元素
        public E removeLast() {
            return remove(size - 1);
        }
    
        //如果数组中有元素e,那么将其删除,否则什么也不做
        public void removeElement(E e) {
            int index = find(e);
            if (index != -1) {
                remove(index);
            }
        }
    
    
        @Override
        public String toString() {    //覆盖父类的toString方法
    
            StringBuilder res = new StringBuilder();
            res.append(String.format("Array: size=%d, capacity=%d\n", size, data.length));
            res.append('[');
            for (int i = 0; i < size; i++) {
                res.append(data[i]);
                if (i != size - 1) {
                    res.append(", ");
                }
            }
            res.append(']');
            return res.toString();
        }
    
        private void resize(int newCapacity) {
            E[] newData = (E[]) new Object[newCapacity];
            for (int i = 0; i < size; i++) {
                newData[i] = data[i];
            }
            data = newData;
        }
    }
    

4.. 对我们实现的栈进行测试:

public class Main {

    public static void main(String[] args) {

        ArrayStack<Integer> stack = new ArrayStack<>();

        //测试入栈push
        for (int i = 0; i < 5; i++) {
            stack.push(i);
            System.out.println(stack);
        }

        //测试出栈
        stack.pop();
        System.out.println(stack);
    }
}
  • 输出结果如下:
  • Stack: [0] top
    Stack: [0, 1] top
    Stack: [0, 1, 2] top
    Stack: [0, 1, 2, 3] top
    Stack: [0, 1, 2, 3, 4] top
    Stack: [0, 1, 2, 3] top
    

5.. 栈的时间复杂度分析

  • Stack<E>
    ·void push(E)    O(1) 均摊
    ·E pop()    O(1)   均摊
    ·E peek()    O(1)
    ·int getSize()    O(1)
    ·boolean isEmpty()    O(1)

    6.. 栈的另外一个应用——括号匹配(leecode的第 20题)

  • 分析:第一:初始化stack,遍历字符序列,将所有左括号的字符存放到stack中;

  •             第二:如果stack为空,表示没有左括号返回false;

  •             第三:弹出stack顶元素与剩余字符匹配,匹配都不成功,返回false ;

  •            第四:匹配完后只有stack为空才表示匹配成功。

  • 注意:栈顶元素反映在嵌套的层次关系中,最近的需要匹配的元素。

  • 业务逻辑如下:
  • import java.util.Stack;
    
    class Solution {
        public boolean isValid(String s) {
            Stack<Character> stack = new Stack<>();
            for (int i = 0; i < s.length(); i++) {
                char c = s.charAt(i);
                if (c == '(' || c == '[' || c == '{') {
                    stack.push(c);
                } else {
                    if (stack.isEmpty()) {
                        return false;
                    }
                    char topChar = stack.pop();
                    if (topChar == '(' && c != ')') {
                        return false;
                    }
                    if (topChar == '[' && c != ']') {
                        return false;
                    }
                    if (topChar == '{' && c != '}') {
                        return false;
                    }
                }
            }
            return stack.isEmpty();   //这里很巧妙
           //此时不能return true,因为如果还有元素,表示匹配失败
        }
    
        //测试
        public static void main(String[] args){
            System.out.println((new Solution()).isValid("()"));  //true
            System.out.println((new Solution()).isValid("()[]}{"));  //false
            System.out.println((new Solution()).isValid("({[]})"));  //true
            System.out.println((new Solution()).isValid("({)}[]"));  //false
        }
    }

     

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值