剑指offer-栈

剑指offer-栈

首先学习一下Java中栈的常用Constructor和Method

构造器或方法 功能描述 类型
public Stack() Creates an empty stack. Constructor
push(E item) Pushes an item onto the top of this stack. Method
peek() Looks at the object at the top of this stack without removing it from the stack. Method
pop() Removes the object on the top of the stack and return this object as the value of this function. Method
empty() Tests if this stack is empty. Method

JZ30 包含min函数的栈

题目描述:定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的 min 函数,输入操作时保证 pop、top 和 min 函数操作时,栈中一定有元素。

思路一

这题不难想到是使用第二个栈,使用一个栈记录进入栈的元素,正常进行push、pop、top操作,使用第二个栈来返回最小值,首先我想到的是使用第二个栈有序的保存所有元素,min操作直接返回这个栈的最顶端元素即可。

import java.util.*;
import java.util.Stack;

public class Solution {

    Stack<Integer> stack = new Stack<>();
    Stack<Integer> stackMin = new Stack<>(); 

    public void push(int node) {
        stack.push(node);
        if(!stackMin.empty()) {
            if(node > stackMin.peek()) {
                Stack<Integer> temp1 = new Stack<>();
                while(!stackMin.empty() && node > stackMin.peek()) {
                    temp1.push(stackMin.pop());

                }
                stackMin.push(node);
                while(!temp1.empty()) {
                    stackMin.push(temp1.pop());
                }
            } else {
                stackMin.push(node);
            }
        } else {
            stackMin.push(node);
        }
    }
    
    public void pop() {
        if (stack.empty()) {
            return ;
        } else {
            int value = stack.pop();
            Stack<Integer> stackTemp = new Stack<>(); 
            while (!stackMin.empty()) {
                if (stackMin.peek() == value) {
                    stackMin.pop();
                    break;      // 遇到相同的元素,就需要跳出while循环,以免重复删除值
                } else {
                    stackTemp.push(stackMin.pop());
                }
            }
            while (!stackTemp.empty()) {
                stackMin.push(stackTemp.pop());
            }
        }
    }
    
    public int top() {
        if (!stack.empty()) {
            int topValue = stack.peek();
            return topValue;
        } 
        return 0;
    }
    
    public int min() {
        if(!stack.empty())
            return stackMin.peek();
        return 0;
    }
}

第一种思路实际上做了很多冗余的功能,因为并不需要给第二个栈排序,具体见思路二。

思路二

还是双栈,使用第一个栈记录进入栈的元素,正常进行push、pop、top操作,使用第二个栈记录每次push进入的最小值;每次第二个栈push元素的时候与第二个栈的栈顶元素比较,若是较小,则进入第二个栈,若是较大,则第二个栈的栈顶元素再次入栈,因为即便加了一个元素,它依然是最小值。于是,每次访问最小值即访问第二个栈的栈顶。

import java.util.Stack;
public class Solution {
    //用于栈的push 与 pop fast-template
    Stack<Integer> s1 = new Stack<Integer>();
    //用于存储最小min
    Stack<Integer> s2 = new Stack<Integer>();
    public void push(int node) {
        s1.push(node);
        //空或者新元素较小,则入栈
        if(s2.isEmpty() || s2.peek() > node)
            s2.push(node);
        else
            //重复加入栈顶
            s2.push(s2.peek());
    }
    public void pop() {
        s1.pop();
        s2.pop();
    }
    public int top() {
        return s1.peek();
    }
   public int min() {
        return s2.peek();
   }
  }

思路三

使用一个栈,入栈和出栈的每个元素变成一个结构体,结构体中包(含元素值,栈内最小值),每次push的时候都更新栈内最小值。

  • top获取结构体第一个值
  • min获取结构体第二个值
  • pop删除栈顶结构体
  • push分两种情况
    • 栈为空时,直接push结构体,(x,最小值也是x)
    • 栈不为空,比较栈顶元素y与结构体x,取最小值压入栈(x,min(x,y))
import java.util.Stack;

public class Solution {
    //用于栈的push 与 pop fast-template
    Stack<stackElemt> s = new Stack<stackElemt>();

    public class stackElemt {
        private int value;
        private int min;

        public stackElemt(int value, int min) {
            this.value = value;
            this.min = min;
        }

    }

    public void push(int node) {
        if (s.isEmpty()) {
            s.push(new stackElemt(node, node));
        } else {
            if (node > s.peek().min) {
                s.push(new stackElemt(node, s.peek().min));
            } else {
                s.push(new stackElemt(node, node));
            }
        }
    }

    public void pop() {
        s.pop();
    }

    public int top() {
        return s.peek().value;
    }
    public int min() {
        return s.peek().min;
    }

}

JZ31 栈的压入、弹出序列

描述:输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。

思路

