栈
栈(Stack)也是一种线性结构。
相比数组,栈对应的操作是数组的子集。
只能从一端添加元素,也只能从一端取出元素,这一端成为栈顶。
栈是一种后进先出的数据结构,Last In First Out(LIFO)
栈的应用:
无处不在的
Undo
操作(撤销)即编译器会使用一个栈来记录我们的输入数据,当我们想撤销输入的时候就是栈的元素出栈。程序调用的系统栈
当我们在写代码的时候,存在函数调用的情况下,就会使用系统栈。系统栈会记录每次切换函数的位置当系统不知道下步执行方向的时候,将会去系统栈中查找之前入栈的位置。这就是代码中存在子逻辑调用的原理。这个对于理解递归很有帮助。
栈的实现
1 | Stack<E> |
从用户的角度看,支持这些操作就好,具体底层实现,用户不关心。实际底层有多种实现方式。
这里栈的实现通过先做一个接口然后继承接口来实现
我们的我们创建的这个ArrayStack 是基于之前创建的Array的 因此内部很多方法都是之前的
1 | public class ArrayStack<E> implements Stack<E> { |
我们测试下
1 | public class Main { |
栈的时间复杂度
栈的应用
我们上面说过两个栈的应用:
undo
操作 – 编辑器
系统调用栈 – 操作系统
接下来我们学习下另一个应用:括号匹配—编译器
我们按照LeetCode
上的一个题目有效的括号来学习下。
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
1 | import java.util.Stack; |
上面是我们使用内置的Stack
来实现的。
我们用Python实现下。
1 | class Solution: |
我们如果想使用自己定义的ArrayStack
只需改动一行代码
当我们想在LeetCode
中使用我们自己写的代码的时候,就需要将之前创建的类当成Solution
类的一个内部类。
队列
队列(Queue)也是一种线性结构
相比数组,队列对应的操作是数组的子集
只能从一段(队尾)添加元素,只能从另一端(队首)取出元素
队列是一种先进先出的数据结构(先到先得)Frist In First Out(FIFO)
队列的实现
1 | Queue<E> |
我们要实现自己的队列,需要实现上面的方法。
和之前学习栈一样,我们使用最开始的Array
进行复用。
先看下目录结构
其中Array
为复用的
接口Queue
主要规范我们要实现的函数
1 | public interface Queue<E> { |
类ArrayQueue
是我们自己实现的类
1 | public class ArrayQueue<E> implements Queue<E> { |
数组队列的复杂度分析
我们看到在入队的时候因为可能扩容,因此在均摊算法下,时间复杂度为O(1)
在出队的时候,因为所有的元素都需要往前挪一位,因此时间复杂度为O(n)
有没有什么方法可以使这两个方法的复杂度都为O(1)
呢?
循环队列
对于基于Array
的队列,每次删除第一个元素的时候,后面的元素全部往前挪一位。因此其时间复杂度是O(n)
我们可以将队首和队尾标识出来,即使队首被删除我们只需要将队首维护的指针增加1就好了。
在循环队列中front
指向的是第一个值位置,tail
指向的是下一个要存放值的位置。
我们看下在最开始队列为空的情况下
在最开始队列为空的时候front
和tail
指向同一个位置。其他时候这两个是不会指向同一个位置的。
每次新增数据,我们将tail
的位置往后挪移位即tail++
。
当有元素出队的时候,只需要将front
位置往后挪一位即front++
。
为什么说是循环数组呢?我们看下在初始数组存放到第七位的时候
当数据存放到第七个位置的时候tail
并没有tail++
而是直接挪到了第一位。这是因为我们在之前删除了前面的元素,导致队列前面为空。我们可以利用起来这些被删除的空间,因此被称为循环队列。
我们把上面的数组看成一个环形,当数据填充到第七位的时候我们的下以为不是tail
加一而是tail+1 % 数据长度
。因此tial
从7
变成0
的公式是(7+1)%8
。
上面我们说过当tail
和front
指向同一个位置的时候表示队列为空,在上图这种情况是不能再往队列中新增数据了。我们规定(tail + 1) % c == fron
表示队列为满。因为当我们再新增一个元素的时候,front
和tail
将指向同一个位置。因此当队列只剩最后一个空间的时候,我们可以认为队列已经满了。
1 | public class LoopQueue<E> implements Queue<E> { |
循环队列和数组队列的输出是一样的,并且对外的函数是一致的。不过解决了出队的O(n)
。
循环队列的复杂度分析
因为出队可能涉及到缩容操作,因此使用均摊算法的时间复杂度为o(n)
。
我们看下ArrayQueue
和LoopQueue
实际运行比较
1 | import java.util.Random; |
两者的主要区别是在出队的时候使用ArrayQueue
是O(n)
而LoopQueue
的出队在均摊算法下是O(1)
。