关于队列和堆栈
1、如何使用栈实现队列的功能
一个输出栈,一个输入栈,push的时候进入输入栈,pop的时候从输出栈弹出,如果输出栈是空的,就把输入栈的内容pop再push进输出栈。
class MyQueue {
private Stack<Integer> input;
private Stack<Integer> output;
public MyQueue() {
input = new Stack<>();
output = new Stack<>();
}
public void push(int x) {
input.push(x);
}
public int pop() {
if(output.empty()){
while(!input.empty())
output.push(input.pop());
}
return output.pop();
}
public int peek() {
int res = this.pop();
output.push(res);
return output.peek();
}
public boolean empty() {
if(input.empty() && output.empty())
return true;
else return false;
}
}
2、两个队列实现一个栈
这里其实有两个思路,一个是在push中实现逻辑,另一个是在pop中实现逻辑,但大体思路都是让一个队列当做输入队列,另一个作为辅助队列,实现先进后出。下面的代码就是在push中实现逻辑。要是在pop中实现逻辑,就是queue1当做输入队列,当要出栈的时候,将queue1里的内容放到queue2中,只留一个用于输出,最后再把queue1和queue2互换。
class MyStack {
Queue<Integer> queue1;
Queue<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<Integer>();
queue2 = new LinkedList<Integer>();
}
public void push(int x) {
queue2.offer(x);
while (!queue1.isEmpty()) {
queue2.offer(queue1.poll());
}
Queue<Integer> temp = queue1;
queue1 = queue2;
queue2 = temp;
}
/** Removes the element on top of the stack and returns that element. */
public int pop() {
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
3、对比一下队列和栈,以及它们底部实现
(1)翻了一下源码,Stack的类继承关系如下图:
Stack继承自Vector,其内部的实现其实是数组,下面来分别看一看其push方法和pop方法:
Stack.java
public E push(E item) {
addElement(item);
return item;
}
Vector.java
public synchronized void addElement(E obj) {
modCount++;
add(obj, elementData, elementCount);
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow(); //进行扩容
elementData[s] = e;
elementCount = s + 1;
}
push方法其实就是对Vector中的成员变量——一个Object数组进行操作,如果现有数组空间已经用完,就进行扩容,这个grow方法可以看看我之前在https://blog.csdn.net/weixin_60245632/article/details/122613643?spm=1001.2014.3001.5501这里写的关于ArrayList底层原理的内容,这里面也使用了grow方法,原理是一样的。
可以想象,pop方法肯定就是将要删除的元素的后面的内容从这个删除的地方开始复制,有点绕,直接看看源码:
Stack.java
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
Vector.java
public synchronized void removeElementAt(int index) {
//删除了一些错误检查代码
int j = elementCount - index - 1;
if (j > 0) {
//这地方就是把index+1开始的元素往前挪一位,直接把index覆盖了
System.arraycopy(elementData, index + 1, elementData, index, j);
}
modCount++;
elementCount--;
elementData[elementCount] = null; //删除引用,让系统进行垃圾回收
(2)再说说队列Queue,这个就比较灵活了,Queue其实就是一个接口,继承自Collection接口,所以是需要子类来实现的,那么根据实现的原理不同,就有多种实现方式。当然,其实队列就是一个特殊的线性表,所以其底层数据结构无非就是链表和数组,我翻了一下,找了两个常用的实现子类,分别使用了链表和数组来作为底层实现。首先是LinkedList:
在LinkedList类中有一个静态内部类Node:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
很明显,LinkedList是通过链表来实现的,并且还是双向链表,那就来看看非常熟悉的offer方法和poll方法是怎么实现的:
LinkedList.java
public boolean offer(E e) {
return add(e);
}
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
可以看出来,offer方法是从链表尾部开始添加的,方法的实现也很简单,调用了linkLast进行了一个结点的连接。那么,poll方法呢:
LinkedList.java
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
rivate E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
可以看到,poll方法是从链表头部开始删除节点的,这也就证实了队列先入先出,一端放入元素,另一端输出元素的原理。而之前的Stack则是从数组的尾部添加元素,尾部删除元素,是先进后出的。然后再来看看底层数据结构是数组的Queue的实现子类PriorityQueue:
PriorityQueue内部有个成员变量Object数组,很明显,是通过数组来实现的,从构造方法也可以看出来,其实就是new了一个Object类型的数组。 那么PriorityQueue的添加删除操作肯定也是通过数组来实现的了,这里就不具体分析PriorityQueue的原理了,再之后堆和二叉树那儿再进行总结。
PriorityQueue.java
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
//一些错误检查辣妈
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
4、如何在给定的二叉树中执行先序遍历
两种方法,递归和迭代。
(1)首先是递归方法:
public void traversal(TreeNode root, ArrayList<Integer> result) {
if(root == null) return;
result.add(root.val);
traversal(root.left, result);
traversal(root.right, result);
}
(2)然后是迭代方法:
public ArrayList<Integer> traverSal(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
ArrayList<Integer> list = new ArrayList<>();
if(root == null)
return list;
stack.push(root);
while(!stack.empty()) {
TreeNode cur = stack.pop();
list.add(cur.val);
if(root.right) stack.push(root.right); //中、左、右,入栈就得是先右后左
if(root.left) stack.push(root.left);
}
return list;
}
5、如何实现后序遍历算法
依然是两种方法,首先是递归:
public void traversal(TreeNode root, ArrayList<Integer> result) {
if(root == null) return;
traversal(root.left, result);
traversal(root.right, result);
result.add(root.val);
}
迭代方法其实就是对前序遍历的变换一下顺序,由中、左、右,变成中、右、左,再进行一下逆置,就变成了左、右、中了。
public ArrayList<Integer> traverSal(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
LinkedList<Integer> list = new LinkedList<>();
if(root == null)
return list;
stack.push(root);
while(!stack.empty()) {
TreeNode cur = stack.pop();
list.addFirst(cur.val); //从链表头部添加元素,其实就相当于进行了逆置
if(root.left) stack.push(root.left);
if(root.right) stack.push(root.right);
}
return list;
}
当然这个方法是利用了LinkedList的API,算是取了个巧,其实也可以采用标记法,在要处理的结点前加一个空标记null,不过这个方法就相对难懂一些。这个方法的来源主要是为了处理在中序遍历中访问顺序(入栈的顺序)和处理的顺序(加入list的顺序)不一致而诞生的,因为前序遍历中这两个顺序是一致的,所以前序遍历的代码没有办法和中序遍历的迭代代码通用,中序遍历需要先将左边结点都放入stack中后再来进行处理,空标记法则是让要访问的结点和处理的结点都进栈,然后在处理的结点前加一个标记,这样就实现了不同顺序遍历的迭代代码的通用性(具体可以看代码随想录)。
public ArrayList<Integer> traverSal(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
ArrayList<Integer> list = new ArrayList<>();
if(root == null)
return list;
stack.push(root);
while(!stack.empty()) {
TreeNode cur = stack.pop();
if(cur != null) {
stack.push(cur);
stack.push(null); //空标记
if(cur.right) stack.push(cur.right);
if(cur.left) stack.push(cur.left);
} else {
cur = stack.pop();
list.add(cur.val);
}
}
return list;
}
6、如何在给定数组中执行二分法搜索
要在数组中执行二分查找,首先数组得有序。另外,代码得注意右边界的问题,如果right=length, 那么循环判断条件应该是left<right;如果right=length-1,那么循环判断条件应该是left<=right。
public int BinarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; //注意这里我选择的右边界
while(left <= right) {
int mid = left + (right - left) >> 1; //这么写是防止溢出,避免写(left+right)/2
if(target == nums[mid]) return mid;
if(target < nums[mid]) right = mid - 1; //如果选择的右边界是length,这里right=mid
else left = mid + 1;
}
return -1; //未找到目标值
}