双指针分别指向压入序列和弹出序列首位,一直将压入序列压入栈,每压入一个元素,压入序列指针后移,判断栈顶元素与弹出序列元素是否相等,不相等继续压入,相等则弹出栈顶元素并弹出序列指针后移,并继续判断栈顶元素是否与弹出序列指针元素相等,若相等继续弹出栈顶元素,不相等就继续压入元素。等待压入序列全部压入进栈,然后将所有元素出栈,如果匹配弹出序列,则返回true,否则返回false。

import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pushV int整型一维数组 
     * @param popV int整型一维数组 
     * @return bool布尔型
     */
    public boolean IsPopOrder (int[] pushV, int[] popV) {
        // write code here
        if(pushV.length == 0)   return true;
        Stack<Integer> s = new Stack<Integer>();
        int i = 0, j = 0;
        for (; i < pushV.length; i++) {
            s.push(pushV[i]);
            while(!s.empty() && s.peek() == popV[j]) {
                s.pop();
                j++;
            }
        }
        boolean result = true;
        // 判断弹出序列和栈内元素完全匹配
        if(j == pushV.length) {
            result = true;
        } else {
            result = false;
        }
        return result;
        
    }
}

JZ73 翻转单词序列

牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“nowcoder. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a nowcoder.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?

示例:

Input: "nowcoder. a am I"
Output: "I am a nowcoder."

思路

可以发现,只有词序倒置,每个单词字母顺序正确,只需要用栈的特性,先存储每个单词字符串,然后再输出就可实现翻转单词序列效果。

值得注意的是Java中字符串String自带很多method能够实现很多功能,譬如:

  • String.split(String regex) –– Splits this string around matches of the given regular-expression.
  • + operation can link string directly.
import java.util.*;
public class Solution {
    public String ReverseSentence(String str) {
        if(str == null) return null;
        String outputStr;
        String[] tempStr = str.split(" ");
        Stack<String> stack = new Stack<String>();
        for(String temp : tempStr) {
            stack.push(temp);
        }
        outputStr = stack.pop();
        while(!stack.empty()) {
            outputStr = outputStr + " " + stack.pop();
        }
        return outputStr;
    }
}

JZ59 滑动窗口最大值

描述
给定一个长度为 n 的数组 num 和滑动窗口的大小 size ,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。窗口大于数组长度或窗口长度为0的时候,返回空。
数据范围: 1≤n≤100001≤n≤10000,0≤size≤100000≤size≤10000,数组中每个元素的值满足 ∣val∣≤10000∣val∣≤10000
要求:空间复杂度 O(n),时间复杂度 O(n)

思路一

暴力方法,遍历数组,然后每选取出窗口数组,并选取出窗口最大值,可行,但是时间复杂度是O(n*k)不满足时间复杂度要求

import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param num int整型一维数组 
     * @param size int整型 
     * @return int整型ArrayList
     */
    public ArrayList<Integer> maxInWindows (int[] num, int size) {
        // write code here
        // 窗口大于数组长度或窗口长度为0的时候,返回空。
        ArrayList<Integer> window = new ArrayList<Integer>();
        if (num.length < size || num.length == 0 || size == 0)  return window;

        // ArrayList<Integer> window = new ArrayList<Integer>();
        int numWindow = num.length - size + 1;

        for (int i = 0; i < numWindow; i++) {
            int[] subNum = new int[size];
            subNum = copyOfRange(num, i, i + size);
            window.add(selectMax(subNum));
        }
        return window;
    }

    public int selectMax(int[] num) {
        int max = num[0];
        for(int i = 0; i < num.length; i++) {
            // 三元运算符
            max = (max >= num[i]) ? max : num[i]; 
        }
        return max;
    }

    public int[] copyOfRange(int[] num, int start, int end) {
        // 包含start,不包含end
        int[] subNum = new int[end - start];
        for(int i = 0; i < (end - start); i++) {
            subNum[i] = num[start + i];
        } 
        return subNum;
    }
}

思路二

在暴力求解的过程中,我们发现子窗口右滑时,新进入的数字A如果比窗口内的其他数字都大,那么数字A之前的数字更最大值就没有关系了,在之后窗口滑动过程都是无用的,因为他们比A早离开子窗口,所以每次滑动窗口时,新增加的数字A与子窗口内的数字进行比较,将小于A的数字都去掉,如果A大于所有数字,就只需要数字A了。每次滑动窗口,还需要判断是否需要丢掉某个元素,因为这个元素可能已经不在子窗口内了。

要实现这样的功能,我们需要一个双向队列,这个队列尾端既能进新的数组下标又能删除数组下标,另一端进行读取数组下标,也能删除数组下标。

