事实上,从功能上来说,数组或链表确实可以替代栈,但你要知道,特定的数据结构是对特定场景的抽象,而且,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错。
    当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,这时我们就应该首选“栈”这种数据结构。

    1. // 基于数组实现的顺序栈
    2. public class ArrayStack {
    3. private String[] items; // 数组
    4. private int count; // 栈中元素个数
    5. private int n; //栈的大小
    6. // 初始化数组,申请一个大小为n的数组空间
    7. public ArrayStack(int n) {
    8. this.items = new String[n];
    9. this.n = n;
    10. this.count = 0;
    11. }
    12. // 入栈操作
    13. public boolean push(String item) {
    14. // 数组空间不够了,直接返回false,入栈失败。
    15. if (count == n) return false;
    16. // 将item放到下标为count的位置,并且count加一
    17. items[count] = item;
    18. ++count;
    19. return true;
    20. }
    21. // 出栈操作
    22. public String pop() {
    23. // 栈为空,则直接返回null
    24. if (count == 0) return null;
    25. // 返回下标为count-1的数组元素,并且栈中元素个数count减一
    26. String tmp = items[count-1];
    27. --count;
    28. return tmp;
    29. }
    30. }

    你不用死记硬背入栈、出栈的时间复杂度,你需要掌握的是分析方法。能够自己分析才算是真正掌握了。现在我就带你分析一下支持动态扩容的顺序栈的入栈、出栈操作的时间复杂度。
    对于出栈操作来说,我们不会涉及内存的重新申请和数据的搬移,所以出栈的时间复杂度仍然是 O(1)。但是,对于入栈操作来说,情况就不一样了。当栈中有空闲空间时,入栈操作的时间复杂度为 O(1)。但当空间不够时,就需要重新申请内存和数据搬移,所以时间复杂度就变成了 O(n)。
    也就是说,对于入栈操作来说,最好情况时间复杂度是 O(1),最坏情况时间复杂度是 O(n)。那平均情况下的时间复杂度又是多少呢?还记得我们在复杂度分析那一节中讲的摊还分析法吗?这个入栈操作的平均情况下的时间复杂度可以用摊还分析法来分析。我们也正好借此来实战一下摊还分析法。为了分析的方便,我们需要事先做一些假设和定义:

    • 栈空间不够时,我们重新申请一个是原来大小两倍的数组;
    • 为了简化分析,假设只有入栈操作没有出栈操作;
    • 定义不涉及内存搬移的入栈操作为 simple-push 操作,时间复杂度为 O(1)。

    image.png
    你应该可以看出来,这 K 次入栈操作,总共涉及了 K 个数据的搬移,以及 K 次 simple-push 操作。将 K 个数据搬移均摊到 K 次入栈操作,那每个入栈操作只需要一个数据搬移和一个 simple-push 操作。以此类推,入栈操作的均摊时间复杂度就为 O(1)。
    通过这个例子的实战分析,也印证了前面讲到的,均摊时间复杂度一般都等于最好情况时间复杂度。因为在大部分情况下,入栈操作的时间复杂度 O 都是 O(1),只有在个别时刻才会退化为 O(n),所以把耗时多的入栈操作的时间均摊到其他入栈操作上,平均情况下的耗时就接近 O(1)。