Java数据结构之队列与OJ题
程序猿教你打篮球 人气:0什么是队列?
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)
入队列:进行插入操作的一端称为队尾!
出队列:进行删除操作的一 端称为队头!
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构, 出队列在数组头上出数据,效率会比较低。
初识Queue
认识一下Queue
如图可见,Queue本质上是一个接口,被Deque(双端队列)继承,被LinkedList实现,所以Queue底层是通过链表来实现的,而Queue是不能实例化对象的, 所以我们想使用队列,则需要new一个LinkedeList对象,实现向上转型,这样就可以使用Queue中的方法了:
add 和 offer 都是入队列,这两个区别是当使用add时,一些队列有大小限制,如果想在一个满的队列中加入新的元素时,调用 add() 方法就会抛出一个 unchecked 异常,而调用 offer() 方法会返回 false。因此就可以在程序中进行有效的判断!
简单使用下Queue
public static void main(String[] args) { Queue<Integer> queue = new LinkedList<>(); queue.offer(1); queue.offer(2); queue.offer(3); System.out.println(queue.peek()); //获取对头元素 -> 1 System.out.println(queue.poll()); //出队头元素 -> 1 System.out.println(queue.peek()); //获取对头元素 -> 2 System.out.println(queue.isEmpty()); //判断队列是否为null -> false queue.clear(); //清空队列 System.out.println(queue.isEmpty()); //判断队列是否为null -> true }
模拟实现Queue
构造方法和成员属性
public class MyQueue { private class Node { private int val; //数据域 private Node next; //指针域名 private Node(int val) { this.val = val; } } //队头入,队尾出 private Node head; //队头引用 private Node last; //队尾引用 private int size; //队列有效数据个数 }
offer 方法
// 队尾入队列 public boolean offer(int val) { Node newNode = new Node(val); // 如果是第一次入队列 if (this.head == null) { this.head = newNode; this.last = newNode; } else { this.last.next = newNode; this.last = this.last.next; } this.size++; return true; }
poll 方法
// 队头出队列 public int poll() { Node node = this.head; // 如果队列为null if (this.head == null) { throw new MyQueueIsEmptyException("队列为空!"); } else { this.head = this.head.next; } this.size--; return node.val; }
peek 方法
// 取队头元素 public int peek() { // 如果队列为null if (this.head == null) { throw new MyQueueIsEmptyException("队列为空!"); } else { return this.head.val; } }
至于size和empty方法,就交给各位小伙伴实现了,由于有了前面链表的基础,队列实现起来是比较简单的,所以更多希望阅读文章的初学者能下来多自己手写以下,画画图。
队列相关的OJ题
设计循环队列 (来源:LeetCode 难度:中等)
题目:设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k): 构造器,设置队列长度为 k 。
Front: 从队首获取元素。如果队列为空,返回 -1 。
Rear: 获取队尾元素。如果队列为空,返回 -1 。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。
解题思路:对于这道题来说,我们有很多种解题思路,但我们要注意一点,如何区分队列为空和队列满的情况?这里可以添加size属性记录,也可使用标记,也可也空一个位置出来区分,那么今天我们就空一个位置,那么如何区分队列空和队列满呢?见下图:
对于循环队列的实现我们采用的时数组方案,有front和rear分别存储队头下标和队尾元素的后一个下标。至于更多需要注意细节的地方,比如修正front和rear的位置时,在代码中有对应的注释,查看即可:
public class MyCircularQueue { private int array[]; //存放数据的数组 private int front; // 队头下标 private int rear; // 队尾下标 public MyCircularQueue(int k) { this.array = new int[k + 1]; //因为我们要浪费一个空间,所以需要多开辟一个空间 } // 入队列 public boolean enQueue(int value) { // 如果队列满的情况 if (isFull()) { return false; } // 没有满就往队尾入元素 this.array[this.rear] = value; // rear往前走一步,需要空出一个位置,所以当rear走到length-1时,需要修正下rear this.rear = (this.rear + 1) % this.array.length; return true; } // 出队列 public boolean deQueue() { // 如果队列为null的情况 if (isEmpty()) { return false; } // 从队头出队列,需要修正队头的位置 this.front = (this.front + 1) % this.array.length; return true; } // 取队头元素 public int Front() { // 如果队列为null的情况 if (isEmpty()) { return -1; } return this.array[this.front]; //返回队头元素 } // 取队尾元素 public int Rear() { // 如果队列为null的情况 if (isEmpty()) { return -1; } // 如果rear为0的情况,我们需要特殊处理 int index = this.rear == 0 ? this.array.length - 1 : this.rear - 1; return this.array[index]; //返回队尾元素 } // 判断队列是否为空 public boolean isEmpty() { // 当front和rear相遇了,则表示队列中没有元素 return this.front == this.rear; } // 判断队列是否满了 public boolean isFull() { // 因为我们会浪费一个空间,所以rear+1等于front就满了 // 但是我们要rear防止越界,所以要进行修正:(this.rear + 1) % this.array.length return (this.rear + 1) % this.array.length == this.front; } }
用队列实现栈 (来源:LeetCode 难度:简单)
题目:请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
解题思路:这道题的解题思路并不难,我们只需要定义两个队列,入栈的时候往不为null的队列入,出栈的时候先将不为空的队列的size()-1个元素全部放到为空的队列中,剩余最后一个元素直接出栈即可,由于取栈顶元素不用删除元素,所以剩余的最后一个元素还要放入另一个队列中,至于更详细的内容可以配合下面代码和注释阅读:
class MyStack { private Queue<Integer> qu1; private Queue<Integer> qu2; public MyStack() { this.qu1 = new LinkedList<>(); this.qu2 = new LinkedList<>(); } public void push(int x) { // 两个队列都为null的情况 if (this.qu1.isEmpty() && this.qu2.isEmpty()) { this.qu1.offer(x); return; } // 哪个队列不为null往哪个队列中入元素 if (!this.qu1.isEmpty()) { this.qu1.offer(x); } else { this.qu2.offer(x); } } public int pop() { // 如果两个队列都为空的情况下,就不能出队操作 if (empty()) { return -1; } // 先将不为空的队列的size-1个元素全部放到为空的队列中 if (!this.qu1.isEmpty()) { while (qu1.size() - 1 != 0) { qu2.offer(qu1.poll()); } return qu1.poll(); //返回最后一个元素 } else { while (qu2.size() - 1 != 0) { qu1.offer(qu2.poll()); } return qu2.poll(); //返回最后一个元素 } } public int top() { // 如果队列为null if (empty()) { return -1; } int ret = 0; //保留剩余最后一个栈顶元素的变量 // 先将不为空的队列的size-1个元素全部放到为空的队列中 if (!this.qu1.isEmpty()) { while (qu1.size() - 1 != 0) { qu2.offer(qu1.poll()); } ret = qu1.peek(); qu2.offer(qu1.poll()); } else { while (qu2.size() - 1 != 0) { qu1.offer(qu2.poll()); } ret = qu2.peek(); qu1.offer(qu2.poll()); //取栈顶元素不能删除掉,还得入另一个队列中去 } return ret; } public boolean empty() { return this.qu1.isEmpty() && this.qu2.isEmpty(); //两个队列都为空,栈才为空 } }
用栈实现队列 (来源:LeetCode 难度:简单)
题目:请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty)。
实现 MyQueue 类:
void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false
解题思路:这道题我们可以这样做,定义两个栈,分别是pushStack和popStack,入队统一都入到pushStack栈中,出队头元素都从popStack中出,如果popStack是空的情况,就先将pushStack栈中所有的元素放入popStack中,再出栈顶元素。 至于判断队列是否为空,需要满足 pushStack和popStack都为空,队列才为空!
class MyQueue { private Stack<Integer> pushStack; private Stack<Integer> popStack; public MyQueue() { this.pushStack = new Stack<>(); this.popStack = new Stack<>(); } public void push(int x) { // 入队列都在pushStack中 this.pushStack.push(x); } public int pop() { // 出队列都从popStack出 if (popStack.empty()) { // 把pushStack栈中的元素都放到popStack栈中 while (!pushStack.empty()) { popStack.push(pushStack.pop()); } } if (!popStack.empty()) { return popStack.pop(); } else { return -1; } } public int peek() { // 取队头元素都从popStack中取 if (popStack.empty()) { // 把pushStack栈中的元素都放到popStack栈中 while (!pushStack.empty()) { popStack.push(pushStack.pop()); } } if (!popStack.empty()) { return popStack.peek(); } else { return -1; } } public boolean empty() { return pushStack.empty() && popStack.empty(); } }
最小栈 (来源:LeetCode 难度:中等)
题目:设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
MinStack() 初始化堆栈对象。
void push(int val) 将元素val推入堆栈。
void pop() 删除堆栈顶部的元素。
int top() 获取堆栈顶部的元素。
int getMin() 获取堆栈中的最小元素。
解题思路:这道题一看就需要用到两个栈,一个栈入数据,一个栈始终压入最小值,如何操作呢?这里我们可以定义两个栈,分别是stack和minStack,入栈的时候入stack这个栈,如果是第一次入栈,则当前入栈元素为最小值,同时也需要入minStack栈中,如果后续入栈元素小于或等于minStack栈顶元素,则也需要入minStack栈,如果比minStack栈顶元素大,则不需要入minStack栈了,出栈的时候,判断stack栈如果与minStack栈元素相等,则minStack也要出栈顶元素!
public class MinStack { private Stack<Integer> stack; private Stack<Integer> minStack; public MinStack() { this.stack = new Stack<>(); this.minStack = new Stack<>(); } public void push(int val) { // 如果stack为null,表示第一次入栈, // 此时入栈的元素也是此时栈中最小值,也要入minStack栈 if (this.stack.isEmpty()) { this.stack.push(val); this.minStack.push(val); return; } this.stack.push(val); // 如果stack栈顶元素小于等于minStack栈顶元素,则也需要入minStack栈中 if (this.stack.peek() <= this.minStack.peek()) { this.minStack.push(val); } } // 出栈 public void pop() { if (this.stack.isEmpty()) { return; } // 如果出栈元素与minStack元素相等,minStack也要出栈 if (this.stack.pop().equals(this.minStack.peek())) { this.minStack.pop(); } } // 取栈顶元素 public int top() { if (this.stack.isEmpty()) { return -1; } else { return this.stack.peek(); } } // 取栈中最小元素 public int getMin() { if (this.stack.isEmpty()) { return -1; } else { return this.minStack.peek(); } } }
加载全部内容