剑指offeⅤ(Java 持续更新...)

本文详细探讨了如何利用单调队列解决滑动窗口最大值问题,以及队列中维护最大值的高效方法。同时介绍了序列化二叉树、字符串排列、正则表达式匹配、丑数计算、骰子点数概率以及打印特定位数数字的多种算法实现,深入浅出地解析了这些经典信息技术问题的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

DAY22

59-Ⅰ:滑动窗口的最大值

59-Ⅱ:队列的最大值

DAY23

37:序列化二叉树(太难了,第一轮刷放过了。下一轮再看)

38:字符串的排列(不行 太难了 回看)

DAY24

19:正则表达式匹配(回看)

49:丑数(多领悟)

 60:n个骰子的点数

DAY25

17:打印从1到最大的n位数

51:

DAY26

14-Ⅱ:剪绳子

43:1~n整数中1出现的次数

44:数组序列中某一位的数字

46:把数字翻译成字符串

48:最长不含重复字符的子字符串


DAY22

59-Ⅰ:滑动窗口的最大值

思路:单调队列。窗口的边界为 i 和 j ,注意左边界的起始位置和结果数组的长度。双端队列实现高效操作头尾元素,算法保证队列中元素始终按照从大到小排列,所以头部元素就是最大值。

  • 在第一个滑动窗口形成之前只考虑将大的元素放入队列,无需考虑删除队列中窗口丢掉的元素
  • 从第一个窗口形成时开始记录每滑动一次,窗口中的最大值
  • 滑动的时候,如果左边要丢掉的数据是滑动前窗口中最大值,则要同时删除队列中的头元素。右边新扩充进来的元素需要和队列里的元素比较,循环删除队列中比新元素小的元素,此时新元素便是队列中最小的元素,将其添加至队尾即可(若没有就不用删除,直接添加到队尾)
  • 因为队列是从大到小的排列顺序,所以将队列头元素添加至结果数组就是当前窗口的最大值
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length == 0 || k == 0) return new int[0];
        Deque<Integer> deque = new LinkedList<>(); //双端队列,里面的数字从大到小排列
        int[] res = new int[nums.length - k + 1];
        for(int j = 0, i = 1 - k; j < nums.length; i++, j++){
            //如果窗口滑动删除的元素是滑动前窗口里最大的元素,则要将队列里的一起删除
            if(i > 0 && deque.peekFirst() == nums[i - 1])
                deque.removeFirst();
            //删除队列中比新扩充进来的元素小的
            while(!deque.isEmpty() && deque.peekLast() < nums[j])
                deque.removeLast();
            deque.addLast(nums[j]);
            if(i >= 0)
                res[i] = deque.peekFirst();
        }
        return res;
    }
}

59-Ⅱ:队列的最大值

思路:辅助队列。队列queue负责存放数据,deque负责存放最大值

入队:

  • 将元素直接加入queue中
  • 当deque不为空时,将双端队列中比新元素小的都移除,然后再将其添加至队尾。(若没有直接添加至队尾)。以保证双端队列里的元素从小到大排列

最大值

  • deque不为空的时候返回头元素即可

移除元素

  • 需要判断移除的是不是当前最大值,若queue中移除的元素与deque的头元素相同,那么要同时移除deque中的元素
  • 若不是当前最大值直接移除返回即可
class MaxQueue {
    Queue<Integer> queue;
    Deque<Integer> deque; //保证从大到小
    public MaxQueue() {
        queue = new LinkedList<>();
        deque = new LinkedList<>();
    }
    
    public int max_value() {
        if(deque.isEmpty()) return -1;
        return deque.peekFirst();
    }
    
    public void push_back(int value) {
        while(!deque.isEmpty() && deque.peekLast() < value)
            deque.pollLast(); //返回并移除
        deque.offerLast(value); //添加成功返回true,否则false
        queue.offer(value);
    }
    
    public int pop_front() {
        if(queue.isEmpty()) return -1;
        /* int rmv = queue.poll();
           if(rmv == deque.peekFirst()) deque.pollFirst();
           return rmv; 更快*/
        if(queue.peek().equals(deque.peekFirst())) deque.pollFirst();
        return queue.poll();
    }
}

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue obj = new MaxQueue();
 * int param_1 = obj.max_value();
 * obj.push_back(value);
 * int param_3 = obj.pop_front();
 */

