对于今天的内容,我相信大家都很容易理解!
在上结说过 栈 先进后出,今天介绍一种先进先出的数据结构---------------队列!
那么我们来看看什么是队列呢?
队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表
队列有点类似于栈,和栈相比,队列中第一个进,第一个出,而栈则第一个进最后一个出。
说形象一点,就像去排队买早餐一样,只有先排在队尾就可以早一点去买到早饭,越往后排队的人就会越晚买到早餐。
既然数据结构都有多种物理结构!那么通过接口就可以实现各自对应的结构了。
看看队列接口是如何编写?
那么动手来试一试吧!
public interface Queue<E> {
/**
* 获取有效元素个数
* */
public int getSize();
/**
* 判空
* */
public boolean isEmpty();
/**
* 清除
* */
public void clear();
/**
* 入队一个新元素e
* */
public void enqueue(E e);
/**
* 出队一个元素e
* */
public E dequeue();
/**
* 获取队首元素(不删除)
* */
public E getFront();
/**
* 获取队尾元素(不删除)
* */
public E getRear();
}
接口写好了,那么如何实现队列的顺序存储结构呢?
定义一个ArrayQueue类去实现队列,看看该如何实现。
从类图中分析到,ArrayQueue和ArrayList是聚合关系,那么就是说可以在线性表的基础上去实现队列。
接下来我们看看怎么去实现!!!
public class ArrayQueue<E> implements Queue<E> {
//定义属性
private ArrayList<E> list;
//构造方法
public ArrayQueue() {
list=new ArrayList<E>();
}
//有参构造方法 参数为 队列大小
public ArrayQueue(int capacity){
list=new ArrayList<E>(capacity);
}
//获取有效元素大小
@Override
public int getSize() {
return list.getSize();
}
//判空
@Override
public boolean isEmpty() {
return list.isEmpty();
}
//清除
@Override
public void clear() {
list.clear();
}
-
进队列是从线性表的队尾进,从表头出。看看图是不是好理解许多!从线性表的尾进即rear的位置,进入队列后队尾指针后移。
@Override public void enqueue(E e) { list.addLast(e); }
-
出队列就是从队头出,那么就是在线性表的表头去出,队尾向前移,所有元素也向前移一位。
@Override public E dequeue() { return list.removeFirst(); } //获取队头 @Override public E getFront() { return list.getFirst(); } //获取队尾 @Override public E getRear() { return list.getLast(); }
-
队列的实质是ArrayList就和线性表 toString一样 获取内容然后按顺序输出出来。
@Override public String toString() { StringBuilder sb=new StringBuilder(); sb.append("ArrayQu: size="+getSize()+",capacity="+list.getCapacity()+"\n"); if(isEmpty()){ sb.append("[]"); }else{ sb.append('['); for(int i=0;i<getSize();i++){ sb.append(list.get(i)); if(i==getSize()-1){ sb.append(']'); }else{ sb.append(','); } } } return sb.toString(); }
-
重写equals()方法对于比较方法的实质来说,还是对ArrayList的比较,那么我们就用ArrayList的方法去比较。
那么问题来了,怎么去比较呢? 将比较对象类型进行类型转换,用这个类型的实质,去实现队列的比较。用最底层实现的ArrayList去实现。@Override public boolean equals(Object obj) { if(obj==null){ return false; } if(obj==this){ return true; } if(obj instanceof ArrayQueue){ ArrayQueue l=(ArrayQueue) obj; return list.equlas(l.list); } return false; } }
-
看过队列的顺序存储发现了什么问题?
首先时间复杂度上
队列---------入队平均 O(1)每一次只需在队尾增加就行
队列---------出队O(n).(只要出队,在队列中的每一个元素都要移动)那么想要时间复杂度小,就需要让数据执行的次数不随N的增大而变大。
这么说也不好理解的话,不管有多少数据,让每一处理都执行1次。
相对队头来说,移动元素 和 移动指针来说,相对指针会容易一点。
所以移动队头指针。
显而易见,问题又来了?
出队后的的空间怎么在利用?
扩容?保证 永远能存数据,但是空间只会越来越多的浪费。
那么只要想到重复利用—可能想法就是,循环了吧!
那么来看看循环会怎样的效果?
在这个基础上去添加数据,用循环的想法的话是不是这样呢
是不是解决了问题了!开心的的赶紧鼓鼓掌?
那么请问如何知道这是一个空的队列,满队列呢?
- 判满 (rear + 1)%list.length == front
- 判空 (rear + 1)%list.length == front
蒙了吧? 空满如何区分呢?
其实区分的意义是为了让它有自己的特性,那么用队尾永远指向一个空,就可以区分空还是满了!
这样?
那么我问你是不是永远会有一个空间是浪费的呢? 其实浪费一个可以忽略的,但是你必须要记住一点就是,有效元素存储空间会减一的!
那么在创建空间是不是应该,在原有的长度上加1避免空间缺失。
你在容积创建的时候加1,在使用的时候不会发现它的长度会发生变化。在使用的时候肯定不是在内部去使用。
看来循环队列可以轻松去处理队列的问题。
那么我们来看看如何编写循环队列ArrayQueueLoop。理解循环的意思其实是角标的循环
public class ArrayQueueLoop<E> implements Queue<E>{
-
属性的定义
private E[] data; //创建一个数组 private int front; //头指针 private int rear; //尾指针 private int size; //有效元素个数 private static int DEFAULT_SIZE=10; //默认容积 public ArrayQueueLoop() { this(DEFAULT_SIZE); } public ArrayQueueLoop(int capacity){ data=(E[]) new Object[capacity+1]; front=0; //默认开始位置为 rear=0; size=0; } //获取有效元素个数 @Override public int getSize() { return size; } @Override public boolean isEmpty() { // 第三布优化 让rear 指向空 //所以 一旦Front = rear 就都为空 return front==rear&&size==0; } @Override public void clear() { size=0; front=0; rear=0; //与其缩容清空 不如重新创建数组 (相比之下 自己决定) }
-
进队列只要遇到插入操作就必须要考率到,是否满?满则扩容或者抛异常等,进队列只需在队尾添加即可,将队尾指针后移。然后队尾指针常指向有效长度的位置,并为空。
@Override public void enqueue(E e) { if((rear+1)%data.length==front){ //判满 //让尾指针下一位是头指针就行 //【扩容】 resize(data.length*2-1); //因为rear 总指向空 } data[rear]=e; // 进队列 从队尾进 初始为 0 的位置 rear=(rear+1)%data.length; size++; }
-
扩容/缩容的思想就是,创建新的数组存放需要复制的元素,将新的数组地址给原数组。
private void resize(int newLen) { E[] newData=(E[]) new Object[newLen]; int index=0;//表示新数组角标 for(int i=front;i!=rear;i=(i+1)%data.length){ newData[index++]=data[i]; //index++ 先用在加 //循环条件 i != rear 指的是front 下一个是 rear指向的为空 // 每一次 i=(i+1)%data.length 找到循环队列中所 对应下标 } front=0; rear=index; // 只有指向为空的时候 才会跳出循环此时index为空 data=newData; }
-
出队先判断是否有元素,出队需要返回出队的数据,队头需要后移,那么,删除了很多元素后,容积过大,是不是就会有不必要的空间浪费,所以需要扩容。
@Override public E dequeue() { if(isEmpty()){ throw new NullPointerException("队列为空!"); } E e=data[front]; //获取 front=(front+1)%data.length; size--; if(size<=data.length/4&&data.length>DEFAULT_SIZE){ resize(data.length/2+1); } return e; } //获取队头信息 @Override public E getFront() { return data[front]; } //获取队尾信息 @Override public E getRear() { return data[(data.length+rear-1)%data.length]; //在循环队列中 总长度 加 队尾指针 数 减 1 对 数组取余 //因为 循环 所以 rear(指向有效元素下一位 为 空) 可以为 //任意的指针 如果指向 0 直接减1 如何减? // 所以 用长度 加 队尾 减 1 指向 循环列表中 的 前一个位置 //就好像 一种有7天 今天星期1 再过 7天星期几 当然星期1 //(循环) }
-
遍历出数据就可,注意只要知道如何结束,就行。
@Override public String toString() { StringBuilder sb=new StringBuilder(); sb.append("ArrayQueueLoop: size="+getSize()+",capacity="+(data.length-1)+"\n"); if(isEmpty()){ sb.append("[]"); }else{ sb.append('['); for(int i=front;i!=rear;i=(i+1)%data.length){ sb.append(data[i]); if((i+1)%data.length==rear){ //一但为空就 结束 sb.append(']'); }else{ sb.append(','); } } } return sb.toString(); }
-
比较方法的重写,实质是对底层数组元素比较
@Override public boolean equals(Object obj) { if(obj == null) { return false; } if(obj == this) { return true; } if(obj instanceof ArrayQueueLoop) { ArrayQueueLoop aql = (ArrayQueueLoop) obj; if(getSize() == aql.getSize()) { for(int i = 0 ; i != rear;i = (i+1)%data.length) { if(data[i] != aql.data[i]) { return false; } } return true; } } return false; } }