import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param num int整型一维数组 
     * @param size int整型 
     * @return int整型ArrayList
     */
    public ArrayList<Integer> maxInWindows (int[] num, int size) {
        // write code here
        // 窗口大于数组长度或窗口长度为0的时候,返回空。
        ArrayList<Integer> window = new ArrayList<Integer>();
        if (num.length < size || num.length == 0 || size == 0)  return window;
        // 使用集合维护一个双向队列,队列中存储数组元素下表
        ArrayDeque<Integer> dq = new ArrayDeque<Integer>();
        // 先遍历一个子窗口
        for (int i = 0; i < size; i++) {
            // 去掉比自己先进队列且小于自己的元素的下标,然后对应数组下标再入队
            while (!dq.isEmpty() && num[dq.peekLast()] < num[i]) {
                dq.pollLast();
            }
            dq.add(i);
        }
        // 遍历后续数组元素
        for (int i = size; i < num.length; i++) {
            // 每移动一次子窗口,返回数组中添加一个值,添加的这个值是队列中的第一个数组下标对应num数组中的值
            window.add(num[dq.peekFirst()]);
            //每移动一次子窗口,队列中有可能需要移走对应下标,需要移走的下标一定在队列最前端
            if(!dq.isEmpty() && dq.peekFirst() < (i - size + 1))
                dq.pollFirst();
            // 加入新的值前,去掉比自己先进队列且小于自己的元素的下标,然后对应数组下标再入队
            while(!dq.isEmpty() && num[dq.peekLast()] < num[i]) {
                dq.pollLast();
            }
            dq.add(i);
        }
        window.add(num[dq.pollFirst()]);
        return window;
    }


}

Read more

如何设计秒杀系统

什么是秒杀设计 秒杀设计 是一种针对高并发、大流量场景的系统设计,通常应用于电商活动中(如限时抢购、促销等),用户在非常短的时间内大量涌入系统,抢购有限的商品或优惠。这种场景下,系统需要能够承受巨大的瞬时并发请求,同时保证数据的一致性和业务的正确性。 秒杀技术分析 秒杀系统贯穿活动、商品和下单三个领域,涵盖了页面静态化、接口限流、Redis预减库存、异步下单和接口动态化等技术。 需要迎接的挑战有: 1. 高并发和压力测试:秒杀活动会带来巨大的流量,服务器和数据库的并发处理能力是关键。 2. 保证数据一致性:抢购涉及到商品库存的实时减少,需要保证在高并发场景下库存数据的准确性和一致性 3. 防止超卖和重复购买:确保同一商品不会被重复购买,同时避免超卖,即使是在极端的高并发情况下 4. 分布式锁和限流:使用分布式锁来保护关键资源,限制用户访问频率以免系统崩溃 5. 性能优化:包括代码层面的优化、数据库的优化、缓存的使用等,以提高系统性能和响应速度。 页面静态化 秒杀页面静态化是将动态生成的秒杀页面转换为静态HTML页面,从而提高页面响应速度和系统性能

By Yucan Huang

分布式系统下雪花算法生成全局唯一ID

雪花算法 在分布式系统中,为了避免多个节点生成相同的唯一标识符id,我们通常使用一种全局唯一ID生成策略,而Snowflake Algorithm就是广泛使用的解决方案之一。 雪花算法简介 雪花算法生成的 ID 是一个 64 位的二进制整数,具有以下组成部分: 部分 字节长度 描述 符号位 1bit 固定为0,因为生成的ID是整数 时间戳 41bits 表示时间戳,单位是毫秒,可以存储大约68年的时间 机器ID 10bits 用于标识不同的机器(节点)。10位可以将其分数据中心ID(5位)和机器ID(5位),共支持1024台机器同时生成ID 序列号 12bits 同一毫秒内的序列号,用来区分在同一台机器、同一时间戳下生成的多个ID,最大支持同一毫秒生成4096个唯一ID 代码实现 package com.can.springbootmessage; public class SnowflakeIdGenerator { // 起始时间戳,Long是64位,

By Yucan Huang

责任链设计模式

什么是责任链模式 项目有个请求,需要有对应的服务来处理,然后这个请求可能需要被很多个层级权限的服务来处理。我们将这些处理该请求的服务放在一条链上,链从前往后,是层级更高的服务,第一个服务处理不了,传递到链上的下一个服务,直到这个请求被处理成功。 责任链模式(Chain of Responsibility)是一种处理请求的模式,它让多个处理器都有机会处理该请求,直到其中某个处理器成功处理该请求,责任链模式把多个处理器串成链,然后让请求在链上传递。 1. 如何把多个处理器串成链,然后让请求在链上传递 客户端发送请求,处理类去处理它的请求,所以有个处理方法(handleRequest),处理类要连接在一起,所以**处理类要有一个方法(成员变量nextHandler)**指向下一个处理类。 我们抽象出一个公共的父类,然后去定义不同的处理类,这些处理类通过nextHandler连接起来。 2. 代码演示 package interview.pattern; public class ChainRespPattern { Handl

By Yucan Huang