DAY23

37:序列化二叉树(太难了,第一轮刷放过了。下一轮再看)

思路:BFS

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if(root == null) return "[]";
        StringBuilder res = new StringBuilder("[");
        Queue<TreeNode> queue = new LinkedList<>(){{add(root);}};
        while(!queue.isEmpty()){
            TreeNode node = queue.poll();
            if(node != null){
                res.append(node.val + ",");
                queue.add(node.left);
                queue.add(node.right);
            }else res.append("null,");
        }
        //System.out.println(res.toString());
        res.deleteCharAt(res.length() - 1);
        res.append("]");
        return res.toString();
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if(data.equals("[]")) return null;
        String[] vals = data.substring(1, data.length() - 1).split(",");
        TreeNode root = new TreeNode(Integer.parseInt(vals[0])); //字符串第一个是root
        Queue<TreeNode> queue = new LinkedList<>(){{add(root);}};
        int i = 1;
        while(!queue.isEmpty()){
            TreeNode node = queue.poll();
            if(!vals[i].equals("null")){
                node.left = new TreeNode(Integer.parseInt(vals[i]));
                queue.add(node.left);
            }
            i++;
            if(!vals[i].equals("null")){
                node.right = new TreeNode(Integer.parseInt(vals[i]));
                queue.add(node.right);
            }
            i++;
        }
        return root;
    }
}

// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.deserialize(codec.serialize(root));

38:字符串的排列(不行 太难了 回看)

思路:DFS。如下图,可看成一个二叉树。x为固定位。

  •  将c[ i ]放入set中,遇到重复的跳过进行下一轮循环
  • 交换c[ i ]和c[ x ],即固定c[ i ]为当前字符
  • 递归,调用dfs(x + 1),即开始固定x +1 个字符(累加固定)
  • 还原c[ i ] 和c [ x ]
class Solution {
    List<String> res = new LinkedList<>();
    char[] c;
    public String[] permutation(String s) {
        c = s.toCharArray();
        dfs(0);
        return res.toArray(new String[res.size()]);
    }
    void dfs(int x){
        if(x == c.length - 1){
            res.add(String.valueOf(c)); //将c转换成字符串添加到res里
            return;
        }
        HashSet<Character> set = new HashSet<>();
        for(int i = x; i < c.length; i++){
            if(set.contains(c[i])) continue; //已经存在的 剪枝
            set.add(c[i]);
            swap(i, x);
            dfs(x + 1);
            swap(i, x);
        }
    }
    void swap(int a, int b){
        char temp = c[a];
        c[a] = c[b];
        c[b] = temp;
    }
}

DAY24

19:正则表达式匹配(回看)

49:丑数(多领悟)

思路:动态规划。首先要知道丑数一定是通过前面的数乘以2、3或5得来的。但是如果给每个数都分别乘以2,3,5排下去就不一定是按顺序的了。

为什么乘以几就要让几对应的指针+1?因为p2表示:dp[p2]之前的数字都已经和2乘过了,下一个需要乘2的就是dp[p2](这个数只是还没和2相乘,有没有和3/5相乘不知道)。p3和p5同理表示。

下一个丑数一定是通过前面的丑数*2/3/5得来的,所以只需要挑出里面最小的作为下一个丑数即可。得到丑数后我们就需要判断这个丑数是dp[p几]*几。

当一个丑数可以通过多个dp[p]*p得到,那么可以得到该丑数的p应该同时+1.

class Solution {
    public int nthUglyNumber(int n) {
        int[] dp = new int[n + 1];
        dp[1] = 1; //dp[0] = 0
        int p2 = 1, p3 = 1, p5 = 1;
        for(int i = 2; i <= n; i++){
            int num2 = dp[p2] * 2, num3 = dp[p3] * 3, num5 = dp[p5] * 5;
            dp[i] = Math.min(Math.min(num2, num3), num5);
            if(dp[i] == num2){
                p2++;
            }
            if(dp[i] == num3){
                p3++;
            }
            if(dp[i] == num5){
                p5++;
            }
        }
        return dp[n];
    }
}

 60:n个骰子的点数

思路:动态规划。

