栈是一种线性结构。html
相比数组,栈对应的操做是数组的子集。java
栈只能从一端添加元素,也只能从一端取出元素,这一端称为栈顶。算法
栈是一种 后进先出 (LIFO: Last In First Out) 的数据结构。数组
撤销(Undo)操做
网络
程序调用的系统栈
数据结构
对于栈这种数据结构,实现起来是十分简单的,这里实现如下几个操做:app
入栈:void push(E element)
dom
出栈: E pop()
ide
查看栈顶元素: E peek()
函数
获取栈中元素个数: int getSize()
判断栈是否为空: boolean: isEmpty()
对于代码的具体实现,可让其支持多态性。因此能够设计一个接口 Stack 定义上面这 5 个栈支持的操做,再设计一个类 ArrayStack 来实现这个接口。
对于 ArrayStack 这个类,实质上是基于以前实现的动态数组类 Array 来实现的一个数组栈。由于对于栈而言,栈对应的操做是数组的子集。能够把栈当成一个数组来看待。
对于 Array 类的具体实现过程,可查看另外一篇 文章 。
在编写栈的具体代码以前,先往工程中导入以前实现的动态数组类 Array,该类代码可从以前的文章中查阅, 由于要基于该类来实现数组栈。
ArrayStack 类的实现:
由于在 Array 类中已经实现了不少操做数组的方法。因此对于 ArrayStack 类实现接口中的方法时,只需复用 Array 类中的方法便可。
基于 Array 类实现 ArrayStack 类也有一些好处:
和 Array 同样拥有了动态伸缩容量的功能,咱们不须要关心栈的容量是否够用,由于容量会动态地进行扩大和缩小。
对于一些非法变量的判断直接复用了 Array 类中的代码,不须要重复编写。
同时,也能够为 ArrayStack 类添加一个接口中没有的方法 getCapacity 提供给用户获取栈的容量。这个方法是这个类中特有的。
在实现 peek 方法以前,能够在 Array 类中扩展两个新方法用于获取数组末尾和数组首部的元素,方便在 ArrayStack 类和后续队列的实现中直接复用。
/** * 获取数组的最后一个元素 * * @return 返回数组的最后一个元素 */ public E getLast() { return get(size - 1); } /** * 获取数组的第一个元素 * * @return 返回数组的第一个元素 */ public E getFirst() { return get(0); }
Stack 接口类代码以下:
/** * 定义栈的基本操做的接口 * 支持泛型 * * @author 踏雪寻梅 * @date 2020/1/8 - 19:20 */ public interface Stack<E> { /** * 获取栈中元素个数 * * @return 栈中若是有元素,返回栈中当前元素个数;栈中若是没有元素返回 0 */ int getSize(); /** * 判断栈是否为空 * * @return 栈为空,返回 true;栈不为空,返回 false */ boolean isEmpty(); /** * 入栈 * 将元素 element 压入栈顶 * * @param element 入栈的元素 */ void push(E element); /** * 出栈 * 将当前栈顶元素出栈并返回 * * @return 返回当前出栈的栈顶元素 */ E pop(); /** * 查看当前栈顶元素 * * @return 返回当前的栈顶元素 */ E peek(); }
ArrayStack 类代码实现以下:
/** * 基于以前实现的动态数组类 Array 实现的数组栈类 ArrayStack * 一样支持泛型 * * @author 踏雪寻梅 * @date 2020/1/8 - 19:26 */ public class ArrayStack<E> implements Stack<E> { /** * 动态数组 array * 基于 array 实现栈的操做 */ private Array<E> array; /** * 构造函数 * 建立一个容量为 capacity 的数组栈 * * @param capacity 要建立的栈的容量,由用户指定 */ public ArrayStack(int capacity) { array = new Array<>(capacity); } /** * 默认构造函数 * 建立一个默认容量的数组栈 */ public ArrayStack() { array = new Array<>(); } @Override public int getSize() { // 复用 array 的 getSize() 方法便可 return array.getSize(); } @Override public boolean isEmpty() { // 复用 array 的 isEmpty() 方法便可 return array.isEmpty(); } /** * 获取栈的容量 * ArrayStack 特有的方法 * * @return 返回栈的容量 */ public int getCapacity() { // 复用 array 的 getCapacity() 方法便可 return array.getCapacity(); } @Override public void push(E element) { // 将数组的末尾做为栈顶,复用 array 的 addLast() 方法实现 array.addLast(element); } @Override public E pop() { // 将数组的末尾做为栈顶,复用 array 的 removeLast() 方法将栈顶元素出栈并返回 return array.removeLast(); } @Override public E peek() { // 将数组的末尾做为栈顶,复用 array 的 getLast() 方法获取栈顶元素 return array.getLast(); } /** * 重写 toString 方法返回数组栈的信息 * * @return 返回数组栈的当前信息 */ @Override public String toString() { StringBuilder result = new StringBuilder(); result.append("ArrayStack: "); result.append("size: ").append(array.getSize()).append(" "); result.append("capacity: ").append(array.getCapacity()).append(" "); result.append("bottom -> [ "); for (int i = 0; i < array.getSize(); i++) { result.append(array.get(i)); // 若是不是最后一个元素 if (i != array.getSize() - 1) { result.append(", "); } } result.append(" ] <- top"); return result.toString(); } }
对于 toString 中的遍历栈中元素,只是为了方便查看栈中是否如咱们设计的同样正确地添加了元素。
对于用户而言,除了栈顶元素,其余的元素是不须要知道的,而且用户也只能操做栈顶元素,不能操做除了栈顶元素以外的其余元素,这也是栈这个数据结构的特色。
测试:
/** * 测试 ArrayStack * * @author 踏雪寻梅 * @date 2020/1/8 - 16:49 */ public class Main { public static void main(String[] args) { ArrayStack<Integer> stack = new ArrayStack<>(); for (int i = 0; i < 10; i++) { // 入栈 stack.push(i); // 打印入栈过程 System.out.println(stack); } // 进行一次出栈 stack.pop(); // 查看出栈后的状态 System.out.println(stack); // 查看当前栈顶元素 Integer topElement = stack.peek(); System.out.println("当前栈顶元素: " + topElement); // 判断栈是否为空 System.out.println("当前栈是否为空: " + stack.isEmpty()); } }
测试结果:
对于实现的这 5 个操做,经过以前文章中对 Array 类的分析能够很快地得出他们的时间复杂度,分别以下:
void push(E element):O(1) 均摊
E pop():O(1) 均摊
E peek():O(1)
int getSize():O(1)
boolean isEmpty():O(1)
题目描述:
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。 有效字符串需知足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 注意空字符串可被认为是有效字符串。 示例 1: 输入: "()" 输出: true 示例 2: 输入: "()[]{}" 输出: true 示例 3: 输入: "(]" 输出: false 示例 4: 输入: "([)]" 输出: false 示例 5: 输入: "{[]}" 输出: true 来源:力扣(LeetCode) 连接:https://leetcode-cn.com/problems/valid-parentheses 著做权归领扣网络全部。商业转载请联系官方受权,非商业转载请注明出处。
题目分析
可以使用栈来解决
思路:
若是括号是一个左括号,就将其压入栈中。
若是遇到右括号,此时将栈顶的左括号出栈查看是否能够和这个右括号匹配。
若是能够匹配,则继续剩余的判断。
若是不能够匹配,则说明这个字符串是无效的。
经过以上分析能够得出:栈顶元素反映了在嵌套的层次关系中,最近的须要匹配的元素。
思路图示:
有效的括号字符串图示
无效的括号字符串图示
题解代码:
方法一: 使用 java 的 util 包中内置的 Stack 类解决。
import java.util.Stack; /** * 括号匹配解决方案 * 方法一:使用 java 的 util 包中内置的 Stack 类解决 * * @author 踏雪寻梅 * @date 2020/1/8 - 21:52 */ public 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 topBracket = stack.pop(); // 匹配失败的状况,如下状况若为发生则进行下一次循环依次判断 if (c == ')' && topBracket != '(') { return false; } if (c == ']' && topBracket != '[') { return false; } if (c == '}' && topBracket != '{') { return false; } } } // for 循环结束后,若是栈中还有字符,说明有剩余的左括号未匹配,此时字符串无效,不然字符串有效 // 即 isEmpty() 返回 true 表示匹配成功;返回 false 表示匹配失败 return stack.isEmpty(); } }
提交结果
方法二:使用本身实现的 ArrayStack 类解决
须要注意的是要在 Solution 中添加本身实现的 Array 类、Stack 接口、ArrayStack 类做为内部类才能使用本身实现的数组栈来解决。
/** * 括号匹配解决方案 * 方法二:使用本身实现的 ArrayStack 类解决 * * @author 踏雪寻梅 * @date 2020/1/8 - 22:25 */ public class Solution { public boolean isValid(String s) { Stack<Character> stack = new ArrayStack<>(); // 遍历给定字符串 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 topBracket = stack.pop(); // 匹配失败的状况,如下状况若为发生则进行下一次循环依次判断 if (c == ')' && topBracket != '(') { return false; } if (c == ']' && topBracket != '[') { return false; } if (c == '}' && topBracket != '{') { return false; } } } // for 循环结束后,若是栈中还有字符,说明有剩余的左括号未匹配,此时字符串无效,不然字符串有效 // 即 isEmpty() 返回 true 表示匹配成功;返回 false 表示匹配失败 return stack.isEmpty(); } private class Array<E> { ...(该类中的代码此处省略) } private interface Stack<E> { ...(该类中的代码此处省略) } private class ArrayStack<E> implements Stack<E> { ...(该类中的代码此处省略) } }
提交结果
队列也是一种线性结构。
相比数组,队列对应的操做是数组的子集。
队列只能从一端(队尾)添加元素,只能从另外一端(队首)取出元素。
队列是一种先进先出 (FIFO:First In First Out) 的数据结构。
对于队列这种数据结构,实现起来是十分简单的,这里实现如下几个操做:
入队:void enqueue(E element)
出队: E dequeue()
查看队首元素: E getFront()
获取队列元素个数: int getSize()
判断队列是否为空: boolean isEmpty()
对于代码的具体实现,和上面实现的栈同样也可让实现的队列支持多态性。因此在此设计一个接口 Queue 定义上面这 5 个队列支持的操做,再设计一个类 ArrayQueue 来实现这个接口。
对于 ArrayQueue 这个类,实质上也是基于以前实现的动态数组类 Array 来实现的。
在编写数组队列的具体代码以前,先往工程中导入以前实现的动态数组类 Array,由于要基于该类来实现数组队列,不过在以前实现栈的时候已经导入过了。
Queue 接口类的实现:
/** * 定义队列的基本操做的接口 * 支持泛型 * * @author 踏雪寻梅 * @date 2020/1/9 - 16:52 */ public interface Queue<E> { /** * 获取队列中元素个数 * * @return 队列中若是有元素,返回队列中当前元素个数;队列中若是没有元素返回 0 */ int getSize(); /** * 判断队列是否为空 * * @return 队列为空,返回 true;队列不为空,返回 false */ boolean isEmpty(); /** * 入队 * 将元素 element 添加到队尾 * * @param element 入队的元素 */ void enqueue(E element); /** * 出队 * 将队首的元素出队并返回 * * @return 返回当前出队的队首的元素 */ E dequeue(); /** * 查看当前队首元素 * * @return 返回当前的队首元素 */ E getFront(); }
ArrayQueue 类的实现:
由于在 Array 类中已经实现了不少操做数组的方法。因此对于 ArrayQueue 类实现接口中的方法时,一样只须要复用 Array 类中的方法便可。
一样,基于 Array 类实现 ArrayQueue 类也有相对应的好处:
和 Array 同样拥有了动态伸缩容量的功能,咱们不须要关心队列的容量是否够用,由于容量会动态地进行扩大和缩小。
对于一些非法变量的判断直接复用了 Array 类中的代码,不须要重复编写。
同时,也能够为 ArrayQueue 类添加一个接口中没有的方法 getCapacity 提供给用户获取队列的容量。这个方法是这个类中特有的。
ArrayQueue 类代码实现以下:
/** * 基于以前实现的动态数组类 Array 实现的数组队列类 ArrayQueue * 一样支持泛型 * * @author 踏雪寻梅 * @date 2020/1/10 - 18:17 */ public class ArrayQueue<E> implements Queue<E> { /** * 动态数组 array * 基于 array 实现队列的操做 */ private Array<E> array; /** * 构造函数 * 建立一个容量为 capacity 的数组队列 * * @param capacity 要建立的队列的容量,由用户指定 */ public ArrayQueue(int capacity) { array = new Array<>(capacity); } /** * 默认构造函数 * 建立一个默认容量的数组队列 */ public ArrayQueue() { array = new Array<>(); } /** * 获取队列的容量 * ArrayQueue 特有的方法 * * @return 返回队列的容量 */ public int getCapacity() { // 复用 array 的 getCapacity() 方法便可 return array.getCapacity(); } @Override public int getSize() { // 复用 array 的 getSize() 方法便可 return array.getSize(); } @Override public boolean isEmpty() { // 复用 array 的 isEmpty() 方法便可 return array.isEmpty(); } @Override public void enqueue(E element) { // 将数组的末尾做为队尾,复用 array 的 addLast() 方法实现 array.addLast(element); } @Override public E dequeue() { // 将数组的首部做为队首,复用 array 的 removeFirst() 方法将队首元素出队并返回 return array.removeFirst(); } @Override public E getFront() { // 复用 array 的 getFirst() 方法获取队首元素 return array.getFirst(); } /** * 重写 toString 方法返回数组队列的信息 * * @return 返回数组队列的当前信息 */ @Override public String toString() { StringBuilder result = new StringBuilder(); result.append("ArrayQueue: "); result.append("size: ").append(array.getSize()).append(" "); result.append("capacity: ").append(array.getCapacity()).append(" "); result.append("front -> [ "); for (int i = 0; i < array.getSize(); i++) { result.append(array.get(i)); // 若是不是最后一个元素 if (i != array.getSize() - 1) { result.append(", "); } } result.append(" ] <- tail"); return result.toString(); } }
对于 toString 中的遍历队列中的元素,只是为了方便查看队列中是否如咱们设计的同样正确地添加了元素。
对于用户而言,除了队首元素,其余的元素是不须要知道的,由于通常来讲业务操做都是针对队首元素的,剩余的元素都在排队等待中。而且用户只能操做队首元素和队尾元素,从队尾进入新数据从队首离开老数据,不能操做除了队首元素和队尾元素以外的其余元素,这也是队列这个数据结构的特色。
测试:
/** * 测试 ArrayQueue */ public static void main(String[] args) { ArrayQueue<Integer> queue = new ArrayQueue<>(); // 判断队列是否为空 System.out.println("当前队列是否为空: " + queue.isEmpty()); for (int i = 0; i < 10; i++) { // 入队 queue.enqueue(i); // 显示入队过程 System.out.println(queue); // 每入队 4 个元素就出队一次 if (i % 4 == 3) { // 出队 queue.dequeue(); // 显示出队过程 System.out.println("\n" + queue + "\n"); } } // 判断队列是否为空 System.out.println("当前队列是否为空: " + queue.isEmpty()); // 获取队首元素 Integer front = queue.getFront(); System.out.println("当前队列队首元素为: " + front); }
测试结果:
对于实现的这 5 个操做,经过以前的 Array 类的分析能够很快地得出他们的时间复杂度,分别以下:
void enqueue(E element):O(1) 均摊
E dequeue():O(n)
在出队的时候,当把队首的元素移出去以后,剩下的元素都要往前移动一个位置。
因此,对于当前实现的基于数组的队列,若是要放的数据量很大的话,好比 100 万、1000 万的数据量的时候,进行出队操做的时候时间性能消耗会很大。
E getFront():O(1)
int getSize():O(1)
boolean isEmpty():O(1)
数组队列出队时的缺陷分析:
在前面的基于数组的实现中,队列在出队时剩余的元素都会往前移动一个位置,若是数据量很大时进行出队将会耗费不少的时间去移动元素。而若是不移动,前面就会存在没有使用的数组空间。因此这是数组队列存在的局限性。
数组队列出队图示:
改进分析:
对于改进这一缺陷,可使用两个变量来记录队首和队尾的位置,分别为 front、tail。
对于 front,指向的是队列的第一个元素所在的位置,而 tail 指向的则是新元素入队时应该要放置的位置。
这样记录后,当元素出队后,只要维护 front 指向的位置就能够了,此时就不须要像以前那样将全部元素都往前移动一个位置了,这样时间复杂度就是 O(1) 级别的了。而当元素入队时,只须要维护 tail 将其从新指向下一个元素入队时应该要放置的位置便可。
因此循环队列的实现思路可以下逐一分析:
初始时,队列为空,front 和 tail 都指向 0,即 front == tail 表示队列为空,元素个数 size 的值也为 0,表示当前元素个数为 0。
当元素入队时,维护 tail 将其指向下一个元素入队时应该要放置的位置,即当前队尾元素的后一个位置。
当元素出队时,维护 front 将其指向出队后的队列中的第一个元素。
当元素入队到 tail 的值不能再加一且队列空间未满的时候,维护 tail 将其指向剩余空间中的第一个位置,使队列中的元素像处于一个环中同样进行循环。
例如如下状况
此时可暂时得出元素入队时更改 tail 的值的公式:
(tail + 1) % capacity
当 tail 的值再加一就等于 front 的值时,此时队列还剩余一个空间(此处这个剩余的空间设计为不记在队列容量中,是额外添加的),这时候表示队列为满,不能再入队,即 (tail + 1) % (capacity + 1) == front 表示队列满(capacity + 1 == data.length)。
此时若再入队,就会让 front 和 tail 相等,因为front == tail 表示队列为空,此时队列又不为空,会产生矛盾。因此在循环队列中额外添加一个空间用来判断队列是否已满。具体过程以下图所示:
综上可总结出关于循环队列的四个公式
front == tail 表示队列为空。
(tail + 1) % data.length == front 表示队列为满。
元素入队时更改 tail 的值的公式:(tail + 1) % data.length。
由于已经设定了在队列中额外添加一个空间用于判断队列是否已满,因此更改 tail 时须要模的是队列底层数组的长度,而不是模队列的容量。可参照下图理解:
同理,可得出元素出队时更改 front 的值得公式:(front + 1) % data.length。
代码实现:
经过以上分析,可设计循环队列 LoopQueue 类的代码以下:
/** * 循环队列类 LoopQueue * 支持泛型 * * @author 踏雪寻梅 * @date 2020/1/10 - 21:35 */ public class LoopQueue<E> implements Queue<E> { /** * 存放循环队列元素的底层数组 */ private E[] data; /** * 队首索引 * 指向队列队首元素所在的索引 */ private int front; /** * 队尾索引 * 指向新元素入队时应该要放置的索引 */ private int tail; /** * 循环队列当前元素个数 */ private int size; /** * 构造函数 * 构建一个容量为 capacity 的循环队列 * * @param capacity 要建立的循环队列的容量,由用户指定 */ public LoopQueue(int capacity) { // 由于在循环队列中额外添加了一个空间用来判断队列是否已满,因此构建 data 时多加了一个空间 data = (E[]) new Object[capacity + 1]; // 循环队列初始化 front = 0; tail = 0; size = 0; } /** * 默认构造函数 * 构建一个容量为 10 的循环队列 */ public LoopQueue() { this(10); } /** * 获取循环队列的容量 * * @return 返回循环队列的容量 */ public int getCapacity() { // 由于在循环队列中额外添加了一个空间用来判断队列是否已满,因此返回时将数组长度的值减一 return data.length - 1; } @Override public int getSize() { return size; } @Override public boolean isEmpty() { // front == tail 表示队列为空,返回 true;不然返回 false return front == tail; } /** * 将循环队列的容量更改成 newCapacity * * @param newCapacity 循环队列的新容量 */ private void resize(int newCapacity) { E[] newData = (E[]) new Object[newCapacity + 1]; // 将 data 的全部元素按顺序 (front ~ tail) 转移到 newData 的 [0, size - 1] 处 for (int i = 0; i < size; i++) { // 在将元素转移到扩容后的空间时,两个数组的 i 的值不必定对应 // 因此取 data 的数据时须要将 i 进行偏移:(i + front) % data.length newData[i] = data[(i + front) % data.length]; } // 将 data 指向扩容后的队列 data = newData; // 从新设定队首、队尾索引的值,由于上处将 data 的全部元素按顺序 (front ~ tail) 转移到 newData 的 [0, size - 1] 处 front = 0; tail = size; } @Override public void enqueue(E element) { // 入队前先查看队列是否已满,data.length <==> getCapacity() + 1 if ((tail + 1) % data.length == front) { // 队列满时进行扩容 resize(getCapacity() * 2); } // 入队 data[tail] = element; // 维护 tail tail = (tail + 1) % data.length; // 维护 size size++; } @Override public E dequeue() { // 出队前查看队列是否为空 if (isEmpty()) { // 队列为空,抛出一个非法参数异常说明空队列不能出队 throw new IllegalArgumentException("Cannot dequeue from an empty queue."); } // 保存出队元素,以返回给用户 E dequeueElement = data[front]; // 出队 // 将原队首元素置空,防止对象游离 data[front] = null; // 维护 front front = (front + 1) % data.length; size--; // 当出队到队列元素个数少到必定程度时,进行减容 if (size == getCapacity() / 4 && getCapacity() / 2 != 0) { resize(getCapacity() / 2); } // 返回出队元素给用户 return dequeueElement; } @Override public E getFront() { // 查看队首元素前查看队列是否为空 if (isEmpty()) { // 队列为空,抛出一个非法参数异常说明队列是空队列 throw new IllegalArgumentException("Queue is empty."); } // 返回队首元素给用户 return data[front]; } /** * 重写 toString 方法返回循环队列的信息 * * @return 返回循环队列的当前信息 */ @Override public String toString() { StringBuilder result = new StringBuilder(); result.append(String.format("LoopQueue: size = %d, capacity = %d\n", size, getCapacity())); result.append("front -> [ "); for (int i = front; i != tail; i = (i + 1) % data.length) { result.append(data[i]); // 若是不是最后一个元素 if ((i + 1) % data.length != tail) { result.append(", "); } } result.append(" ] <- tail"); return result.toString(); } }
测试:
/** * 测试 LoopQueue */ public static void main(String[] args) { LoopQueue<Integer> queue = new LoopQueue<>(); // 判断队列是否为空 System.out.println("当前队列是否为空: " + queue.isEmpty()); for (int i = 0; i < 10; i++) { // 入队 queue.enqueue(i); // 显示入队过程 System.out.println(queue); // 每入队 3 个元素就出队一次 if (i % 3 == 2) { // 出队 queue.dequeue(); // 显示出队过程 System.out.println("\n" + queue + "\n"); } } // 判断队列是否为空 System.out.println("当前队列是否为空: " + queue.isEmpty()); // 获取队首元素 Integer front = queue.getFront(); System.out.println("当前队列队首元素为: " + front); }
测试结果:
对于循环队列的代码,由于使用了 front 和 tail 指向队首和队尾,因此很差再复用 Array 类的代码,不过一样的是底层依旧是基于泛型数组实现的,只不过使用了循环队列的一些公式使其能循环存取数据,而且也和以前实现的 Array、ArrayStack、ArrayQueue 类同样实现了动态伸缩容量的功能,让用户再也不担忧容量够不够使用的问题。
对于循环队列,由于使用了 front 来指向队首,因此相比以前的数组队列,能够很快的得出在出队的时候的时间复杂度是 O(1) 级别的。
又由于在代码实现中一样实现了自动伸缩容量,因此在入队和出队的时候可能会触发 resize 方法进行扩大容量和减小容量,经过以前的 Array 类的分析,对于入队和出队时的时间复杂度 O(1) 一样能够得出是均摊的。因此对于循环队列中的 5 个基本操做,时间复杂度以下:
void enqueue(E element):O(1) 均摊
E dequeue():O(1) 均摊
E getFront():O(1)
int getSize():O(1)
boolean isEmpty():O(1)
实现到此,栈和队列的基本数据结构都实现完成了,最后再编写一些代码来实际测试一下数组队列和循环队列之间的效率差别。
测试代码:
import java.util.Random; /** * 测试 ArrayQueue 和 LoopQueue 的效率差距 * * @author 踏雪寻梅 * @date 2020/1/8 - 16:49 */ public class Main { public static void main(String[] args) { // 测试数据量 int opCount = 100000; // 测试数组队列所须要的时间 ArrayQueue<Integer> arrayQueue = new ArrayQueue<>(); double arrayQueueTime = testQueue(arrayQueue, opCount); System.out.println("arrayQueueTime: " + arrayQueueTime + " s."); // 测试循环队列所须要的时间 LoopQueue<Integer> loopQueue = new LoopQueue<>(); double loopQueueTime = testQueue(loopQueue, opCount); System.out.println("loopQueueTime: " + loopQueueTime + " s."); // 计算二者间的差距 double multiple = arrayQueueTime / loopQueueTime; System.out.println("在这台机器上,对于 " + opCount + " 的数据量,loopQueue 用时比 arrayQueue 用时大约快 " + multiple + " 倍."); } /** * 测试使用队列 queue 运行 opCount 个 enqueue 和 dequeue 操做所须要的时间,单位: 秒 * * @param queue 测试的队列 * @param opCount 测试的数据量 * @return 返回整个测试过程所须要的时间,单位: 秒 */ private static double testQueue(Queue<Integer> queue, int opCount) { long startTime = System.nanoTime(); // 用于生成随机数入队 Random random = new Random(); // opCount 次 enqueue for (int i = 0; i < opCount; i++) { // 入队 queue.enqueue(random.nextInt(Integer.MAX_VALUE)); } // opCount 次 dequeue for (int i = 0; i < opCount; i++) { // 出队 queue.dequeue(); } long endTime = System.nanoTime(); // 将纳秒单位的时间转换为秒单位 return (endTime - startTime) / 1000000000.0; } }
在以上代码中:
对于数组队列而言,入队时单次操做是 O(1) 的,而 opCount 次入队是 O(n) 的;出队时单次操做是 O(n) 的,而 opCount 次出队则是 O(n2) 的;因此对于整个 testQueue 方法,数组队列的时间复杂度是 O(n2) 级别的。
对于循环队列而言,入队时和出队时单次操做都是均摊复杂度 O(1) 的,因此 opCount 次入队和出队则是 O(n) 的,因此对于整个 testQueue 方法,循环队列的时间复杂度是 O(n) 级别的。
因此,能够预估:随着 n 的值愈来愈大,数组队列的时间消耗会比循环队列大许多。
测试结果:
最后,从结果中,能够看出循环队列的入队和出队操做所用的时间消耗比数组队列的快了很大的倍数。固然,随着机器配置的不一样,测试结果可能也会不一样,但可以验证循环队列比数组队列快就能够了。
若有写的不足的,请见谅,请你们多多指教。