目录
一、设计一个有getMin功能的栈。
要求:
1、pop、push、getMin()操作的时间复杂度都是O(1)。
2、设计的栈类型可以使用现成的栈结构。
//两个栈实现有getMin功能的栈,pop()、push()、getMin()都是O(1)
public class MyStack {
private Stack<Integer> stackData;
private Stack<Integer> stackMin;
public MyStack(){
this.stackData = new Stack<Integer>();
this.stackMin = new Stack<Integer>();
}
public void push(int newNum){
stackData.push(newNum);
if (stackMin.isEmpty() || this.getMin() >= newNum){
stackMin.push(newNum);
}
}
public int pop(){
if (this.stackData.isEmpty()){
throw new RuntimeException("Your stack is empty.");
}
int re = stackData.pop();
if (stackMin.peek() == re){
stackMin.pop();
}
return re;
}
public int getMin(){
return stackMin.peek();
}
}
需要注意的就是,每次弹出的时候,也要检查stackMin看需不需要弹出。
二、由两个栈组成的队列
编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek)
//两个栈组成队列
public class TwoStackQueue {
private Stack<Integer> stackPush;
private Stack<Integer> stackPop;
public TwoStackQueue(){
this.stackPop = new Stack<Integer>();
this.stackPush = new Stack<Integer>();
}
public void add(int newNum){
this.stackPush.push(newNum);
}
public int pop(){
if (stackPush.isEmpty() && stackPop.isEmpty()){
throw new RuntimeException("Your queue is Empty");
}
if (stackPop.isEmpty()){
while (!stackPush.isEmpty()){
stackPop.push(stackPush.pop());
}
}
return stackPop.pop();
}
public int peek(){
if (stackPush.isEmpty() && stackPop.isEmpty()){
throw new RuntimeException("Your queue is Empty");
}
if (stackPop.isEmpty()){
while (!stackPush.isEmpty()){
stackPop.push(stackPush.pop());
}
}
return stackPop.peek();
}
public static void main(String[] args) {
TwoStackQueue twoStackQueue = new TwoStackQueue();
twoStackQueue.add(1);
twoStackQueue.add(2);
twoStackQueue.add(3);
System.out.println(twoStackQueue.pop());
System.out.println(twoStackQueue.pop());
twoStackQueue.add(4);
twoStackQueue.add(5);
twoStackQueue.add(6);
System.out.println(twoStackQueue.pop());
}
}
需要注意的是,压入可以正常压入第一个栈中,但是弹出的时候,必须按照之前一次性压入第二个栈的时候的顺序弹出,也就是说,当元素push的时候,不能够把这个元素同时push进第二个栈。pop的时候,依次pop出元素,如果第二个栈弹空了,那么就把第一个栈的元素全部压入第二个栈中。第二个栈的所有数据的来自时机是当前栈中已经为空了,不能够有数据的时候添加数据进来,这样会打乱整个元素的进入和出队顺序。peek()查看的时候也一样。
三、如何仅用递归函数和栈操作逆序一个栈。
//如何用递归函数和栈操作逆序一个栈。
public class Three {
public static void reverse(Stack<Integer> stack){
if (stack.isEmpty()){
return;
}
int last = getAndRemoveLastNum(stack);
reverse(stack);
stack.push(last);
}
//移除栈底元素并返回
public static int getAndRemoveLastNum(Stack<Integer> stack){
int result = stack.pop();
if (stack.isEmpty()){
return result;
}else {
int last = getAndRemoveLastNum(stack);
stack.push(result);
return last;
}
}
}
注意事项:记住一点,先弹出的要后压入,中间继续这个过程。
四、用一个栈实现另一个栈的排序
一个栈中元素的类型为整型,现在想将该栈从顶到底按从大到小的顺序排序,只许申请一个栈。除此之外,可以申请新的变量。但不能申请额外的数据结构。如何完成排序?
//用一个栈对另外一个栈排序
public class SortStackByStack {
public static void main(String[] args) {
Stack<Integer> stackTest = new Stack<>();
stackTest.push(2);
stackTest.push(6);
stackTest.push(1);
stackTest.push(4);
stackTest.push(3);
sortStackByStack(stackTest);
while (!stackTest.isEmpty()){
System.out.println(stackTest.pop());
}
}
public static void sortStackByStack(Stack<Integer> stack){
Stack<Integer> sortStack = new Stack<>();
while (!stack.isEmpty()){
int cur = stack.pop();
while (!sortStack.isEmpty() && cur> sortStack.peek()){
stack.push(sortStack.pop());
}
sortStack.push(cur);
}
while (!sortStack.isEmpty()){
stack.push(sortStack.pop());
}
}
}
注意事项:要原栈从顶到底是大到小,那么辅助栈就得是小到大的。然后把辅助栈的全部弹入原栈即可。
五、生成窗口最大值数组。
有一个整型数组arr和一个大小为w的窗口从数组的最左边滑到最右边,窗口每次向右边滑一个位置。输出每一种窗口状态下的最大值。
时间复杂度为O(N)、空间复杂度也为O(N)。
public class MaxWindow {
public static void main(String[] args) {
int[] test = new int[]{4,3,5,4,3,3,6,7};
int[] res = getMaxWindow(test,3);
System.out.println(Arrays.toString(res));
}
public static int[] getMaxWindow(int[] arr,int w){
//不符合规范的直接返回
if (arr == null || arr.length < w || w<1 ){
return null;
}
//最后需要返回的数组
int[] res = new int[arr.length-w+1];
//数组的下标,后面用来赋值
int index = 0;
//双端队列
LinkedList<Integer> linkedList = new LinkedList<>();
//遍历
for (int i=0;i<arr.length;i++){
//双端队列的末尾如果打不过arr[i],就弹出直到末尾打过了arr[i],然后就把arr[i]的下标i放到双端队列中去
while (!linkedList.isEmpty() && arr[linkedList.peekLast()] <= arr[i]){
linkedList.pollLast();
}
linkedList.addLast(i);
//如果双端队列的头节点的下标是滑动窗口左边界的前一个,说明此时维持的最大值下标的头节点已经过期,需要弹出。
if (linkedList.peekFirst() == i-w){
linkedList.pollFirst();
}
//返回的数组中,得从i = w-1的位置处开始给每一个窗口的最大值赋值了,取出来。
if (i >= w-1){
res[index++] = arr[linkedList.peekFirst()];
}
}
return res;
}
}
注意事项:
1)使用双端队列维持的时候注意与原数组的边界问题,
2)注意维持的双端队列的头节点随着i的移动,表示的值是否会过期。如果过期了就需要弹出。
3)返回的数组从什么时候开始记录。
六、构造数组的MaxTree。
MaxTree的定义如下:
- 数组必须没有重复元素。
- MaxTree是一颗二叉树,数组的每一个值对应一个二叉树节点。
- 包括MaxTree树在内在其中的每一颗子树上。值最大的节点都是树的头。
要求:时间复杂度为O(N)、空间复杂度为O(N)。
//构造数组的MaxTree
public class MaxTree {
public Node getMaxTree(int[] arr) {
Node[] nArr = new Node[arr.length];
for (int i = 0; i < arr.length; i++) {
nArr[i] = new Node(arr[i]);
}
Stack<Node> stack = new Stack<>();
HashMap<Node, Node> lBigMap = new HashMap<Node, Node>();
HashMap<Node, Node> rBigMap = new HashMap<Node, Node>();
for (int i = 0; i != nArr.length; i++) {
Node curNode = nArr[i];
while ((!stack.isEmpty()) && stack.peek().getValue() < curNode.getValue()) {
popStackSetMap(stack, lBigMap);
}
stack.push(curNode);
}
while (!stack.isEmpty()) {
popStackSetMap(stack, lBigMap);
}
for (int i = nArr.length - 1; i > -1; i--) {
Node curNode = nArr[i];
while ((!stack.isEmpty()) && stack.peek().getValue() < curNode.getValue()) {
popStackSetMap(stack, rBigMap);
}
stack.push(curNode);
}
while (!stack.isEmpty()) {
popStackSetMap(stack, rBigMap);
}
Node head = null;
for (int i = 0; i < nArr.length; i++) {
Node curNode = nArr[i];
//获取左边比当前数大的第一个数。
Node left = lBigMap.get(curNode);
//获取右边比当前数大的第一个数。
Node right = rBigMap.get(curNode);
//如果当前数在左边和右边都找不到比它大的数,说明它就是整棵树的头节点
if (left == null && right == null) {
head = curNode;
}
//说明只在右边找到一个比它大的数
else if (left == null) {
//那就让当前节点成为比它大的那个数的孩子
if (right.getLeft() == null) {
right.setRight(curNode);
} else {
right.setLeft(curNode);
}
}
//只在左边找到一个比当前数大的。
else if (right == null) {
if (left.getRight() == null) {
left.setRight(curNode);
} else {
left.setLeft(curNode);
}
} else {//在左边和右边都找到了比当前数大的数。然后取小的那个作为当前数的父节点
Node parent = left.getValue() < right.getValue() ? left : right;
if (parent.getLeft() == null) {
parent.setLeft(curNode);
} else {
parent.setRight(curNode);
}
}
}
return head;
}
//把维持栈中的每一个元素都找到比它大的数。用map来装,后续要的时候时间O(1)
void popStackSetMap (Stack < Node > stack, HashMap < Node, Node > BigMap){
Node curNode = stack.pop();
if (stack.isEmpty()) {
BigMap.put(curNode, null);
} else {
BigMap.put(curNode, stack.peek());
}
}
}
1)把每一个数的左边的第一个比它大的数和右边第一个比它的数,取其中比较小的那个当作当前节点的父节点。
2)如果左边和右边都找不到比它大的数,说明它在数组中是最大的,也是整颗树的头节点
以上就是用来解题的思路。
注意事项:
本质:
1、利用单调栈来维持大小顺序。从左到右遍历数组的时候就可以得到每个元素它的左边第一个比它大的数。然后用一个map来存储。后面取出来的话就只要O(1)就可以找到它的左边第一个比它大的数。右边同理。就有两个map。
2、依次构造树出来。
七、求最大子矩阵的大小
给定一个整型矩阵map,其中的值只有0和1两种,求其中全是1的所有矩形区域中,最大的矩形区域为1的数量。
时间复杂度O(N * M)n行m列。
//子矩阵中,最多的1的个数
public class MaxRecSize {
public static int maxRecSize(int[][] map){
int[] height = new int[map.length];
int maxArea = 0;
for (int i = 0 ; i < map[0].length; i++){
for (int j = 0; j<map.length; j++){
height[j] = map[j][i] == 0 ? 0 : height[j] + 1;
}
//求遍历每一行中的元素,拿跟之前的最大的相比,然后取大的。
maxArea = Math.max(maxRecFromBottom(height),maxArea);
}
return maxArea;
}
//求每一行中的元素,最大能到多少,利用单调栈,因为就是求一个数的离它最近的比它小的数。
private static int maxRecFromBottom(int[] height) {
if (height == null || height.length == 0){
return 0;
}
int maxArea = 0;
Stack<Integer> stack = new Stack<>();
for (int i = 0 ; i < height.length; i++){
//当来到的i位置的数比单调栈中的栈顶元素要小的时候,就要把栈顶元素进行弹出,并且可以得到此时的栈顶元素它的矩形面积。
while (!stack.isEmpty() && height[i] <= height[stack.peek()]){
int j = stack.pop();
int k = stack.isEmpty() ? -1:stack.peek();
int curArea = (i - (k+1)) * height[j];
maxArea = Math.max(maxArea,curArea);
}
//如果没有出现意外情况,就把数组中的所有元素按照从栈顶到栈顶从大到小进行排序了。换句话说就是可以得到每个元素的矩形面积
stack.push(i);
}
while (!stack.isEmpty()){
int j = stack.pop();
int k = stack.isEmpty() ? -1 : stack.peek();
int curArae = (height.length - (k+1)) * height[j];
maxArea = Math.max(curArae,maxArea);
}
return maxArea;
}
}
j是弹出的元素。k是离i最近的一个比它小的数。k+1的意思是k的后面一个肯定是大于等于i的。
八、最大值减去最小值小于或等于num的子数组数量。
给定数组arr和整数num,共返回有多少个子数组满足如下情况:
max(arr[i..j]) - min(arr[i..j]) <= num;
要求:时间复杂度O(N)
//求有多少个子数组,max - min <= num
public class GetNum {
public static int getNum(int[] arr,int num){
if (arr == null || arr.length ==0){
return 0;
}
LinkedList<Integer> maxLinkedList = new LinkedList<>();
LinkedList<Integer> minLinkedList = new LinkedList<>();
int res = 0;
int i = 0;
int j = 0;
while (i<arr.length){
while (j<arr.length){
//如果子数组右边界往右扩的时候,遇见的数比max队列中的要大,要么就要更新最大值了。
while (!maxLinkedList.isEmpty() && arr[j] > arr[maxLinkedList.peekLast()]){
maxLinkedList.pollLast();//就把max队列中小于当前数的全部弹出。
}
maxLinkedList.push(j);
//如果子数组的右边界扩张的时候遇见了比小队列中的数还小的时候,说明要更新最小值了。
while (!minLinkedList.isEmpty() && arr[j] < arr[minLinkedList.peekLast()]){
minLinkedList.pollLast();//弹出比当前数大的数。
}
minLinkedList.push(j);
//以上两步就把最大值和最小值的维持做好了。
//接下来就是判断最大值和最小值的差不满足题目的要求的时候,就停止右边界往右扩张了。
if (arr[maxLinkedList.peekFirst()] - arr[minLinkedList.peekFirst()] > num){
break;
}
//如果一直满足条件没有被break掉的话,右边界也就是j继续往右边扩一个位置。
j++;
}
//上面的i和j都固定下来的时候。此时就要计算有多少个子数组了。
res += j-i;
//因为马上要进行左边界扩张了,如果当前的双端队列中的头节点有是当前的i的话,下一轮这个双端队列中维持的数据应该就是过期了的,需要弹出。
if (maxLinkedList.peekFirst() == i){
maxLinkedList.pollFirst();
}
if (minLinkedList.peekFirst() == i){
minLinkedList.pollFirst();
}
//遍历下一个i
i++;
}
return res;
}
}
注意事项:
1)两个双端队列来维持右边界遍历过的最大值和最小值。解决最关键的就是遍历数组的时候,左指针依次对准i。然后右边界进行扩张。直到max - min > num。然后得到右边界的位置j。j-i就是能组成的子数组。然后i+1左边界右走一位。总的就是累计和就行了。
2)需要主要的是,每次用双端队列来维持一个局部状态中的值的时候考虑这个值是不是会随i的移动而过期。过期了就需要弹出。这在滑动中很重要的。
参考:《程序员代码面试指南》-左程云