只有1个骰子时,dp[1]是代表当骰子点数之和为2时的概率,它会对当有2个骰子时的点数之和为3、4、5、6、7、8产生影响,因为当有一个骰子的值为2时,另一个骰子的值可以为1~6,产生的点数之和相应的就是3~8;比如dp[2]代表点数之和为3,它会对有2个骰子时的点数之和为4、5、6、7、8、9产生影响;所以k在这里就是对应着第i个骰子出现时可能出现六种情况。

class Solution {
    public double[] dicesProbability(int n) {
        double[] dp = new double[6];
        Arrays.fill(dp, 1.0 / 6.0); //当只有一个骰子时1~6概率都为1/6
        for(int i = 2; i <= n; i++){
            //每次的点数之和范围会有点变化,点数之和的值最大是i*6,最小是i*1,i之前的结果值是不会出现的;
            //比如i=3个骰子时,最小就是3了,不可能是2和1,所以点数之和的值的个数是6*i-(i-1),化简:5*i+1
            double[] sum = new double[5*i + 1]; 
            for(int j = 0; j < dp.length; j++){
                for(int k = 0; k <6; k++){
                    sum[j + k] += dp[j] / 6.0;
                }
            } 
            dp = sum;
        }
        return dp;
    }
}

DAY25

17:打印从1到最大的n位数

打印数的公式是10^{n}-1

思路一:普通解法

思路二:全排列(不如思路一快)

  • 数组num存放的就是每个数字,所以num[0]的取值范围是1~9,首位不能为0
  • dfs中的for循环就是给num中每一位一次添加数字,当遇到终止条件时就将此时num的字符串转化为成数字返回。
思路一:
class Solution {
    public int[] printNumbers(int n) {
        int[] res = new int[(int)Math.pow(10, n) - 1];
        for(int i = 0; i < res.length; i++){
            res[i] = i + 1;
        }
        return res;
    }
}
思路二
class Solution {
    int[] res;
    int count = 0;
    public int[] printNumbers(int n) {
        res = new int[(int)Math.pow(10, n) - 1];
        for(int i = 1; i <= n; i++){ //i是位数
            for(char j = '1'; j <= '9'; j++){ //j是首位,首位不能为0
                char[] num = new char[i];
                num[0] = j;
                dfs(1, num, i);
            }
        }
        return res;
    }

    void dfs(int index, char[] num, int digit){
        if(index == digit){ 
            //当遍历到最后一位后,将数组中的字符转化为整数类型输出
            res[count++] = Integer.parseInt(String.valueOf(num));
            return;
        }
        for(char i = '0'; i <= '9'; i++){ //除了首位意外取值范围都是0~9
            num[index] = i;
            dfs(index + 1, num, digit);
        }
    }
}

51:

思路:分支思想。归并排序。分割到每组只剩下两个,然后小的在前大的在后,若交换了位置说明原本是逆序的,计数器+1。依次合并,排序换位置的次数为原本逆序的对数,计数。

递归合并算法:指针 i 在左部分遍历,j 在右部分遍历,每部分都是从小到大排列的。

  • 当左部分的第 i 个数大于右部分第 j 个数,那么 左部分i 后面的数全部都大于第 j 个数,计数,并移动指针 i,将第 j 个数放入最终有序的大数组中 。
  • 当左部分的第 i 个数小于右部分第 j 个数,则移动指针 i ,计数,并将第 i 个数放入最终有序大数组中,继续判断。

nums是原始数组,最后便利结束后变成了从小到大排列的顺序数组;temp是中间需要分割比较的数组。(暂存数组 nums闭区间 [i, r] 内的元素至辅助数组temp)

class Solution {
    int[] nums, temp;
    public int reversePairs(int[] nums) {
        this.nums = nums;
        temp = new int[nums.length];
        return mergeSort(0, nums.length - 1);
    }
    int mergeSort(int left, int right){
        if(left >= right) return 0; //终止条件
        int m = (left + right) / 2; //从中间分割
        int res = mergeSort(left, m) + mergeSort(m + 1, right); //递归划分
        //合并
        int i = left, j = m + 1;
        for(int k = left; k <= right; k++)
            temp[k] = nums[k];
        for(int k = left; k <= right; k++){
            if(i == m + 1) 
            //左子数组已合并完,因此添加右子数组当前元素 tmp[j],并执行j=j+1 
                nums[k] = temp[j++];
            else if(j == right + 1 || temp[i] <= temp[j])
            //右子数组已合并完,因此添加左子数组当前元素 tmp[i],并执行i=i+1
            //temp[i] <= temp[j]的操作也是i+=1,所以合并成一个if
                nums[k] = temp[i++];
            else{
                nums[k] = temp[j++];
                res += m - i + 1; //从i到m的数都比第j个数大
            }
        }
        return res;
    }
}

