数据结构(二)---栈,队列

(一)栈(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))的区别就非常明显了

猜你喜欢

转载自blog.csdn.net/jaybillions/article/details/80862408