队列描述
队列也是一种线性数据结构 只能从一端(队尾)添加元素,从另一端(队首)取出元素 队列是一种先进先出的数据结构 FIFO 队列就像食堂排队
从第一个到最后一个 第一个打饭的人向出去,最后一个打饭的人后出去 也就是先进先出 第一个打饭的叫做队首 ,先出的那个 最后一个打饭的叫做队尾 ,后进的那个
创建队列
1.先创建一个队列接口
public interface QueueInterface < T> {
public int getSize ( ) ;
public boolean isEmpty ( ) ;
public void enqueue ( T ele) ;
public T dequee ( ) ;
public T getFront ( ) ;
}
2.实现队列
先声明队,队列也是一个数组
在给出队列元素的数量 判断队列存了多少元素,只需要返回队列的元素个数即可所以时间复杂度为O(1) 判断队列是否为空,同上时间复杂度为O(1) 入队,实际上就是给数组的尾部添加元素,所以时间复杂度为O(1) 出队,一样是给数组的头部删除元素,所以时间复杂度为O(n) 查看队首元素,就是数组的查询数组的头部元素,所以时间复杂度为O(1)
public class MyQueue < T> implements QueueInterface < T> {
private MyArray< T> data;
private int size;
public MyQueue ( ) {
data = new MyArray < > ( ) ;
size = 0 ;
}
@Override
public int getSize ( ) {
return size;
}
@Override
public boolean isEmpty ( ) {
return size == 0 ;
}
@Override
public void enqueue ( T ele) {
try {
data. addTail ( ele) ;
size++ ;
} catch ( IllegalAccessException e) {
e. printStackTrace ( ) ;
}
}
@Override
public T dequee ( ) {
T result = null;
try {
result = data. removeHeader ( ) ;
size-- ;
} catch ( IllegalAccessException e) {
e. printStackTrace ( ) ;
}
return result;
}
@Override
public T getFront ( ) {
try {
return data. getHead ( ) ;
} catch ( IllegalAccessException e) {
e. printStackTrace ( ) ;
return null;
}
}
@Override
public String toString ( ) {
StringBuilder result = new StringBuilder ( ) ;
result. append ( "队列中总共存" + size + "元素个数" ) ;
result. append ( " 队首 [" ) ;
try {
for ( int i = 0 ; i < size; i++ ) {
result. append ( data. getEleByIndex ( i) ) ;
if ( i != size - 1 ) {
result. append ( "," ) ;
}
}
result. append ( "] 队尾" ) ;
return result. toString ( ) ;
} catch ( IllegalAccessException e) {
e. printStackTrace ( ) ;
return null;
}
}
}
循环队列描述
其中dequeue()操作的时间复杂度为O(n),
是因为在出队时, 数组后面的元素都要进行前移。 本质就是移除数组的头部元素, 为了解决前移的问题,
可以使用front记录队首位置,在出队的时候只需要front后移就可 使用tail记录入队元素的位置,tail永远指向待添加元素的位置,入队时只需要tail后移即可 这就时循环队列。 就像食堂打饭一样
普通队列就是排队打饭,打一个,走一个 循环队列即就是食堂大妈那个小车车,打一个阿姨前进一步
创建循环队列
有三种情况
第一种:
当队列为空时,队首和队尾都指向索引为0的位置 添加一个元素tail后移一次,tail永远指向我们的队尾位置 即front = tail 队列为空 第二种:
如果初始队列长度为8,此时索引为0,1俩个位置的元素已经出队,用原始的方法需要扩容操作才能继续进行入队操作, 但是前面俩个位置由于已经释放元素,属于空闲区域,我们也可以向0,1为索引的位置进行入队操作. 如此操作的话就可以继续循环操作.不进行扩容,岂不是效率更高,时间复杂度为O(1) ,这样的操作,我们称其为循环数 第三种:
即 (tail + 1)%length = front时,我们把这种情况称作为队列已满 就是为了避免front = tail 队列为空情况的出现 但是这样子我们会浪费一个空间, 浪费空间的目的是用空间来换取时间
实现循环队列
public class MyLoopQueue < T> implements QueueInterface < T> {
}
声明数组容器,这里阿三用的是java里面的原生数组 定义队列的队首下标和队尾下标 给予有参无参构造方法 判断循环队列是否为空,用上述创建循环队列的第一种情况front == tail为空
private T[ ] data;
private int front, tail, size;
public MyLoopQueue ( int capacity) {
data = ( T[ ] ) new Object [ capacity + 1 ] ;
front = 0 ;
tail = 0 ;
size = 0 ;
}
public MyLoopQueue ( ) {
this ( 10 ) ;
}
@Override
public int getSize ( ) {
return size;
}
@Override
public boolean isEmpty ( ) {
return front == tail;
}
队列扩容
先声明一个object类型的数组,然后对其转型 长度加一是为了用空间来换时间 然后for循环,将原队列赋值给新队列中
newData[i] = data[(front + i) % data.length]; 赋值给新队列,原队列的顺序不能改变,所以在这里队首始终为索引为0的位置.原队首front可能指向索引为2的位置. eg:
0 null 1 2 3 此时队首索引为2,队首的值为1.队尾索引为0值为0 null为空间浪费,这里用空间来换时间 在(tail + 1) % data.length == front时就需要进行队列扩容 newData[i] = data[(front + i) % data.length]; newData[0] = data[2%5],此时新队列队首的索引更改为0,从0开始向新队列进行赋值 直至赋完所有的值,在将新队列赋给老队列即扩容成功
private void resize ( int newCapacity) {
T[ ] newData = ( T[ ] ) new Object [ newCapacity + 1 ] ;
for ( int i = 0 ; i < size; i++ ) {
newData[ i] = data[ ( front + i) % data. length] ;
}
tail = size;
data = newData;
front = 0 ;
}
入队
是向队尾加一个元素 先判断队列是否已满,若满则进行扩容,调用上述扩容方法, 若不满,则将元素插入队尾,元素个数++ 若队列下标前部为空,后部已满则将插入的元素放置在队列前部,构成循环队列 eg:
此队列长度为5即 null null 1 2 3 此时就将元素带插入的元素ele()放置索引为0的位置上构成循环队列 ele null 1 2 3 此时队首索引为2,队首的值为1.队尾索引为0值为ele 时间复杂度为O(1)
@Override
public void enqueue ( T ele) {
if ( ( tail + 1 ) % data. length == front) {
resize ( 2 * data. length) ;
}
data[ tail] = ele;
size++ ;
tail = ( tail + 1 ) % data. length;
}
队列出队
是向队首去除一个元素 先判断队列是否为空 长度– 将队首指针后移, 若长度小于队列长度的四分之一 时间复杂度为O(1)
不同于队列,因为有个tail 但此O(1)为均摊复杂度,会有扩容的影响
@Override
public T dequee ( ) {
if ( front == tail) {
return null;
}
T result = data[ front] ;
size-- ;
front = ( front + 1 ) % data. length;
if ( size == data. length / 4 && data. length / 2 > 0 ) {
resize ( data. length / 2 ) ;
}
return result;
}
查看队首元素
先判断队列是否为空 不为空则返回front为索引的元素 为空返回null 时间复杂度为O(1)
@Override
public T getFront ( ) {
if ( front != tail) {
return data[ front] ;
}
return null;
}
重写toString返回
这里为了打印循环队列所以重新toString 这里result.append(data[(front + i) % data.length]);是一个易错点 这里必须是data.length不能是size size为队列元素的个数若为此,打印时队首队尾顺序会出错
@Override
public String toString ( ) {
StringBuilder result = new StringBuilder ( ) ;
result. append ( "队列中总共存" + size + "元素个数" ) ;
result. append ( " 队首 [" ) ;
for ( int i = 0 ; i < size; i++ ) {
result. append ( data[ ( front + i) % data. length] ) ;
if ( i != size - 1 ) {
result. append ( "," ) ;
}
}
result. append ( "] 队尾" ) ;
return result. toString ( ) ;
}
队列与循环队列在出队的比较
写一个输出的方法 输出出队结束时间-入队时间 传的参数是接口类型的
public static void compareQueue ( QueueInterface queue) {
long StartTime = System. nanoTime ( ) ;
int num = 100000 ;
Random random = new Random ( ) ;
for ( int i = 0 ; i < num; i++ ) {
queue. enqueue ( random. nextInt ( 100 ) ) ;
}
while ( ! queue. isEmpty ( ) ) {
queue. dequee ( ) ;
}
long endTime = System. nanoTime ( ) ;
System. out. println ( ( endTime - StartTime) / 1000000000.0 ) ;
}
书写main方法 将我们自己写的队列与循环队列输入进去 入队出队的元素为10万的时候,会发现循环队列的出队比普通队列的时间快多了
public static void main ( String[ ] args) {
QueueInterface myQueue = new MyQueue < > ( ) ;
compareQueue ( myQueue) ;
myQueue = new MyLoopQueue ( ) ;
compareQueue ( myQueue) ;
}