DAY26

14-Ⅱ:剪绳子

思路:与Ⅰ的区别就是绳长的范围变大了,所以需要有大数取余的过程。

由算术几何均值不等式得:当且仅当n_{1}=n_{2}=...=n_{a}时候等号成立。

\frac{n_{1}+n_{2}+...+n_{a}}{a}\geq \sqrt[a]{n_{1}n_{2}...n_{a}}

推论1:将绳子以相等的长度等分为多段 ,得到的乘积最大。

推导过程:https://leetcode.cn/problems/jian-sheng-zi-ii-lcof/solution/mian-shi-ti-14-ii-jian-sheng-zi-iitan-xin-er-fen-f/

推论2:尽可能将绳子以长度3等分为多段时,乘积最大。

所以可得切分规则:

  • 把绳子尽可能切为多个长度为3的片段,留下的最后一段绳子长度可能是0,1,2
  • 若最后一段为2,直接乘即可,相当于和上一段凑成了3*2,比将它拆开1*1大
  • 若最后一段为1,与上一段合并为4,拆分成2*2比3*1要大

大数越界:当a增大时,最后返回的3^a大小以指数级别增长,可能超出 int32 甚至 int64 的取值范围,导致返回值错误。

求余问题:在仅使用 int32 类型存储的前提下,正确计算 x^a 对p求余的值。(越界后除以p取余数继续验算,对最后的结果无影响!!)

class Solution {
    public int cuttingRope(int n) {
        if( n <= 3) return n - 1; //n从2开始
        int b = n % 3, p = 1000000007; //将绳子切成每段长度为3的,最后余下的那一段的长度b可能是0/1/2
        long res = 1;
        int a = n / 3; //一共切成了a段
        for(int i = 1; i < a; i++)
            res = (res * 3) % p; //从第一段开始验算3^a是否越界,一共验算a-1次
            if(b == 0) return (int)(res * 3 % p);
            if(b == 1) return (int)(res * 4 % p); //余1的时候可以将多余的和前一个3合并为4,4拆成2*2更大
            return (int)(res * 6 % p); //余2的时候和上一段合并为5 拆成2*3
    }
}



// 求 (x^a) % p —— 循环求余法。固定搭配建议背诵
   public long  remainder(int x,int a,int p){  //x为底数,a为幂,p为要取的模
        long rem = 1 ;
        for (int i = 0; i < a; i++) {
            rem = (rem * x) % p ;   
        }
        return rem;
    }

43:1~n整数中1出现的次数

思路:将1~n的个位、十位、百位...的1的次数相加即可。

  • 当前值为0时,此位 1 的出现次数只由高位 high 决定,计算公式为:high×digit
  • 当前值为1时,计算公式为:high×digit+low+1
  • 当前值>1时,计算公式为(high+1)*digit

用23041举例,我的理解是:

当前cur在百位为0,现在要求百位1的个数,那么取值范围应该是00100~22199。也就是说高位部分有00~22共23种可取值,而地位部分从00到99都可以取不影响,共100种,所以低位部分可取值范围就是digit,根据排列组合可得共有23*100种,也就是high*digit

用23141举例:

当前cur在百位为1,那么取值范围应该是00100~23141。高位部分可能取值是00~23,这里要注意的是当高位部分等于23的时候,低位部分的可取值范围就不是00~99了,而是00~41,所以才和低位是有关系的。这样就可以得出高位是00~22的时候低位是00~99,共high*digit中,再加上高位是23时,低位是00~41共42种可能,也就是low + 1。所以计算公式为high*digit+low+1

用23441举例

当前cur在百位为4,取值范围应该是00100~23199。高位部分可取范围是00~23,这个与上一个不同的是当高位部分取23的时候,低位部分可取值范围是00~99,所以与低位无关了。那么计算公式就是(high+1)*digit。

