前言
队列
队列的概念
队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表,一种先进先出的数据结构。
队尾:允许插入的一端。
队头:允许删除的一端。
队列的实现
队列的层次实现可以是链表,也可以是数组。
队列的链表实现
队列
如果采用单链表实现,若使用头插与尾删,则出队的时间复杂度为O(n),若使用头删与尾插,且具有last标记(可看作指针),则时间复杂度为O(1).
如果采用双链表实现,则时间复杂度一定为O(1).
实现的方法与属性
public class MyQueue {
ListNode first; //创建指向队列首部的标记
ListNode last; //创建指向队列尾部的标记
//入队列
public void offer(int val) {}
//出队列
public int poll() {}
//获取队头元素 但是不删除
public int peek() { }
//判空
public boolean isEmpty() { }
//获取队列元素个数
public int size(){}
}
内部类实现节点
static class ListNode{
//设定一个节点对象
int data ;
ListNode prev ;
ListNode next;
public ListNode(int data) {
this.data = data;
}
}
入队列
实现思想:采用头插法,如果为空,则使得头标记与尾标记均指向新节点
不为空,则进行头插的操作。
public void offer(int data) {
//采用头插法
ListNode cur = new ListNode(data);
if(isEmpty()){
first = last = cur;
}else {
cur.next = first;
first.prev = cur;
first = cur;
}
}
出队列
实现思路:
1 . 先判断队列是否为空,队列为空抛异常。
2. 队列不为空,将尾标记指向尾节点的前一个节点,然后将前一个节点的next指针置为空,删除尾结点。
因为我队入采取的是头插,所以队出采取尾删,如果大家采取尾插,则队出就采取头删了。
public int poll() {
try {
if (isEmpty()) {
throw new EmptyException("空队列异常!!!");
}
}catch (EmptyException e){
e.printStackTrace();
}
int ret = last.data;
last = last.prev;
last.next = null; // 删除尾结点
return ret;
}
获取队头元素但不删除
获取队头元素 但是不删除
实现思路:
1.先判断队列是否为空,队列为空抛异常。
2 .队列不为空,返回尾节点的数据。(因为我采取的是链表尾部的队首)
public int peek() {
//获取队头元素
try {
if (isEmpty()) {
throw new EmptyException("空队列异常!!!");
}
}catch (EmptyException e){
e.printStackTrace();
}
return last.data;
}
判空
思想:直接返回头是否为空就行。
public boolean isEmpty(){
return first == null;
}
获取队列元素个数
思想:直接遍历链表即可。
public int size(){
//遍历整个链表
ListNode cur = first;
int count = 0 ;
while (cur!=null){
count++;
cur = cur.next;
}
return count;
}
队列的数组实现
如果采用普通的数组实现队列,在下标大的一端进行插入数据,在下标小的一端删除数据,
我们可以在删除一个数据后,将所有的数据向左移动一位,但是时间复杂度就变成了O(N),能否用数组实现一个时间复杂度为O(1),则不浪费空间的队列呢?——即循环队列。
循环队列
循环队列图:
实现思想:我们有front标记指向队列的首部,用rear标记指向队列的尾部,用于指向要插入数据的位置,当下标的值超出数组长度时,便用求模计算找回对应的位置。
方法属性实现:
class MyCircularQueue {
//实现的数组
private int[] elements;
//指向首元素的标记
int first = 0;
//指向尾元素的标记
int last = 0 ;
//有效数组元素的个数
int size = 0;
//构造器,设置队列长度为 k
public MyCircularQueue(int k) {}
// 向循环队列插入一个元素。如果成功插入则返回真。
public boolean enQueue(int value) {}
//从循环队列中删除一个元素。如果成功删除则返回真。
public boolean deQueue() {}
//从队首获取元素。如果队列为空,返回 -1
public int Front() {}
//获取队尾元素。如果队列为空,返回 -1 。
public int Rear() {}
//检查循环队列是否为空。
public boolean isEmpty() {}
//检查循环队列是否已满。
public boolean isFull() {}
};
构造方法
public MyCircularQueue(int k) {
//用数组实现一个循环队列
this.elements = new int[k];
}
向循环队列插入一个元素,成功插入则为真。
实现思想:
- 判断队列是否已满,满了就返回false。
- 不满就在last处放。
- 在放完之后,将last的值+1然后模除队列的长度
public boolean enQueue(int value) {
//向循环队列中插入一个元素
//先判断循环队列是否满
if(isFull()){
return false;
}
//未满
this.elements[last] = value;
last = (last+1)% elements.length;
size ++;
return true;
}
从循环队列中删除一个元素,成功删除则为真
实现思想:
- 判断队列是否为空,空就返回false。
- 不空就直接将front指向下一个位置。
- 因为是循环队列,所以front+1再对数组长度进行求模。
public boolean deQueue() {
//从队列中删除一个元素,删除队首的元素
if(isEmpty()){
return false;
}
//如果队列不为空,删除队首的元素,first+1,
first = (first +1)% elements.length ; //必须保证first值到最大值时,能够再返回起点
size = size-1;
return true;
}
从队首获取元素,如果队列为空,返回-1
实现思路:
- 先判断队列是否为空,为空返回-1。
- 不为空,返回front下标对应值。
public int Front() {
if(isEmpty()){
return -1;
}
//如果队列不为空,返回队首元素
return elements[first];
}
获取队尾元素。如果队列为空,返回 -1
实现思想:
- 如果队列不为空,返回队尾元素
- 队尾元素的下标不一定为last -1 ,如果last 正好此时为0,那么last-1的结果为-1
按照求模的方法:(last+数组长度-1)%数组长度,获取当前last之前的值
public int Rear() {
if(isEmpty()){
return -1;
}
return elements[(last + elements.length-1 )%elements.length];
}
检查循环队列是否为空
实现思想:通过size,判断size是否为0,来判断循环队列是否为空。
public boolean isEmpty() {
//判断空,满足头标记与尾标记都相等,且无数值
if( size ==0){
return true;
}
return false;
}
判断循环队列是否已满
实现思想:当front标记与last标记相同,且size不为0时,说明循环队列已满。
public boolean isFull() {
if(first ==last && size>0){
return true;
}
return false;
}
}
双端队列(Deque)
双端队列如图:
指可以两边都可以插入,都可以删除的队列。
Deque是一个接口,在java中有两个类实现了此接口
- ArrayDeque类,此类并没有在集合框架中
- LinkedList类
ArrayDeque是双端队列的线性实现。
LinkedList是双端队列的链式实现。
这两个类的对象可以当做栈与队列使用,因为他们实现了栈与队列的方法。
Deque<Integer> stack = new ArrayDeque<>();//双端队列的线性实现
Deque<Integer> queue = new LinkedList<>();//双端队列的链式实现
队列Oj题
1. 用队列实现栈
实现思想:
1 . 队列是先进先出操作,栈是先进后出,所以一个队列不可能实现栈,我们采用两个队列实现栈
2. 入栈,则直接将数据插入队列1即可
3. 出栈:将队列1中的size-1个元素,全部放入到队列2中去,剩余的一个元素,则进行出队即可。
如果再进行出栈,则将队2中的size-1个元素,放入到队1中去,将剩余的一个元素再进行出队
class MyStack {
Queue<Integer> qu1 = new LinkedList<Integer>();
Queue<Integer> qu2 = new LinkedList<Integer>();
public MyStack() {
}
public void push(int x) {
//将数据压入有数据的队列中
if(!qu1.isEmpty()){
qu1.offer(x);
}else if(!qu2.isEmpty()){
qu2.offer(x);
}else{
qu1.offer(x);
}
}
public int pop() {
//执行出栈操作,先判断栈是否为空
if (empty()) {
return -1;
}
//如果不为空
if (!qu1.isEmpty()) {
int len = qu1.size()-1;
for (int i = 0; i < len; i++) {
int val = qu1.poll();
qu2.offer(val);
}
//
return qu1.poll();
}
int len = qu2.size() -1 ;
for (int i = 0; i < len; i++) {
int val = qu2.poll();
qu1.offer(val);
}
return qu2.poll();
}
public int top() {
// 获取栈顶元素
//将所有的数据转移到另一个队列中,期间经过一个临时变量,最后临时变量中的值即为栈顶元素
if(empty()){
return -1;
}
int val = 0;
if(!qu1.isEmpty()){
// for(int i=0;i<qu1.size();i++){ //有问题,当qu1执行poll方法后,会出现判断条件发生改变的情况
while (!qu1.isEmpty()){
val = qu1.poll();
qu2.offer(val);
}
}else{
while (!qu2.isEmpty()){
val = qu2.poll();
qu1.offer(val);
}
}
return val;
}
public boolean empty() {
if(qu1.isEmpty()&&qu2.isEmpty()){
return true;
}
return false;
}
}
2. 用栈实现队列
实现思想:
- 通过两个栈来实现队列
- 对于入队操作,直接将所有的数据放入栈1中即可
- 对于出队操作,则先判断栈2中是否有数据,如果有则出栈2的数据,如果没有,则将栈1中的全部数据转入栈2,然后再出栈2中的数据即可。
class MyQueue {
Stack<Integer> stack1 = new Stack<>();
Stack<Integer> stack2 = new Stack<>();
public MyQueue() {
}
public void push(int x) {
//将所有的数据压入栈1
stack1.push(x);
}
public int pop() {
//先判断栈是否为空
if(empty()){
return -1;
}
if(!stack2.isEmpty()){
//如果栈2不等于空,
return stack2.pop();
}
//如果栈2为空
while(!stack1.isEmpty()){
//将栈1的元素全部压入栈2中去
int val = stack1.pop();
stack2.push(val);
}
return stack2.pop();
}
public int peek() {
//获取栈顶的元素
//先判断栈是否为空
if(empty()){
return -1;
}
if(!stack2.isEmpty()){
//如果栈2不等于空,
return stack2.peek();
}
//如果栈2为空
while(!stack1.isEmpty()){
//将栈1的元素全部压入栈2中去
int val = stack1.pop();
stack2.push(val);
}
return stack2.peek();
}
public boolean empty() {
return stack1.isEmpty()==true && stack2.isEmpty() ==true ;
}
}