一.概念
双端队列的意思是可以在头部和尾部添加和删除元素,更一般的单向链表队列比起来更加的灵活,下面我们用双向循环带哨兵链表和数组来分别实现
二.定义接口Dequeue
/**
* 双端队列
*/
public interface Dequeue<E> {
//队头添加元素
boolean offerTop(E e);
//队尾添加元素
boolean offerTail(E e);
//队头取元素并删除该元素
E pollTop();
//队尾取元素并删除该元素
E pollTail();
//队头取元素
E peekTop();
//队尾取元素
E peekTail();
//是否为空
boolean isEmpty();
//是否为满
boolean isFull();
}
三.双向循环带哨兵链表链表实现
1.图示
(1)向队头添加节点
(2)向队尾添加节点
对于移除节点,和上面的类似,代码注释中也都做了解释,这里就不在展示了。
2.代码实现
/**
* 双向链表实现双端队列
* @param <E>
*/
public class LinkedDequeue<E> implements Dequeue<E> ,Iterable<E>{
//内部节点类
static class Node<E>{
Node<E> pre;
E value;
Node<E> next;
public Node(Node<E> pre,E value,Node<E> next){
this.pre = pre;
this.value = value;
this.next = next;
}
}
//容量
int capacity;
//队列中元素的个数
int size;
//哨兵
Node<E> sentinel = new Node<>(null,null,null);
//初始化
public LinkedDequeue(int capacity){
//初始化容量
this.capacity = capacity;
//开始时哨兵的下一个节点是本身
sentinel.next = sentinel;
//上一个节点也是本身
sentinel.pre = sentinel;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
//获取第一个元素
Node<E> p = sentinel.next;
@Override
public boolean hasNext() {
//如果p==sentinel就说明遍历完成了,没有下一个了
return p != sentinel;
}
@Override
public E next() {
E value = p.value;
p = p.next;
return value;
}
};
}
/**
* 向头部添加节点
* a -> added -> b
* <- <-
* a是哨兵, b是哨兵的下一个节点
* @param e
* @return
*/
@Override
public boolean offerTop(E e) {
if(isFull()){
return false;
}
Node<E> a = sentinel;
Node<E> b = sentinel.next;
Node<E> added = new Node<>(a,e,b);
a.next = added;
b.pre = added;
size++;
return true;
}
/**
* 向尾部添加节点
*
a -> added -> b
* <- <-
* a是哨兵的上一个,b是哨兵
*
* @param e
* @return
*/
@Override
public boolean offerTail(E e) {
if(isFull()){
return false;
}
Node<E> b = sentinel;
Node<E> a = sentinel.pre;
Node<E> added = new Node<>(a,e,b);
a.next = added;
b.pre = added;
size++;
return true;
}
/**
* 将头部节点移除
* @return 头部元素的值
*
* a removed b
* a是哨兵,removed是哨兵的下一个,b是removed的下一个
* a -> b
* <-
*/
@Override
public E pollTop() {
if(isEmpty()){
return null;
}
Node<E> a = sentinel;
Node<E> removed = sentinel.next;
Node<E> b = removed.next;
a.next = b;
b.pre = a;
size--;
return removed.value;
}
/**
* 将尾部节点移除
* @return 尾部元素的值
*
* a removed b
* b是哨兵,removed是b的上一个,a是removed的上一个
* a -> b
* <-
*/
@Override
public E pollTail() {
if(isEmpty()){
return null;
}
Node<E> b = sentinel;
Node<E> removed = sentinel.pre;
Node<E> a = removed.pre;
a.next = b;
b.pre = a;
size--;
return removed.value;
}
/**
* 获取第一个元素的值,但是不移除
* @return
*/
@Override
public E peekTop() {
if(isEmpty()){
return null;
}
return sentinel.next.value;
}
/**
* 获取最后一个元素的值,但是不移除
* @return
*/
@Override
public E peekTail() {
if(isEmpty()){
return null;
}
return sentinel.pre.value;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean isFull() {
return size == capacity;
}
}
四.数组带头尾指针实现
1.图示
(1)刚开始时head和tail都在同一位置,表示空
当在尾部tail添加元素时,先在tail处添加元素,然后让tail加一,
注意:这里的加一不是直接tail++,因为是循环队列,所以若到了array.length处,也就是末尾,下一次加一就要跑到索引0的位置了,一般我们之前是用的求余数运算来找到合法的索引位置,但是我们这里会用一个新的方式来运算索引
(2)tail处添加元素
(3)head处添加元素
我们在head处添加元素时,我们是让head先减一,然后再在head处添加,为啥呢?因为我们是循环数组,让head减一,相当于改变了头的位置,好像是在向左移动一样,向左扩展,
注意:这里的减一也是要算出合法的索引,而且如果head本来是在索引0处,那么head就要移动到array.length处,这样就好像是在向前移动
对于头部尾部移除元素,都是类似的,在此不过多描述。
(4)代码实现
/**
* 数组实现双端队列
* @param <E>
*/
public class ArrayDequeue<E> implements Dequeue<E>,Iterable<E>{
E array[];
//头指针
int head;
//尾指针
int tail;
@SuppressWarnings("all")
public ArrayDequeue(int capacity){
//实际容量要多空出一个来判断空满
array = (E[]) new Object[capacity+1];
}
//加一方法
public static int inc(int i,int length){
if(i + 1 >= length){
return 0;
}
return i + 1;
}
//减一方法
public static int dec(int i,int length){
if(i - 1 < 0){
return length - 1;
}
return i - 1;
}
/**
* 头部添加元素
*
* 先让head--,然后再在head处添加元素
*
* @param e
* @return
*/
@Override
public boolean offerTop(E e) {
if(isFull()){
return false;
}
head = dec(head,array.length);
array[head] = e;
return true;
}
/**
* 在尾部添加依赖
*
* 先在tail处添加元素,然后让tail++
* @param e
* @return
*/
@Override
public boolean offerTail(E e) {
if(isFull()){
return false;
}
array[tail] = e;
tail = inc(tail,array.length);
return true;
}
/**
* 移除队头元素
*
* 先获取head处元素,然后让head++
*
* @return
*/
@Override
public E pollTop() {
if(isEmpty()){
return null;
}
E e = array[head];
array[head] = null; //help GC
head = inc(head,array.length);
return e;
}
/**
* 移除队尾元素
*
* 先让tail--,然后返回tail处的值
*
* @return
*/
@Override
public E pollTail() {
if(isEmpty()){
return null;
}
tail = dec(tail,array.length);
E e = array[tail];
array[tail] = null; //help GC
return e;
}
@Override
public E peekTop() {
if(isEmpty()){
return null;
}
return array[head];
}
@Override
public E peekTail() {
if(isEmpty()){
return null;
}
return array[dec(tail, array.length)];
}
/**
* 判断是否为空
* @return
*/
@Override
public boolean isEmpty() {
return head == tail;
}
/**
* 判断是否为满
* @return
*/
@Override
public boolean isFull() {
//当 head > tail 时,
/**
* 需要判断 head - tail == 1
*/
/**
* 当 tail > head 时
* 需要判断 tail - head == array.length - 1;
*/
if(head > tail){
return head - tail == 1;
}else if( head < tail){
return tail - head == array.length - 1;
}else{
return false;
}
}
/**
* 遍历
* @return
*/
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p = head;
@Override
public boolean hasNext() {
return p != tail;
}
@Override
public E next() {
E e = array[p];
p = inc(p,array.length);
return e;
}
};
}
}
五.及时释放元素
我们在之前的队列和栈的移除元素操作时,每次都是直接跳过元素,相当于移除了该元素,其实这里有误区:
1.如果元素类型为基本类型,比如Int类型,那么一个数字占四个字节,直接跳过和设置为0都占用内存空间,设置为0意义不大,所以直接跳过就可以了。
2.如果元素类型为引用类型,比如Student学生对象,那么如果你直接跳过该元素,其实他并没有被释放,也就是不能被GC回收,这时需要我们手动释放元素,我们可以让array[x]=null;来释放内存空间
另外说一点,在jdk中有一个类LinkedList,这个类可用当栈,也可以当队列,也实现了双端队列的功能