class Solution {
    public int countDigitOne(int n) {
        int res = 0, digit = 1; // 表示位,从个位开始
        int high = n / 10, cur = n % 10, low = 0; //高位从十位开始,cur从个位开始
        while(high != 0 || cur != 0){
            if(cur == 0) res += high *digit;
            else if(cur == 1) res += high * digit + low + 1;
            else res += (high + 1) * digit;
            low += cur * digit; //low是cur后面数字组成的数
            cur = high % 10;
            high /= 10; //high是cur前面的数字组成的数,不用乘位数
            digit *= 10;
        }
        return res;
    }
}

44:数组序列中某一位的数字

数位数量就例如是2位数90个共占用180个数位

  •  先计算出第n为数字是属于几位数部分的
  • 然后再计算出这位是属于那个数字
  • 最后计算这位是属于数字里的第几位

看着蛮简单的,但是中间计算过程好费脑。首先判断n是几位数的时候用它循环减去一位两位三位数...所占的位数,当n递减的值不再大于某位数占用的位数时候则说明它属于某位数。

注意数字和n都是从0开始的。所以后面是n-1

比如n=13时候,先减去一位数占的位数9之后剩下4,二位数占180个位,所以第十三位是属于两位数的。

而现在n的值4就可以计算出它所在的数字是多少。每位数的起始位是0/10/100...,所以用刚才循环里记录的start,加上(n-1)除以位数(2),也就是每两位组成一个数字,便可得到它所属的数字是多少。

最后要判断它是这个数字里的第几位(从0开始),(n-1)除以位数的余数就是它所在数字中的位数,将这个作为索引输出转化为字符串的num对应的位就是结果。

class Solution {
    public int findNthDigit(int n) {
        int digit = 1;
        long start = 1;
        long count = 9;
        while(n > count){
            n -= count; //减去个位数占得位数,百位数占的位数...
            digit += 1; //位数每次+1,即一位数到两位数到三位数到...
            start *= 10; //n位数的起始就是1/10/100/1000 start*10即可
            count = digit * start * 9; //数位数量
        }
        long num = start + (n - 1) / digit; //得到第n位所在的数字是多少
        //将数字转化为字符,输出它的第0/1/2/3..位
        return Long.toString(num).charAt((n - 1) % digit) - '0';
    }
}

46:把数字翻译成字符串

思路:动态规划。对于num里的一个数有两种情况:

  • 只翻译自己
  • 和前一个数字组成10~25之间的两位数翻译

类同青蛙跳台阶,只不过这个一次跳两阶有条件限制

  • 当大于10小于25,dp[ i ] = dp[ i - 1 ] + dp[ i - 2 ]
  • 不在范围内则是dp[ i ] = dp[ i - 1 ]
class Solution {
    public int translateNum(int num) {
        String s = String.valueOf(num); //转换成字符串
        int a = 1, b = 1;
        for(int i = 2; i <= s.length(); i++){
            String temp = s.substring(i - 2, i);
            //当两位数大于10小于25的时候也可以翻译,否则就只能单独翻译
            int c = temp.compareTo("10") >= 0 && temp.compareTo("25") <= 0 ? a + b : a;
            /*
            int value=str1.compareTo(str2);
            当str1小于str2时,返回小于0的值,
            当str1与str2相同时,返回0,
            当str1大于str2时,返回大于0的值。
            */
            b = a;
            a = c;
        }
        return a;
    }
}

48:最长不含重复字符的子字符串

思路:动态规划。

  • 字符不重复:长度+1,此时最大子串长度 = 上一位的最大子串长度 + 1
  • 字符重复:不考虑中间存在其他重复的前提下,最大子串长度 = 现在的位置 - 相同字符上次出现的位置

/*
字符    a   b   c   c   c   b   a   d                                                          
长度    1   2   3   1   1   2   3   4
*/
class Solution {
    public int lengthOfLongestSubstring(String s) {
        int ans = 0, n = s.length(), last = 0;
        HashMap<Character, Integer> map = new HashMap<>(n);
        for(int i = 0; i < n; i++){
            Character c = s.charAt(i);
            last = Math.min(i - map.getOrDefault(c, -1), last + 1);
            ans = Math.max(ans, last);
            map.put(c, i);
        }
        return ans;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值