(一)栈(Stack)
栈是一种后进先出的数据结构,也称Last In First Out(LIFO)
(a)栈的特点
1.栈也是一种线性结构
2.相比较于数组,栈对应的操作是数组的子集
3.只能从一端添加元素,也只能从一端取出元素,这一端成为栈顶
(b)栈的应用
1.无处不在的Undo操作(撤销)
2.程序调用的系统栈
3.括号匹配
3.括号匹配Demo代码
package cn.data.Stack;
import java.util.Stack;
public class Solution {
/**
* 此处还有一种方式:可以引入自己编写的Stack类,而不用Java本身的类
* 这时候不需要import java.util.Stack
* Stack<Character> stack = new Stack<>();
* 修改为:ArrayStack<Character> stack = new ArrayStack<>();
* */
public boolean isValid(String s){
ArrayStack<Character> stack = new ArrayStack<>();
//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;
}
if(c==')'&& stack.pop()!='('){
return false;
}
/**if(c==')'&& stack.pop()=='('){
return true;
}为何这种逻辑有问题?
因为此时只能判断栈顶的元素是否和c匹配,只能保证局部正确,如果栈内其他元素不和后面的匹配
那整个匹配就无法成功
* */
if(c==']'&& stack.pop()!='['){
return false;
}
if(c=='}'&& stack.pop()!='{'){
return false;
}
}
}
//此处不可以直接写return true;因为不确定栈里面是否还有元素
//如果栈里面有元素,则说明有的括号没有元素可匹配,则匹配不成功
return stack.isEmpty();
}
//测试
public static void main(String[] args){
boolean flag1 = new Solution().isValid("()[]{}");
boolean flag2 = new Solution().isValid("([)][]{}");
System.out.println(flag1);
System.out.println(flag2);
}
}
控制台输出结果:
true
false
(c)栈中包含的5个方法
1.void push(E)
2.E pop()
3.E peek() 查看栈顶的元素
4.int getSize()
5.boolean isEmpty()从用户的角度看,支持这些操作就好,具体底层实现,用户不关心,实际底层有多种实现方式
(d)队列的实现: Interface Queue<E>(ArrayQueue<E>实现该接口)
1.首先写一个接口
package cn.data.Stack;
public interface Stack<E> {
int getSize(); //时间复杂度为O(1)
boolean isEmpty(); //时间复杂度为O(1)
E pop(); //有可能会触发resize操作,均摊 时间复杂度为O(1)
void push(E e); //有可能会触发resize操作,均摊 时间复杂度为O(1)
E peek(); //时间复杂度为O(1)
}
2.复写接口中的方法,自定义ArrayStack
package cn.data.Stack;
import cn.data.Array;
public class ArrayStack<E> implements Stack<E> {
Array<E> array;
//若用户知道栈最多可承载的元素量,有参构造函数
public ArrayStack(int capacity){
array = new Array<>(capacity);
}
//用户不知道栈最多可承载的元素量,无参构造函数
public ArrayStack(){
array =new Array<>();
}
@Override
public int getSize(){
return array.getSize();
}
@Override
public boolean isEmpty(){
return array.isEmpty();
}
public int getCapacity(){
return array.getCapacity();
}
@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 String toString(){
StringBuilder res = new StringBuilder();
res.append("Stack:");
res.append("[");
for(int i=0;i<array.getSize();i++){
res.append(array.get(i));
//若不是最后一个元素,用“, ”分隔开
if(i!=array.getSize()-1)
res.append(", ");
}
res.append("] top");//提示用户右侧的元素是栈顶
return res.toString();
}
}
3.编写测试类,对ArrayStack中的方法进行测试
package cn.data.Stack;
public class Main {
public static void main(String[] args) {
ArrayStack<Integer> stack = new ArrayStack<>();
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 出栈,栈顶元素出栈
(二)队列
队列是一种先进先出的数据结构(先到先得)First In First Out (FIFO)
(a)队列(Queue)的特点
1.队列也是一种线性结构
2.相比数组,队列对应的操作是数组的子集
3.只能从一端(队尾)添加元素,只能从另一端(队首)取出元素
(b)队列中包含的5个方法
void enqueue(E) 入队
E dequeue() 出队
E getFront() 获得队首元素
int getSize()
boolean isEmpty()
(c)队列的实现: Interface Queue<E>(ArrayQueue<E>实现接口)
1.接口的实现
package cn.data.Queue;
public interface Queue<E> {
int getSize();
boolean isEmpty();
void enqueue(E e);
E dequeue();
E getFront();
}
2.实现数组队列(ArrayQueue)复写接口中的方法
package cn.data.Queue;
import cn.data.Array;
public class ArrayQueue<E> implements Queue<E>{
private Array<E> array;
//如果用户可以估计队列的长度,使用传参的方法初始化
public ArrayQueue(int capacity){
array = new Array<>(capacity);
}
//如果用户 无法估计队列的长度,使用无参的构造方法
public ArrayQueue(){
array = new Array<>();
}
//时间复杂度为O(1)
@Override
public int getSize(){
return array.getSize();
}
//时间复杂度为O(1)
@Override
public boolean isEmpty(){
return array.isEmpty();
}
public int getCapacity(){
return array.getCapacity();
}
//在队尾增加元素
//均摊复杂度为O(1)
@Override
public void enqueue(E e){
array.addLast(e);
}
//在队首减去元素
//在队首去除元素后,后面的所有元素都得往前移动,时间复杂度O(n)
//时间复杂度太高,如何降低时间复杂度
@Override
public E dequeue(){
return array.removeFirst();
}
//时间复杂度为O(1)
@Override
public E getFront(){
return array.getFirst();
}
//覆盖Object类中的toString方法
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Queue:");
res.append("front[");
for(int i=0;i<array.getSize();i++){
res.append(array.get(i));
//若不是最后一个元素,用“, ”分隔开
if(i!=array.getSize()-1)
res.append(", ");
}
res.append("] tail");
return res.toString();
}
//测试用例
public static void main(String[] args) {
ArrayQueue<Integer> queue = new ArrayQueue<>();
for(int i=0;i<10;i++){
//入队
queue.enqueue(i);
System.out.println(queue);
if(i%3==2){
//出队(队首元素)
queue.dequeue();
System.out.println(queue);
}
}
}
}
控制台输出结果:
Queue:front[0] tail 从队尾增加元素
Queue:front[0, 1] tail 队尾增加
Queue:front[0, 1, 2] tail
Queue:front[1, 2] tail 队首元素出队
Queue:front[1, 2, 3] tail
Queue:front[1, 2, 3, 4] tail 队尾元素入队
Queue:front[1, 2, 3, 4, 5] tail
Queue:front[2, 3, 4, 5] tail 队首元素出队
Queue:front[2, 3, 4, 5, 6] tail
Queue:front[2, 3, 4, 5, 6, 7] tail
Queue:front[2, 3, 4, 5, 6, 7, 8] tail
Queue:front[3, 4, 5, 6, 7, 8] tail
Queue:front[3, 4, 5, 6, 7, 8, 9] tail
3.实现循环队列(LoopQueue)
package cn.data.Queue;
public class LoopQueue<E> implements Queue<E> {
private E[] data;
private int front,tail;
private int size;
//有参的构造函数
public LoopQueue(int capacity){
//循环队列中用户所使用的空间比数组长度少一个
data =(E[]) new Object[capacity+1];
front = 0;
tail = 0;
size = 0;
}
//无参构造函数调用有参构造函数
public LoopQueue(){
this(10);
}
//循环队列最多可以存放的元素
public int getCapacity(){
return data.length-1;
}
@Override
public boolean isEmpty(){
return front==tail;
}
@Override
public int getSize(){
return size;
}
//入队操作
@Override
public void enqueue(E e){
//首先检查队列是否是满的
if((tail+1)%data.length==front){
//若果是满的,进行扩容操作.扩成2倍
resize(getCapacity()*2);
}
data[tail]=e;
//由于是循环队列,注意将其对数组长度求余
tail = (tail+1)%data.length;
size++;
}
//出队操作
//在循环队列中,出队操作的均摊时间复杂度 由O(n)变为O(1).
@Override
public E dequeue(){
//判断一下队列是否为空
if(isEmpty()){
throw new IllegalArgumentException("Cannot dequeue from an empty queue");
}
E ret = data[front];
data[front] = null;
//维护一下front和size
front = (front+1)%data.length;
size--;
//若元素出队后,队列中元素个数不及队列长度的一半,并且元素出队后
//队列不能为空,则进行缩容操作
if(size==getCapacity()/4 && getCapacity()/2!=0){
resize(getCapacity()/2);
}
return ret;
}
//查看队首元素
@Override
public E getFront(){
if(isEmpty()){
throw new IllegalArgumentException("Queue is empty");
}
return data[front];
}
private void resize(int newCapacity){
E[] newData =(E[])new Object[newCapacity+1];
//将原来数组中的元素放入newData数组中
for(int i=0;i<size;i++){
//原来数组中的元素起始位置不一定为0
//但我们需要将其放入新数组中且起始元素位置为0,
//两者之间有一个front的偏移量
newData[i] = data[(i+front)%data.length];
}
data = newData;
front = 0;
tail = size;
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append(String.format("Queue:size = %d,capacity = %d ", size,getCapacity()));
res.append(" front[");
//遍历循环队列,由于队尾元素有可能比队首小,所以不一定是i<tail
for(int i=front;i!=tail;i=(i+1)%data.length){
res.append(data[i]);
/**对比两种遍历方式
* for(int i=0;i<size;i++){
newData[i] = data[(i+front)%data.length];
}
*
* */
//若当前索引不是最后一个元素,用“, ”分隔开
if((i+1)%data.length!=tail)
res.append(", ");
}
res.append("] tail");
return res.toString();
}
//测试用例
public static void main(String[] args) {
LoopQueue<Integer> queue = new LoopQueue<>();
for(int i=0;i<10;i++){
//入队
queue.enqueue(i);
System.out.println(queue);
if(i%3==2){
//出队(队首元素)
queue.dequeue();
System.out.println(queue);
}
}
}
}
循环队列:
当front==tail,队列为空
(tail+1)%c==front 队列为满 c=8
front指的是队列中第一个有元素的位置,tail指的是队列中第一个没有元素的位置
在数组,capacity中,浪费一个空间,以此区分队列是处于空还是处于满的状态
控制台输出:
Queue:size = 1,capacity = 10 front[0] tail 初始化默认队列长度为10
Queue:size = 2,capacity = 10 front[0, 1] tail 队尾增加元素
Queue:size = 3,capacity = 10 front[0, 1, 2] tail 队尾增加元素
Queue:size = 2,capacity = 5 front[1, 2] tail 队首元素出队,元素个数少于队列元素的四分之一,进行缩容操作
Queue:size = 3,capacity = 5 front[1, 2, 3] tail
Queue:size = 4,capacity = 5 front[1, 2, 3, 4] tail
Queue:size = 5,capacity = 5 front[1, 2, 3, 4, 5] tail
Queue:size = 4,capacity = 5 front[2, 3, 4, 5] tail
Queue:size = 5,capacity = 5 front[2, 3, 4, 5, 6] tail 从队尾增加元素,队列满了,下一步若元素增加,会进行扩容操作
Queue:size = 6,capacity = 10 front[2, 3, 4, 5, 6, 7] tail
Queue:size = 7,capacity = 10 front[2, 3, 4, 5, 6, 7, 8] tail
Queue:size = 6,capacity = 10 front[3, 4, 5, 6, 7, 8] tail
Queue:size = 7,capacity = 10 front[3, 4, 5, 6, 7, 8, 9] tail
数组队列与循环队列的区别主要在于出队方法dequeue()的复杂度降低了,由数组队列的O(n)变为循环队列的O(1).
4.编写测试类来测试ArrayQueue和LoopQueue两个队列出队操作的时间复杂度比较
package cn.data.Queue;
import java.util.Random;
public class Main {
//测试使用q运行opCount个enqueue和dequeue操作所需要的时间,单位:秒
private static double testQueue(Queue<Integer> q,int opCount){
//nanoTime返回的是纳秒级别的时间
long startTime = System.nanoTime();
Random random = new Random();
for(int i=0;i<opCount;i++){
//生成0到int间最大的数进行入队操作
q.enqueue(random.nextInt(Integer.MAX_VALUE));
}
for(int i=0;i<opCount;i++){
q.dequeue();
}
long endTime = System.nanoTime();
//纳秒级数据不易阅读,将其转换为秒级别的
return (endTime -startTime)/1000000000.0;
}
public static void main(String[] args) {
int opCount = 100000;
ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
double time1 = testQueue(arrayQueue,opCount);
System.out.println("ArrayQueue,time:"+time1+"s");
LoopQueue<Integer> loopQueue = new LoopQueue<>();
double time2 = testQueue(loopQueue,opCount);
System.out.println("LoopQueue,time:"+time2+"s");
}
}
控制台输出结果:
Exception in thread "main" java.lang.IllegalArgumentException: AddLast failed,Array is full
at cn.data.Array.addLast(Array.java:35)
at cn.data.Queue.ArrayQueue.enqueue(ArrayQueue.java:37)
at cn.data.Queue.Main.testQueue(Main.java:14)
at cn.data.Queue.Main.main(Main.java:30)
分析:由控制台输出打印可看出:addLast方法添加失败,因为Array数组已满。仔细阅读发现,ArrayQueue和LoopQueue都是实现的Queue接口,ArrayQueue和LoopQueue中的一些方法是调用Array中的方法来实现的,这也说明了队列操作是数组操作的一部分所以需要查看Array类中的addLast方法。
发现原因: //向所有元素后添加一个新元素
//缺少扩容操作,所以容易造成数组填满,无法添加元素的情况
/*public void addLast(E e){
//添加元素前 ,要判断数组中是否还有位置
if(size == data.length){
throw new IllegalArgumentException("AddLast failed,Array is full");
}
data[size] = e;
size++;
}
可采用以下写法:
public void addLast(E e){
// 复用下方的add方法
add(size,e);
}
最后控制台的输出结果:
[Ljava.lang.Object;@4554617c
[Ljava.lang.Object;@74a14482
[Ljava.lang.Object;@1540e19d
[Ljava.lang.Object;@677327b6
[Ljava.lang.Object;@14ae5a5
[Ljava.lang.Object;@7f31245a
[Ljava.lang.Object;@6d6f6e28
[Ljava.lang.Object;@135fbaa4
[Ljava.lang.Object;@45ee12a7
[Ljava.lang.Object;@330bedb4
[Ljava.lang.Object;@2503dbd3
[Ljava.lang.Object;@4b67cf4d
[Ljava.lang.Object;@7ea987ac
[Ljava.lang.Object;@12a3a380
[Ljava.lang.Object;@29453f44
[Ljava.lang.Object;@5cad8086
ArrayQueue,time:4.819054826s
LoopQueue,time:0.019589191s
发现在队列元素较多的时候,时间复杂度(O(n)和O(1))的区别就非常明显了