Algorithm

@[AKPower]

算法

Manacher (马拉车)算法

最长回文子串

// Manacher算法
class Solution {
    public String longestPalindrome(String s) {
        // 构造#字符串
        String s1 = new String();
        for(int i=0;i<s.length();i++){
            s1 = s1 + "#"+s.charAt(i);
        }
        s1 = s1+"#";
        int len = s1.length();
        int[] d1 = new int[len+1];  //d1[i]表示s1中以i为中心的回文串的半径,恰好对应s中回文串长度
        int l=0,r=-1;
        int ans = -1;
        int rem = 0;
        //迭代更新以i为中心的半径值
        for(int i=0;i<len;i++){
            int d = i<r?Math.min(r-i,d1[l+r-i]):1;
            while(d<=i&&i+d<len&&s1.charAt(i+d)==s1.charAt(i-d)){
                d++;
            }
            d--;
            d1[i] = d;
            if(d>ans){
                ans = d;
                rem = i;
            }
            if(i+d>r){
                l = i-d;
                r = i+d;
            }
        }
        //找出回文串
        String ans_s = new String();
        for(int i=rem-d1[rem];i<=rem+d1[rem];i++){
            if(s1.charAt(i)=='#')continue;
            ans_s = ans_s+s1.charAt(i);
        }
        return ans_s;
    }
}

单调栈

单调栈用途:再一次遍历中,不断寻找遍历元素x向左或向右第一个大于或小于x的位置

单调性判断:在一次更新中,要看栈顶元素是否是比当前值大才能计算(出栈才能计算)-递增栈,还是比当前值小才能计算-递减栈

单调栈总结:使用到单调栈解决问题时绝不是先去想单调栈的设计进而贴合题目,而是先从问题本身出发

去除重复字母

给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

示例 1:

输入:s = "bcabc"
输出:"abc"

示例 2:

输入:s = "cbacdcbc"
输出:"acdb"

提示:

  • 1 <= s.length <= 104
  • s 由小写英文字母组成
// 
class Solution {
    public String removeDuplicateLetters(String s) {
        Stack<Character> st = new Stack<>();
        int[] dir = new int[26]; // 保存每个字符最后出现的下标
        boolean[] instack = new boolean[26]; // 记录字符是否已经在栈中
        for(int i=0;i<s.length();i++){
            int num = s.charAt(i)-'a';
            dir[num] = i;
        }
        for(int i=0;i<s.length();i++){
            int num = s.charAt(i)-'a';
            if(instack[num])continue;
            while(!st.isEmpty()&&st.peek()-'a'>=num&&dir[st.peek()-'a']>=i)instack[st.pop()-'a']=false;
            st.add(s.charAt(i));
            instack[num] = true;
        }
        StringBuilder sb = new StringBuilder();
        while(!st.isEmpty()){
            sb.append(st.pop());
        }
        return sb.reverse().toString();
    }
}

移掉 K 位数字

给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。

示例 1 :

输入:num = "1432219", k = 3
输出:"1219"
解释:移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219 。

示例 2 :

输入:num = "10200", k = 1
输出:"200"
解释:移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。

示例 3 :

输入:num = "10", k = 2
输出:"0"
解释:从原数字移除所有的数字,剩余为空就是 0 。

提示:

  • 1 <= k <= num.length <= 105
  • num 仅由若干位数字(0 - 9)组成
  • 除了 0 本身之外,num 不含任何前导零
class Solution {
    /**
        单调递增栈:消除扫描过程中的峰值
     */
    public String removeKdigits(String num, int k) {
        Stack<Integer> st = new Stack<>();
        for(int i=0;i<num.length();i++){
            char c = num.charAt(i);
            while(!st.isEmpty()&&num.charAt(st.peek())-c>0&&k>0){
                st.pop();
                k--;
            }
            st.add(i);
        }
        // 还没移除完时,从大的开始移除
        while(k>0){
            st.pop();
            k--;
        }
        StringBuilder sb = new StringBuilder();
        st.stream().map(c->num.charAt(c)-'0').forEach(sb::append);
        String ans = sb.toString();
        int index = 0;
        // 消除前导零
        while(index<ans.length()&&ans.charAt(index)=='0'){
            index++;
        }
        if(index==ans.length())return "0";
        return ans.substring(index);
    }
}

子数组最小值之和

给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。

由于答案可能很大,因此 返回答案模 10^9 + 7 。

class Solution {
    private static int mod = 1000000007;
    public int sumSubarrayMins(int[] arr) {
        int[] l =new int[arr.length];
        int[] r =new int[arr.length];
        Stack<Integer> s = new  Stack<>();
        //左边不重复
        for(int i=0;i<arr.length;i++){
            // 找到左边第一个小于等于arr[i]的位置
            while(!s.isEmpty()&&arr[s.peek()]>arr[i]){
                s.pop();
            }
            int L = -1;
            if(!s.isEmpty())L = s.peek();
            l[i] = i-L-1;
            s.add(i);
        }
        s.clear();
        //右边重复一次
        for(int i=arr.length-1;i>=0;i--){
            // 找到右边第一个小于arr[i]的位置
            while(!s.isEmpty()&&arr[s.peek()]>=arr[i]){
                s.pop();
            }
            int R = arr.length;
            if(!s.isEmpty())R = s.peek();
            r[i] = R-i-1;
            s.add(i);
        }
        long sum = 0;
        // 这里必须先转化为long类型
        for(int i=0;i<arr.length;i++){
            sum = (sum + ((l[i]+1)*(r[i]+1))*(long)arr[i])%mod;
        }
        return (int)sum;
    }
}

验证前序遍历序列二叉搜索树(未解决)

给定一个 无重复元素 的整数数组 preorder如果它是以二叉搜索树的先序遍历排列 ,返回 true

示例 1:

img

输入: preorder = [5,2,1,3,6]
输出: true

二分

合并 K 个升序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

示例 2:

输入:lists = []
输出:[]

示例 3:

输入:lists = [[]]
输出:[]
// 二分法类:似于归并排序去合并两表
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length==0)return null;
        return dfs(lists,0,lists.length-1);
    }
    // 二分要合并的区间
    public ListNode dfs(ListNode[] lists,int l,int r){
        if(l==r)return lists[l];
        int mid = (l+r)>>1;
        ListNode l_node = dfs(lists,l,mid);
        ListNode r_node = dfs(lists,mid+1,r);
        return merge(l_node,r_node);
    }
    // 合并函数
    public ListNode merge(ListNode a,ListNode b){
        ListNode head = new ListNode(0);
        ListNode p = head;
        while(a!=null&&b!=null){
            if(a.val<=b.val){
                p.next = a;
                p = a;
                a = a.next;
            }
            else{
                p.next = b;
                p = b;
                b = b.next;
            }
        }
        if(a!=null){
            p.next = a;
        }
        else if(b!=null){
            p.next = b;
        }
        return head.next;
    }
}

找出最安全路径(37场周赛)

给你一个下标从 0 开始、大小为 n x n 的二维矩阵 grid ,其中 (r, c) 表示:

  • 如果 grid[r][c] = 1 ,则表示一个存在小偷的单元格
  • 如果 grid[r][c] = 0 ,则表示一个空单元格

你最开始位于单元格 (0, 0) 。在一步移动中,你可以移动到矩阵中的任一相邻单元格,包括存在小偷的单元格。

矩阵中路径的 安全系数 定义为:从路径中任一单元格到矩阵中任一小偷所在单元格的 最小 曼哈顿距离。

返回所有通向单元格 (n - 1, n - 1) 的路径中的 最大安全系数

单元格 (r, c) 的某个 相邻 单元格,是指在矩阵中存在的 (r, c + 1)(r, c - 1)(r + 1, c)(r - 1, c) 之一。

两个单元格 (a, b)(x, y) 之间的 曼哈顿距离 等于 | a - x | + | b - y | ,其中 |val| 表示 val 的绝对值。

示例 1:

img
输入:grid = [[1,0,0],[0,0,0],[0,0,1]]
输出:0
解释:从 (0, 0) 到 (n - 1, n - 1) 的每条路径都经过存在小偷的单元格 (0, 0) 和 (n - 1, n - 1) 。
// 二分加dfs进行寻找最优安全路径
/**
1.二分安全系数
2.对于某个安全系数mid,如果能找到一条路径其每个点的安全系数都不小于mid,说明存在安全系数至少为mid的路径
3.所有路径的安全系数之多是min(d[0][0],d[n-1][n-1])
*/      
class Solution {
    int[][] d;
    static int[][] nex = {{0,1},{0,-1},{1,0},{-1,0}};
    Set<Integer> vis;
    public int maximumSafenessFactor(List<List<Integer>> grid) {
        int n = grid.size();
        // 定义
        d = new int[n+1][n+1];
        vis = new HashSet<>();
        //初始化安全系数矩阵
        for(int i=0;i<n;i++){
            for(int j = 0;j<n;j++){
                d[i][j] = -1;
                if(grid.get(i).get(j)==1)d[i][j] = 0; //小偷的地方安全系数为0
            }
        }
        // 计算每个点的安全系数
        boolean change = true; //判断是否计算完所有的安全系数
        // l代表安全系数,那我们就找安全系数为l-1的点进而找到安全系数为l的点
        for(int l = 1;l<n*2&&change;l++){
            change = false;
            for(int i=0;i<n;i++){
                for(int j=0;j<n;j++){
                    if(d[i][j]!=l-1)continue;
                    for(int k=0;k<4;k++){
                        int tx = i+nex[k][0];
                        int ty = j+nex[k][1];
                        if(tx<0||ty<0||tx>=n||ty>=n||d[tx][ty]!=-1)
                        continue;
                        d[tx][ty] = l;
                        change = true;
                    }
                }
            }
        }
        // 二分加dfs进行寻找最优安全路径
        /**
            1.二分安全系数
            2.对于某个安全系数mid,如果能找到一条路径其每个点的安全系数都不小于mid,说明存在安全系数至少为mid的路径
            3.所有路径的安全系数之多是min(d[0][0],d[n-1][n-1])
         */
        int l = 0,r = Math.min(d[0][0],d[n-1][n-1]);
        while(l<=r){
            int mid = (l+r)>>1;
            if(dfs(0,0,n,mid)){
                l = mid+1;
            }
            else r = mid-1;
            vis.clear();
        }
        return r;
    }
    public boolean dfs(int x,int y,int n,int dis){
        if(x==n-1&&y==n-1){
            return true;
        }
        if(vis.contains(x*n+y))return false;
        vis.add(x*n+y);
        for(int k=0;k<4;k++){
            int tx = x+nex[k][0];
            int ty = y+nex[k][1];
            // d[tx][ty]<dis 说明此路径安全系数低于dis,不可行
            if(tx<0||ty<0||tx>=n||ty>=n||d[tx][ty]<dis)continue;
            boolean flag = dfs(tx,ty,n,dis);
            if(flag)return true;
        }
        return false;
    }
}

发下午茶

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

import java.util.*;
import java.io.*;

class Solution{
    //静态方法里必须使用静态成员及函数
    public static int K,N;
    public static int[] T; 
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        K = sc.nextInt();
        N = sc.nextInt();
        T = new int[N+1];
        for(int i=0;i<N;i++){
            T[i] = sc.nextInt();
        }
        int l = 0,r = 10001001;
        int mid;
        while(l<=r){
            mid = (l+r)>>1;
            if(JD(mid)){
                r = mid-1;
            }
            else l = mid+1;
        }
        System.out.println(l);
    }
    //判断函数
    public static boolean JD(int d){
        int[] t = new int[T.length];
        int id=0;
        while(id<K&&t[N-1]<T[N-1]){
            int p = 0;
            for(int i=0;i<N&&p<d;i++){
                if(p+1+T[i]-t[i]<=d){
                    p = p+1+T[i]-t[i];
                    t[i] = T[i];
                }
                else {
                    t[i] += (d - (p+1));
                    p = d;
                }
            }
            id++;
        }
        if(t[N-1]==T[N-1])return true;
        return false;
    }
}

机器人跳跃问题(有坑点)

机器人正在玩一个古老的基于 DOS 的游戏。游戏中有 N+1 座建筑——从 0 到 N 编号,从左到右排列。编号为 0 的建筑高度为 0 个单位,编号为 i 的建筑的高度为 H(i) 个单位。
起初, 机器人在编号为 0 的建筑处。每一步,它跳到下一个(右边)建筑。假设机器人在第 k 个建筑,且它现在的能量值是 E, 下一步它将跳到第个 k+1 建筑。它将会得到或者失去正比于与 H(k+1) 与 E 之差的能量。如果 H(k+1) > E 那么机器人就失去 H(k+1) - E 的能量值,否则它将得到 E - H(k+1) 的能量值。
游戏目标是到达第个 N 建筑,在这个过程中,能量值不能为负数个单位。现在的问题是机器人以多少能量值开始游戏,才可以保证成功完成游戏?

解题思路:二分能量值;

问题:如果e = 100000,h = 100000, h[] = [0,0,.....] 在进行判断的时候 e = e*2 的的方式增长,然而在e = max(h[])的时候就可以return true

子数组最大平均数 II(困难)

给你一个包含 n 个整数的数组 nums ,和一个整数 k

请你找出 长度大于等于 k 且含最大平均值的连续子数组。并输出这个最大平均值。任何计算误差小于 10-5 的结果都将被视为正确答案。

示例 1:

输入:nums = [1,12,-5,-6,50,3], k = 4
输出:12.75000
解释:
- 当长度为 4 的时候,连续子数组平均值分别为 [0.5, 12.75, 10.5] ,其中最大平均值是 12.75 。
- 当长度为 5 的时候,连续子数组平均值分别为 [10.4, 10.8] ,其中最大平均值是 10.8 。
- 当长度为 6 的时候,连续子数组平均值分别为 [9.16667] ,其中最大平均值是 9.16667 。
当取长度为 4 的子数组(即,子数组 [12, -5, -6, 50])的时候,可以得到最大的连续子数组平均值 12.75 ,所以返回 12.75 。
根据题目要求,无需考虑长度小于 4 的子数组。
/*
前缀和 和 二分查找
- 最大平均值在数组最大值和最小值之间,不断猜测最大平均值mid,寻找子数组平均值大于mid的
- 判断:(a1−mid)+(a2−mid)+(a3−mid)...+(aj−mid)≥0则存在子数组,存在left=mid
- 也可能是其中一段数组,所以记录数组前缀和,
		数组长度超过k之后,用当前前缀和减去前面最小的前缀和,如果大于0则存在
- 为了在第二个循环里判断第k个之后的前缀和,一定给sum下标加1
*/
class Solution {
    public double findMaxAverage(int[] nums, int k) {
        double left = -10000;//这里用数组的最大最小范围
        double right = 10000;
        double ret = 0;
        while(left+0.00001<right){
            double mid = (left+right)*0.5;
            if(check(nums,k,mid)){
                left=mid;
                ret=mid;
            }else{
                right=mid;
            }
        }
        return ret;
    }
    public boolean check(int[] nums, int k,double mid){
        //为了和k保持同步给sum下标+1
        double[] sum = new double[nums.length+1];//数组每个位置的前缀和
        sum[0] = 0;
        for(int i=1;i<k;i++){//前k-1个
            sum[i] = nums[i-1] - mid + sum[i-1];
        }
        double minSum = 0;//最小的前缀和
        for(int i=k;i<=nums.length;i++){//第k个开始会判断是否合适,以及更新最小值
            sum[i] = nums[i-1] - mid + sum[i-1];
            if(sum[i]-minSum>=0)return true;
            if(sum[i+1-k]<minSum)minSum = sum[i+1-k];//前面的中最小值
        }
        return false;
    }
}

动态规划

和为目标值的最长子序列的长度(01背包)

给你一个下标从 0 开始的整数数组 nums 和一个整数 target

返回和为 targetnums 子序列中,子序列 长度的最大值 。如果不存在和为 target 的子序列,返回 -1

子序列 指的是从原数组中删除一些或者不删除任何元素后,剩余元素保持原来的顺序构成的数组。

示例 1:

输入:nums = [1,2,3,4,5], target = 9
输出:3
解释:总共有 3 个子序列的和为 9 :[4,5] ,[1,3,5] 和 [2,3,4] 。最长的子序列是 [1,3,5] 和 [2,3,4] 。所以答案为 3 。

示例 2:

输入:nums = [4,1,3,2,1,5], target = 7
输出:4
解释:总共有 5 个子序列的和为 7 :[4,3] ,[4,1,2] ,[4,2,1] ,[1,1,5] 和 [1,3,2,1] 。最长子序列为 [1,3,2,1] 。所以答案为 4 。

示例 3:

输入:nums = [1,1,5,4,5], target = 3
输出:-1
解释:无法得到和为 3 的子序列。

提示:

  • 1 <= nums.length <= 1000
  • 1 <= nums[i] <= 1000
  • 1 <= target <= 1000
class Solution {
    public int lengthOfLongestSubsequence(List<Integer> nums, int target) {
        // 统计前i个数装进target为j的容器里装满时的最多的个数
        int[] dp = new int[target+1];
        // 统计前i个数装进target为j的容器里最多能装多少容量
        int[] load = new int[target+1];
        for(int num:nums){
            for(int j = target;j>=num;j--){
                load[j] = Math.max(load[j],load[j-num]+num);
                // 只有容量能装满时才更新dp数组
                if(load[j]==j)dp[j] = Math.max(dp[j],dp[j-num]+1);
            }
        }
        return load[target]==target?dp[target]:-1;
    }
}

零钱兑换-(完全背包)

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

示例 3:

输入:coins = [1], amount = 0
输出:0

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 231 - 1
  • 0 <= amount <= 104
class Solution {
    public int coinChange(int[] coins, int amount) {
        if(amount==0)return 0;
        // 记录前i个硬币装多次总金额为j时最少使用的个数
        int[] dp = new int[amount+1];
        Arrays.fill(dp,amount+1);
        dp[0] = 0;
        for(int num:coins){
            for(int j=0;j+num<=amount;j++){ // 01背包相反
                dp[j+num] = Math.min(dp[j+num],dp[j]+1);
            }
        }
        if(dp[amount]>amount)return -1;
        return dp[amount];
    }
}

最小高度树(换根DP)

树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。

给你一棵包含 n 个节点的树,标记为 0n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 aibi 之间存在一条无向边。

可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树

请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。

树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。

示例 1:

img

输入:n = 4, edges = [[1,0],[1,2],[1,3]]
输出:[1]
解释:如图所示,当根是标签为 1 的节点时,树的高度是 1 ,这是唯一的最小高度树。

示例 2:

img

输入:n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]]
输出:[3,4]

提示:

  • 1 <= n <= 2 * 104
  • edges.length == n - 1
  • 0 <= ai, bi < n
  • ai != bi
  • 所有 (ai, bi) 互不相同
  • 给定的输入 保证 是一棵树,并且 不会有重复的边
//树形 DP 中的换根 DP 问题又被称为二次扫描,通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。

//通常需要两次 DFS,第一次 DFS 预处理诸如深度,点权和之类的信息,在第二次 DFS 开始运行换根动态规划。
class Solution {
    List<Integer> L_arr[] ;
    int[] dp;
    int[] dps;
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        if(n==1)return Arrays.asList(0);
        L_arr = new ArrayList[n];
        dp = new int[n];
        dps = new int[n];
        for(int i=0;i<n;i++)L_arr[i] = new ArrayList<>();
        for(int i=0;i<n-1;i++){
            L_arr[edges[i][0]].add(edges[i][1]);
            L_arr[edges[i][1]].add(edges[i][0]);
        }
        dfs1(0,-1);
        dfs2(0,-1);
        List<Integer> ans = new ArrayList<>();
        // 求答案
        int h = n;
        for (int i = 0; i < n; ++i) {
            if (dps[i] < h) {
                h = dps[i];
                ans.clear();
            }
            if (dps[i] == h) ans.add(i);
        }
        return ans;

    }
    // 先用指定的一个根进行预处理
    public int dfs1(int u,int fa){
        for(int v:L_arr[u]){
            if(v==fa)continue;
            dp[u] = Math.max(dp[u],dfs1(v,u)+1);
        }
        return dp[u];
    }
    // 使用第二个递归进行换根dp
    public void dfs2(int u,int fa){
        int first = -1,second = -1;
        // 求子树的最大dp值和次大dp值
        for(int v:L_arr[u]){
            if(dp[v]>first){
                second = first;
                first = dp[v];
            }
            else if(dp[v]>second)second = dp[v];
        }
        // 先保存当前根的dp值,因为后续会有改动
        dps[u] = first+1;
        for(int v:L_arr[u]){
            if(v==fa)continue;
            // 根据下一步的根来修改当前节点的dp值
            dp[u] = (dp[v]==first?second:first)+1;
            dfs2(v,u);
        }
    }
}

栅栏涂色

k 种颜色的涂料和一个包含 n 个栅栏柱的栅栏,请你按下述规则为栅栏设计涂色方案:

  • 每个栅栏柱可以用其中 一种 颜色进行上色。

  • 相邻的栅栏柱 最多连续两个 颜色相同。

    提供解题思路:最多连续两个,可以从dp的多状态进行转换

给你两个整数 kn ,返回所有有效的涂色 方案数

示例 1:

img

输入:n = 3, k = 2
输出:6
解释:所有的可能涂色方案如上图所示。注意,全涂红或者全涂绿的方案属于无效方案,因为相邻的栅栏柱 最多连续两个 颜色相同。
class Solution {
    // dp[n][k]
    public int numWays(int n, int k) {
        int[][] dp = new int[n+1][2];
        //dp[n][0]:以位置为n结尾的最后连个不同色的方案数
        //dp[n][1]:以位置为n结尾的最后连个同色的方案数
        dp[1][0] = k;
        dp[1][1] = 0;

        for(int i=2;i<=n;i++){
            dp[i][0] = (dp[i-1][0] + dp[i-1][1])*(k-1);
            dp[i][1] = dp[i-1][0];
        }
        return dp[n][0]+dp[n][1];
    }
}

范围中美丽整数的数目(数位dp)

给你正整数 lowhighk

如果一个数满足以下两个条件,那么它是 美丽的

  • 偶数数位的数目与奇数数位的数目相同。
  • 这个整数可以被 k 整除。

请你返回范围 [low, high] 中美丽整数的数目。

示例 1:

输入:low = 10, high = 20, k = 3
输出:2
解释:给定范围中有 2 个美丽数字:[12,18]
- 12 是美丽整数,因为它有 1 个奇数数位和 1 个偶数数位,而且可以被 k = 3 整除。
- 18 是美丽整数,因为它有 1 个奇数数位和 1 个偶数数位,而且可以被 k = 3 整除。
以下是一些不是美丽整数的例子:
- 16 不是美丽整数,因为它不能被 k = 3 整除。
- 15 不是美丽整数,因为它的奇数数位和偶数数位的数目不相等。
给定范围内总共有 2 个美丽整数。

示例 2:

输入:low = 1, high = 10, k = 1
输出:1
解释:给定范围中有 1 个美丽数字:[10]
- 10 是美丽整数,因为它有 1 个奇数数位和 1 个偶数数位,而且可以被 k = 1 整除。
给定范围内总共有 1 个美丽整数。

示例 3:

输入:low = 5, high = 5, k = 2
输出:0
解释:给定范围中有 0 个美丽数字。
- 5 不是美丽整数,因为它的奇数数位和偶数数位的数目不相等。

提示:

  • 0 < low <= high <= 109
  • 0 < k <= 20
class Solution {
    // pos mod cnt1 cnt2 pre_zero
    // 核心:思考那些状态能确定0~pos位的个数
    // 0~pos位的限制条件有余数mod,偶数个数cnt1,奇数个数cnt2,是否有前导零pre_ze
    int[][][][][] dp;
    List<Integer> nums;
    int K;
    public int numberOfBeautifulIntegers(int low, int high, int k) {
        dp = new int[15][25][15][15][2];
        for(int i=0;i<15;i++){
            for(int j=0;j<25;j++){
                for(int kk = 0;kk<15;kk++){
                    for(int jj=0;jj<15;jj++){
                        dp[i][j][kk][jj][0] = -1;
                        dp[i][j][kk][jj][1] = -1;
                        }
                }
            }
        }
        nums = new ArrayList<>();
        K = k;
        int sum1 = get(high);
        int sum2 = get(low-1);
        return sum1 - sum2;
    }
    public int get(int n){
        nums.clear();
        while(n>0){
            nums.add(n%10);
            n /= 10;
        }
        return dfs(nums.size()-1,nums.size()/2,nums.size()/2,0,true,1);
    }
    public int dfs(int pos,int cnt1,int cnt2,int mod,boolean limit,int pre_zero){
        if(pos<0){
            if(cnt1 == 0&&cnt2 == 0&&mod == 0)return 1;
            return 0;
        }
        if(!limit&&dp[pos][mod][cnt1][cnt2][pre_zero]!=-1)return dp[pos][mod][cnt1][cnt2][pre_zero];
        int up = limit?nums.get(pos):9;
        int sum  = 0;
        for(int i=0;i<=up;i++){
            if(pre_zero==1&&i==0){
                int dec = (pos%2==1?1:0);
                sum += dfs(pos-1,cnt1-dec,cnt2-dec,mod,false,1);
                continue;
            }
            if(pre_zero==1&&pos%2==0&&i>0)continue;
            if(cnt1==0&&i%2==0)continue;
            if(cnt2==0&&i%2==1)continue;
            sum += dfs(pos-1,cnt1-(i%2==0?1:0),cnt2-(i%2==0?0:1),(mod*10+i)%K,limit&&i==nums.get(pos),0);
        }
        if(!limit)dp[pos][mod][cnt1][cnt2][pre_zero] = sum;
        return sum;
    }
}

夏季特惠(未解决)

某公司游戏平台的夏季特惠开始了,你决定入手一些游戏。现在你一共有X元的预算,该平台上所有的 n 个游戏均有折扣,标号为 i 的游戏的原价ai元,现价只要bi元(也就是说该游戏可以优惠ai-bi元)并且你购买该游戏能获得快乐值为wi。由于优惠的存在,你可能做出一些冲动消费导致最终买游戏的总费用超过预算,但只要满足获得的总优惠金额不低于超过预算的总金额,那在心理上就不会觉得吃亏。现在你希望在心理上不觉得吃亏的前提下,获得尽可能多的快乐值。

大礼包(记忆化搜索)

image-20230727145344259
class Solution {
    Map<List<Integer>,Integer> mp;
    Integer  ans;
    List<Integer> prices;
    public int shoppingOffers(List<Integer> price, List<List<Integer>> special, List<Integer> needs) {
        if(needs==null)return 0;
        mp = new HashMap<>();
        ans = Integer.MAX_VALUE;
        prices = price;
        List<List<Integer>> special_useful = new ArrayList<>();
        //筛选有用的special:比单件买便宜
        for(List<Integer> g:special){
            int sum = 0;
            for(int i=0;i<g.size()-1;i++){
                sum += (g.get(i)*price.get(i));
            }
            if(sum<g.get(g.size()-1))continue;
            special_useful.add(g);
        }
        //记忆化搜索
        dfs(special_useful,needs,0,prices.size());
        return ans;
    }
    public void dfs(List<List<Integer>> special,List<Integer> needs,Integer cost,Integer cnt){
        Integer need_max = Collections.max(needs);
        if(need_max == 0){
            ans = Math.min(ans,cost);
            return;
        }
        //加了剪枝:7ms  不加剪枝:110ms
        if(mp.get(needs)!=null&&mp.get(needs)<=cost)return;
        mp.put(needs,cost);
        // Integer len = special.size();
        boolean flag1 = false;
        //先选择可用的大礼包
        for(List<Integer> g:special){
            boolean flag = false;
            List<Integer> needs_f = new ArrayList<>();
            for(int i=0;i<cnt;i++){
                if(g.get(i)>needs.get(i)){
                    flag = true;
                    break;
                }
                needs_f.add(needs.get(i)-g.get(i));
            }
            if(flag)continue; //大礼包超量
            flag1 = true;
            dfs(special,needs_f,cost+g.get(cnt),cnt);
        }
        if(flag1)return;//在此步买过大礼包,说明没有必要在单件买
        //没有买过大礼包,所以直接把剩余清单按照单件全买
        for(int i=0;i<cnt;i++){
            cost += (prices.get(i)*needs.get(i));
        }
        ans = Math.min(ans,cost);
    }

}

青蛙过河(记忆化搜索)

一只青蛙想要过河。 假定河流被等分为若干个单元格,并且在每一个单元格内都有可能放有一块石子(也有可能没有)。 青蛙可以跳上石子,但是不可以跳入水中。

给你石子的位置列表 stones(用单元格序号 升序 表示), 请判定青蛙能否成功过河(即能否在最后一步跳至最后一块石子上)。开始时, 青蛙默认已站在第一块石子上,并可以假定它第一步只能跳跃 1 个单位(即只能从单元格 1 跳至单元格 2 )。

如果青蛙上一步跳跃了 k 个单位,那么它接下来的跳跃距离只能选择为 k - 1、k 或 k + 1 个单位。 另请注意,青蛙只能向前方(终点的方向)跳跃。

class Solution {
    Map<List<Integer>,Boolean> mp;
    public boolean canCross(int[] stones) {
        mp = new HashMap<>();
        return dfs(stones,0,0);
    }
    public boolean dfs(int[] stones,int id,int k){
        if(id==stones.length-1){
            return true;
        }
        List<Integer> lis = Arrays.asList(id,k);
        if(mp.get(lis)!=null)return false;
        mp.put(lis,true);
        int x;
        //“尝试”去在当前位置跳跃k-1,k,k+1步
        x = stones[id]+k-1;
        int idx = jd(stones,id+1,stones.length-1,x);
        //idx=-1代表无法跳跃(没有落脚的石子)
        if(k-1>0&&idx!=-1&&dfs(stones,idx,k-1))return true;
        x = stones[id]+k;
        idx = jd(stones,id+1,stones.length-1,x);
        if(k>0&&idx!=-1&&dfs(stones,idx,k))return true;
        x = stones[id]+k+1;
        idx = jd(stones,id+1,stones.length-1,x);
        if(k+1>0&&idx!=-1&&dfs(stones,idx,k+1))return true;
        return false;
    }
    //判断在stones中是否有在x位置的石子---二分法
    public int jd(int[] stones,int l,int r,int x){
        while(l<=r){
            int mid = (l+r)>>1;
            if(stones[mid]==x)return mid;
            if(stones[mid]>x){
                r =  mid-1;
            }
            else l = mid+1;
        }
        return -1;
    }
    
}

最短移动距离(未解决)

给定一棵 n 个节点树。节点 1 为树的根节点,对于所有其他节点 i,它们的父节点编号为 floor(i/2) (i 除以 2 的整数部分)。在每个节点 i 上有 a[i] 个房间。此外树上所有边均是边长为 1 的无向边。
树上一共有 m 只松鼠,第 j 只松鼠的初始位置为 b[j],它们需要通过树边各自找到一个独立的房间。请为所有松鼠规划一个移动方案,使得所有松鼠的总移动距离最短。

预测赢家(区间dp)

给你一个整数数组 nums 。玩家 1 和玩家 2 基于这个数组设计了一个游戏。

玩家 1 和玩家 2 轮流进行自己的回合,玩家 1 先手。开始时,两个玩家的初始分值都是 0 。每一回合,玩家从数组的任意一端取一个数字(即,nums[0] 或 nums[nums.length - 1]),取到的数字将会从数组中移除(数组长度减 1 )。玩家选中的数字将会加到他的得分上。当数组中没有剩余数字可取时,游戏结束。

如果玩家 1 能成为赢家,返回 true 。如果两个玩家得分相等,同样认为玩家 1 是游戏的赢家,也返回 true 。你可以假设每个玩家的玩法都会使他的分数最大化。

解题思路:题中的分数最大化可以理解为差值最大化,所以可以使用dp记录每一个区间的先手使差值最大化的取值策略,即:

  1. 选左边

  2. 选右边
    d p [ l ] [ r ] = m a x ( n u m s [ l ] − d p [ l + 1 ] [ r ] , n u m s [ r ] − d p [ l ] [ r − 1 ] ) dp[l][r] = max(nums[l]-dp[l+1][r],nums[r]-dp[l][r-1]) dp[l][r]=max(nums[l]dp[l+1][r],nums[r]dp[l][r1])

class Solution {
    public boolean PredictTheWinner(int[] nums) {
        int[][] dp = new int[nums.length][nums.length];
        for(int i=0;i<nums.length;i++){
            dp[i][i] = nums[i];
        }
        //区间动态规划
        for(int d = 2;d<=nums.length;d++){
            for(int l = 0;l+d-1<nums.length;l++){
                int r = l+d-1;
                dp[l][r] = Math.max(nums[l]-dp[l+1][r],nums[r]-dp[l][r-1]);
            }
        }
        return dp[0][nums.length-1]>=0;
    }
}

4键键盘

假设你有一个特殊的键盘包含下面的按键:

  • A:在屏幕上打印一个 'A'
  • Ctrl-A:选中整个屏幕。
  • Ctrl-C:复制选中区域到缓冲区。
  • Ctrl-V:将缓冲区内容输出到上次输入的结束位置,并显示在屏幕上。

现在,你可以 最多 按键 n 次(使用上述四种按键),返回屏幕上最多可以显示 'A' 的个数

示例 1:

输入: n = 3
输出: 3
解释: 
我们最多可以在屏幕上显示三个'A'通过如下顺序按键:
A, A, A

示例 2:

输入: n = 7
输出: 9
解释: 
我们最多可以在屏幕上显示九个'A'通过如下顺序按键:
A, A, A, Ctrl A, Ctrl C, Ctrl V, Ctrl V

提示:

  • 1 <= n <= 50
class Solution {
    public int maxA(int n) {
        int[] dp = new int[n+1];
        for(int i=1;i<=n;i++){
            dp[i] = dp[i-1]+1; // 打印来源1:输入'A'

            for(int j=1;j<i-2;j++){// 打印来源2:ctrl-v
                dp[i] = Math.max(dp[i],dp[j]+dp[j]*(i-j-2));
            }
        }
        return dp[n];
    }
}

图论

欧拉图

有一个需要密码才能打开的保险箱。密码是 n 位数, 密码的每一位都是范围 [0, k - 1] 中的一个数字。

保险箱有一种特殊的密码校验方法,你可以随意输入密码序列,保险箱会自动记住 最后 n 位输入 ,如果匹配,则能够打开保险箱。

例如,正确的密码是 “345” ,并且你输入的是 “012345” :
输入 0 之后,最后 3 位输入是 “0” ,不正确。
输入 1 之后,最后 3 位输入是 “01” ,不正确。
输入 2 之后,最后 3 位输入是 “012” ,不正确。
输入 3 之后,最后 3 位输入是 “123” ,不正确。
输入 4 之后,最后 3 位输入是 “234” ,不正确。
输入 5 之后,最后 3 位输入是 “345” ,正确,打开保险箱。
在只知道密码位数 n 和范围边界 k 的前提下,请你找出并返回确保在输入的 某个时刻 能够打开保险箱的任一 最短 密码序列 。

解题思路:类似于编译原理中的构造自动机,是一张有向欧拉图,dfs深度优先搜索遍历所有边

class Solution {
    Set<Integer> s;
    String ans;
    public String crackSafe(int n, int k) {
        s = new HashSet<>();
        ans = new String();
        int mod = 1;
        for(int i=0;i<n-1;i++){
            mod = mod*10;
        }
        dfs(0,k,mod);
        for(int i=0;i<n-1;i++){
            ans = ans+"0";
        }
        return ans;
    }
    public void dfs(int id,int k,int mod){
        for(int i=0;i<k;i++){
            int e = id*10+i;
            if(s.contains(e))continue;
            s.add(e);
            dfs(e%mod,k,mod);
            ans += i;
        }
        return;
    }
}

模拟

实现加减乘除运算

 /**
        定义运算符的优先级:(,),/,*,+,-
        原理:中缀表达式转后缀表达式
        数据结构:栈、数组
        栈的使用:1.符号栈——中缀转后缀
                  2.数组list——存储后缀表达式
                  3.数值栈——计算后缀表达式
     */
class Solution {
    public int calculate(String s) {
        //定义优先级
        Map<Character,Long> priority = new HashMap<>();
        priority.put(')',1l);
        priority.put('*',2l);
        priority.put('/',2l);
        priority.put('+',3l);
        priority.put('-',3l);
        priority.put('(',4l);
        //映射运算符
        Map<Character,Long> mp = new HashMap<>();
        Integer M = Integer.MAX_VALUE;
        mp.put('+',Long.valueOf(M.longValue()+1));//48
        mp.put('-',Long.valueOf(M.longValue()+2));//49
        mp.put('*',Long.valueOf(M.longValue()+3));//50
        mp.put('/',Long.valueOf(M.longValue()+4));//51
        //符号栈
        Stack<Character> st1 = new Stack<>();
        // 数值栈
        Stack<Long> st2 = new Stack<>();
        //后缀表达式列表
        List<Long> lt = new ArrayList<>();

        //中缀转后缀
        for(int i=0;i<s.length();){
            Character c = s.charAt(i);
            if(c>='0'&&c<='9'){
                Long num = 0l;
                while(i<s.length()&&s.charAt(i)>='0'&&s.charAt(i)<='9'){
                    num = num*10+(s.charAt(i)-'0');
                    i++;
                }
                lt.add(num);
                continue;
            }
            else if(c == '(')st1.add(c);
            else if(c == '+' || c == '-' || c == '*' || c == '/'){ //优先级不低于当前运算符就出栈去优先参与运算
                while(!st1.isEmpty()&&priority.get(st1.peek())<=priority.get(c)){
                    lt.add(mp.get(st1.peek()));
                    st1.pop();
                }
                st1.add(c);
            }
            else{
                while(st1.peek()!='('){
                    lt.add(mp.get(st1.peek()));
                    st1.pop();
                }
                st1.pop();
            }
            i++;
        }
        while(!st1.isEmpty())lt.add(mp.get(st1.pop()));
        System.out.println("转后缀完成");
        
        // 计算后缀表达式
        for(Long num:lt){
            if(num<=Integer.MAX_VALUE)st2.add(num);
            else{
                Long x = st2.pop();
                Long y = st2.pop();
                Long ans ;
                if(mp.get('+') == num)ans = y+x;
                else if(mp.get('-') == num)ans = y-x;
                else if(mp.get('*') == num)ans = y*x;
                else ans = y/x;
                st2.add(ans);
            }
        }
        return st2.pop().intValue();
    }
}

并查集

岛屿数量

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

class Solution {
    int[] f;
    public static int[][] nex = {{0,1},{0,-1},{1,0},{-1,0}};
    public List<Integer> numIslands2(int m, int n, int[][] positions) {
        f = new int[m*n+1];
        Set<Integer> set = new HashSet<>();
        //初始化并查集
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                int x = i*n+j;
                f[x] = x;
            }
        }
        List<Integer> ans = new ArrayList<>();
        int cnt = positions.length;
        int add = 0;
        for(int i=0;i<cnt;i++){
            int x = positions[i][0];
            int y = positions[i][1];
            int id = x*n+y;
            if(set.contains(id)){
                ans.add(add);
                continue;
            }
            int root = find(id);
            add = add+1; //先假设增加一个岛屿
            //接下来进行合并岛屿
            for(int k=0;k<4;k++){
                int tx = x+nex[k][0];
                int ty = y+nex[k][1];
                int tid = tx*n+ty;
                if(tx<0||tx>=m||ty<0||ty>=n||set.contains(tid)==false)continue;
                int root_tid = find(tid); //判断是否属于同一个岛屿
                if(root_tid==root)continue;
                f[root_tid] = root; //合并岛屿
                add--;  
            }
            ans.add(add);
            set.add(id);
        }
        return ans;
    }
    //带有路径压缩的并查集
    public int find(int x){
        return x==f[x]?x:(f[x] = find(f[x]));
    }
}

滑动窗口

长度为 K 的无重复字符子串

给你一个字符串 S,找出所有长度为 K 且不含重复字符的子串,请你返回全部满足要求的子串的 数目

class Solution {
    public int numKLenSubstrNoRepeats(String s, int k) {
        if(k>s.length())return 0;
        int kind = 0;	//记录不同种类个数
        int ans = 0;
        int[] cnt = new int[26];	//记录出现次数
        for(int i=0;i<26;i++)cnt[i] = 0;
        for(int i=0;i<k;i++){
            if(++cnt[s.charAt(i)-'a'] == 1)kind++;
        }
        if(kind == k)ans = 1;
        for(int i=k;i<s.length();i++){
            if(++cnt[s.charAt(i)-'a'] == 1)kind++;
            if(--cnt[s.charAt(i-k)-'a'] == 0)kind--;
            if(kind == k)ans++;
        }
        return ans;
    }
}

二叉搜索树

给出两棵二叉搜索树的根节点 root1root2 ,请你从两棵树中各找出一个节点,使得这两个节点的值之和等于目标值 Target

如果可以找到返回 True,否则返回 False

class Solution {
    List<Long> tree[];
    public boolean twoSumBSTs(TreeNode root1, TreeNode root2, int target) {
        if(root1==null)return false;
        
        return dfs(root2,target-root1.val)||
        twoSumBSTs(root1.left,root2,target)||
        twoSumBSTs(root1.right,root2,target);
        
    }
    // BTS 本身是在树上进行搜索,二叉搜索也是在树逻辑上搜索
    public boolean dfs(TreeNode root,int target){
        if(root==null)return false;
        if(root.val==target)return true;
        if(root.val>target) return dfs(root.left,target);
        else return dfs(root.right,target);
    }
}

思维

合法分组的最少组数(阿里周赛)

给你一个长度为 n 下标从 0 开始的整数数组 nums

我们想将下标进行分组,使得 [0, n - 1] 内所有下标 i恰好 被分到其中一组。

如果以下条件成立,我们说这个分组方案是合法的:

  • 对于每个组 g ,同一组内所有下标在 nums 中对应的数值都相等。
  • 对于任意两个组 g1g2 ,两个组中 下标数量差值不超过 1

请你返回一个整数,表示得到一个合法分组方案的 最少 组数。

示例 1:

输入:nums = [3,2,3,2,3]
输出:2
解释:一个得到 2 个分组的方案如下,中括号内的数字都是下标:
组 1 -> [0,2,4]
组 2 -> [1,3]
所有下标都只属于一个组。
组 1 中,nums[0] == nums[2] == nums[4] ,所有下标对应的数值都相等。
组 2 中,nums[1] == nums[3] ,所有下标对应的数值都相等。
组 1 中下标数目为 3 ,组 2 中下标数目为 2 。
两者之差不超过 1 。
无法得到一个小于 2 组的答案,因为如果只有 1 组,组内所有下标对应的数值都要相等。
所以答案为 2 。

示例 2:

输入:nums = [10,10,10,3,1,1]
输出:4
解释:一个得到 2 个分组的方案如下,中括号内的数字都是下标:
组 1 -> [0]
组 2 -> [1,2]
组 3 -> [3]
组 4 -> [4,5]
分组方案满足题目要求的两个条件。
无法得到一个小于 4 组的答案。
所以答案为 4 。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 109

思路:按照最小次数进行分组,加入最小次数为r,对此数分别进行拆分,依次拆分为1,2,3…r组进行讨论

class Solution {
    Map<Integer,Integer> mp;
    public int minGroupsForValidAssignment(int[] nums) {
        mp = new HashMap<>();
        for(int num:nums){
            int cnt = mp.getOrDefault(num,0)+1;
            mp.put(num,cnt);
        }
        Set<Integer> keys = mp.keySet();
        int l = 1,r = Integer.MAX_VALUE;
        for(int key:keys){
            int cnt = mp.get(key);
            System.out.println(key+" "+cnt);
            r = Math.min(r,cnt);
        }
        int ans = Integer.MAX_VALUE;
        for(int i=1;i<=r;i++){
            int d = r/i;
            int mod = r%i;
            System.out.println(d);
            int d = r/i;
            int mod = r%i;
            int ret = b_search(d);
            if(ret!=-1)return ret;
            if(mod!=0)continue; // 如果没有绝对平均的分到每个组里,则不能判断d-1
            ret = b_search(d-1);
            if(ret!=-1)return ret;
        }
        return -1;
    }
    // 判断每组至少为d时是否可以拆分,并返回组数
    public int b_search(int d){
        int sum = 0;
        for(int num:mp.keySet()){
            int cnt = mp.get(num);
            int k = cnt/(d+1);
            int mod = cnt%(d+1);
            if(mod!=0&&k+mod<d)return -1;
            sum = sum+k+(mod==0?0:1);
        }
        return sum;
    }
}

缺失的第一个正数

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

示例 1:

输入:nums = [1,2,0]
输出:3

示例 2:

输入:nums = [3,4,-1,1]
输出:2

示例 3:

输入:nums = [7,8,9,11,12]
输出:1

提示:

  • 1 <= nums.length <= 5 * 105
  • -231 <= nums[i] <= 231 - 1
/**
首先推断出缺失的第一个正数一定在[1,nums.length+1]的范围内,借助桶排序的思想来交换数组内的
*/
class Solution {
    public int firstMissingPositive(int[] nums) {
        for(int i=0;i<nums.length;i++){
            if(nums[i]<=0||nums[i]>nums.length)continue;
            if(nums[nums[i]-1]==nums[i])continue;
            // 借助桶排序的思想
            while(nums[nums[i]-1]!=nums[i]){
                int id = nums[i]-1;
                int t = nums[id];
                nums[id] = nums[i];
                nums[i] = t;
                // 如果是把当前值交换到当前位置前面了,则不需要再交换
                // 因为前面交换过来的值已经做过交换判断
                if(id<=i)break; 
                if(nums[i]<=0||nums[i]>nums.length)break;
            }
        }
        for(int i=0;i<nums.length;i++){
            if(nums[i] != i+1)return i+1;
        }
        return nums.length+1;
    }
}

N 字形变换

将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。

比如输入字符串为 "PAYPALISHIRING" 行数为 3 时,排列如下:

P   A   H   N
A P L S I I G
Y   I   R

之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"

请你实现这个将字符串进行指定行数变换的函数:

string convert(string s, int numRows);
class Solution {
    public String convert(String s, int numRows) {
        if(s.length()<=numRows||numRows==1)return s;
        int d = 2*numRows-2; // 周期
        StringBuilder sb = new StringBuilder();
        char[] arr = s.toCharArray();
        int first = 0;
        while(first<numRows){
            int nex = d-first;
            for(int i=first;i<arr.length;i+=d,nex+=d){
                sb.append(arr[i]);
                if(nex>=arr.length)break;
                //核心:判断斜线上元素是否和竖线上两端点元素重合
                if(nex == i||nex == i+d)continue; 
                sb.append(arr[nex]); // 不重合的情况
            }
            first++;
        }
        return sb.toString();
    }
}

三数之和(字节)

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

**注意:**答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

提示:

  • 3 <= nums.length <= 3000
  • -105 <= nums[i] <= 105
/**
标签:数组遍历
首先对数组进行排序,排序后固定一个数 nums[i]nums[i]nums[i],再使用左右指针指向 nums[i]nums[i]nums[i]后面的两端,数字分别为 nums[L]nums[L]nums[L] 和 nums[R]nums[R]nums[R],计算三个数的和 sumsumsum 判断是否满足为 0,满足则添加进结果集
如果 nums[i]nums[i]nums[i]大于 0,则三数之和必然无法等于 0,结束循环
如果 nums[i]nums[i]nums[i] == nums[i−1]nums[i-1]nums[i−1],则说明该数字重复,会导致结果重复,所以应该跳过
当 sumsumsum == 0 时,nums[L]nums[L]nums[L] == nums[L+1]nums[L+1]nums[L+1] 则会导致结果重复,应该跳过,L++L++L++
当 sumsumsum == 00 时,nums[R]nums[R]nums[R] == nums[R−1]nums[R-1]nums[R−1] 则会导致结果重复,应该跳过,R−−R--R−−
*/
class Solution {
    public static List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> ans = new ArrayList();
        int len = nums.length;
        if(nums == null || len < 3) return ans;
        Arrays.sort(nums); // 排序
        for (int i = 0; i < len ; i++) {
            if(nums[i] > 0) break; // 如果当前数字大于0,则三数之和一定大于0,所以结束循环
            if(i > 0 && nums[i] == nums[i-1]) continue; // 去重
            int L = i+1;
            int R = len-1;
            while(L < R){
                int sum = nums[i] + nums[L] + nums[R];
                if(sum == 0){
                    ans.add(Arrays.asList(nums[i],nums[L],nums[R]));
                    while (L<R && nums[L] == nums[L+1]) L++; // 去重
                    while (L<R && nums[R] == nums[R-1]) R--; // 去重
                    L++;
                    R--;
                }
                else if (sum < 0) L++;
                else if (sum > 0) R--;
            }
        }        
        return ans;
    }
}

判断是否能拆分数组(37周周赛)

给你一个长度为 n 的数组 nums 和一个整数 m 。请你判断能否执行一系列操作,将数组拆分成 n非空 数组。

在每一步操作中,你可以选择一个 长度至少为 2 的现有数组(之前步骤的结果) 并将其拆分成 2 个子数组,而得到的 每个 子数组,至少 需要满足以下条件之一:

  • 子数组的长度为 1 ,或者
  • 子数组元素之和 大于或等于 m

如果你可以将给定数组拆分成 n 个满足要求的数组,返回 true ;否则,返回 false

**注意:**子数组是数组中的一个连续非空元素序列。

示例 :

输入:nums = [2, 3, 3, 2, 3], m = 6
输出:true
解释:
第 1 步,将数组 nums 拆分成 [2, 3, 3, 2] 和 [3] 。
第 2 步,将数组 [2, 3, 3, 2] 拆分成 [2, 3, 3] 和 [2] 。
第 3 步,将数组 [2, 3, 3] 拆分成 [2] 和 [3, 3] 。
第 4 步,将数组 [3, 3] 拆分成 [3] 和 [3] 。
因此,答案为 true 。 

题解:

我的做法是用了很麻烦的搜索的方法,其实仔细想想,至少有一对相邻的值(nums[i],nums[i+1])的和大于等于m的条件下,采用一下策略则必能拆分成功

1.从左端或者右端分离出一个值作为拆分的一部分,另一部分则是包含(nums[i],nums[i+1])的序列

2.循环步骤1

证明:如果所有相邻值的和小于m,拆分到最后总会出现由一个双值序列拆分成两个单值序列的情况,那么以上双值序列的和小于m,拆分失败,所以至少有一对相邻的值(nums[i],nums[i+1])的和大于等于m的条件下,拆分比成功

class Solution {
    public boolean canSplitArray(List<Integer> nums, int m) {
        if(nums.size()<=2)return true;
        for(int i=1;i<nums.size();i++){
            if(nums.get(i-1)+nums.get(i)>=m)return true;
        }
        return false;
    }
}

子序列最大优雅度–困难(37周赛)

给你一个长度为 n 的二维整数数组 items 和一个整数 k

items[i] = [profiti, categoryi],其中 profiticategoryi 分别表示第 i 个项目的利润和类别。

现定义 items子序列优雅度 可以用 total_profit + distinct_categories2 计算,其中 total_profit 是子序列中所有项目的利润总和,distinct_categories 是所选子序列所含的所有类别中不同类别的数量。

你的任务是从 items 所有长度为 k 的子序列中,找出 最大优雅度

用整数形式表示并返回 items 中所有长度恰好为 k 的子序列的最大优雅度。

**注意:**数组的子序列是经由原数组删除一些元素(可能不删除)而产生的新数组,且删除不改变其余元素相对顺序。

提示:

  • 1 <= items.length == n <= 105
  • items[i].length == 2
  • items[i][0] == profiti
  • items[i][1] == categoryi
  • 1 <= profiti <= 109
  • 1 <= categoryi <= n
  • 1 <= k <= n

题解:

堆+哈希:将items按利润降序排序,然后将前k加入选择集合,然后枚举剩余的项目items[i]

  • items的项目类别在选择集合中已有,则直接跳过该项目
  • 若**items[i]**的项目类别没有在选择集合中
    • 若当前选择集合中存在出现次数大于1的项目,将其中利润最小的项目移出集合,同时将**items[i]**加入集合
    • 若当前选择集合中不存在出现次数大于1的项目,结束枚举
class Solution {
    class Item implements Comparable<Item>{
        int profit;
        int category;
        Item(int p,int c){
            profit = p;
            category = c;
        }   
        // 从大到小排序:-(this.profit-t.profit)
        public int compareTo(Item t){
            return -(this.profit-t.profit);
        }
    }
    public long findMaximumElegance(int[][] items, int k) {
        List<Item> L = new ArrayList<>();
        Stack<Item> st = new Stack<>(); //stack只存放重复类别中除最大利润项目之外的其他项目
        int n = items.length;
        int[] cnt = new int[n+1];
        for(int i=0;i<n;i++){
            L.add(new Item(items[i][0],items[i][1]));
            cnt[i] = 0;
        }
        Collections.sort(L);
        Long ans = 0l;
        Long p_sum = 0l;
        Long c_sum = 0l; 
        for(int i=0;i<k;i++){
            int category = L.get(i).category;
            p_sum = p_sum+L.get(i).profit;
            if(++cnt[category]==1)c_sum++;
            else st.add(L.get(i));
            ans = p_sum+c_sum*c_sum;
        }
        for(int i=k;i<n;i++){
            int category = L.get(i).category;
            if(++cnt[category] != 1)continue;
            if(st.isEmpty())break;
            //栈顶元素就是重复类别中最小的利润的项目
            Item item = st.pop();
            cnt[item.category]--;
            p_sum -= item.profit;
            p_sum += L.get(i).profit;
            c_sum ++;
            ans = Math.max(ans,p_sum+c_sum*c_sum);
        }
        return ans;
    }
}

倍增

二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

示例 1:

img

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。

示例 2:

img

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。

示例 3:

输入:root = [1,2], p = 1, q = 2
输出:1

提示:

  • 树中节点数目在范围 [2, 105] 内。
  • -109 <= Node.val <= 109
  • 所有 Node.val 互不相同
  • p != q
  • pq 均存在于给定的二叉树中。
算法1:倍增法

时间复杂度:预处理O(nlogn)+查询O(logn)=O(nlogn)使用于频繁查询的情况下

class Solution {
    // 定义倍增数组dp的元素类型:节点以及节点对应的哈希值
    class Result{
        TreeNode node = null;
        Integer id;
        public Result(TreeNode nod,Integer i){
            node = nod;
            id = i;
        }
    }
    int siz = 0;
    Map<Integer,Integer> mp = new HashMap<>();
    Result[][] dp = new Result[100005][31];
    int[] dep = new int[100005];
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        dfs(root,null);
        return lca(p,q);
    }
    // 回溯进行初始化dp
    public void dfs(TreeNode root,TreeNode fa){
        if(root==null)return ;
        mp.put(root.val,siz);
        int id = siz++;
        int fa_id = fa==null?-1:mp.get(fa.val);
        dp[id][0] = new Result(fa,fa_id);
        if(fa==null)
            dep[id] = 1;
        else dep[id] = dep[mp.get(fa.val)]+1;
        for(int i=1;i<31&&(dp[id][i-1]!=null&&dp[id][i-1].id!=-1);i++){
            dp[id][i] = dp[dp[id][i-1].id][i-1];
        }
        dfs(root.left,root);
        dfs(root.right,root);
    }
    // 倍增LCA算法
    public TreeNode lca(TreeNode p,TreeNode q){
        TreeNode l = p;
        TreeNode r = q;
        if(dep[mp.get(l.val)]<dep[mp.get(r.val)]){
            TreeNode t = l;
            l = r;
            r = t;
        }
        int delt_h = dep[mp.get(l.val)]-dep[mp.get(r.val)];
        for(int j = 0;delt_h>0;delt_h>>=1,j++){
            if(delt_h%2==1) l = dp[mp.get(l.val)][j].node;
        }
        // 以上部分是对其p,q节点
        if(l==r)return l;
        // 以下部分进行倍增寻找最远的非公共祖先的两个节点
        for(int j=30;j>=0&&l!=r;j--){
            if(dp[mp.get(l.val)][j]==null||
            dp[mp.get(l.val)][j].id==dp[mp.get(r.val)][j].id)continue;
            l = dp[mp.get(l.val)][j].node;
            r = dp[mp.get(r.val)][j].node;
        }
        return dp[mp.get(l.val)][0].node;
    }
}
算法2:回溯法

时间复杂度:O(n)使用于少量查询的情况

class Solution {
    TreeNode ans;
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null)return null;
        if(root==p||root==q)return root;
        TreeNode l = lowestCommonAncestor(root.left,p,q);
        TreeNode r = lowestCommonAncestor(root.right,p,q);
        if(l!=null&&r!=null)return root; // 左右都找到了源节点,说明此根是p,q的最近公共祖先,返回
        // 只找到一个源节点或者是两个源节点的最近公共祖先节点,那么返回这个节点
        if(l!=null)return l;
        if(r!=null)return r;
        return null;
    }
}

倍增详解:https://leetcode.cn/problems/kth-ancestor-of-a-tree-node/solutions/2305895/mo-ban-jiang-jie-shu-shang-bei-zeng-suan-v3rw/

在传球游戏中最大化函数值

给你一个长度为 n 下标从 0 开始的整数数组 receiver 和一个整数 k

总共有 n 名玩家,玩家 编号 互不相同,且为 [0, n - 1] 中的整数。这些玩家玩一个传球游戏,receiver[i] 表示编号为 i 的玩家会传球给编号为 receiver[i] 的玩家。玩家可以传球给自己,也就是说 receiver[i] 可能等于 i

你需要从 n 名玩家中选择一名玩家作为游戏开始时唯一手中有球的玩家,球会被传 恰好 k 次。

如果选择编号为 x 的玩家作为开始玩家,定义函数 f(x) 表示从编号为 x 的玩家开始,k 次传球内所有接触过球玩家的编号之 ,如果有玩家多次触球,则 累加多次 。换句话说, f(x) = x + receiver[x] + receiver[receiver[x]] + ... + receiver(k)[x]

你的任务时选择开始玩家 x ,目的是 最大化 f(x)

请你返回函数的 最大值

注意:receiver 可能含有重复元素。

示例 1:

传递次数传球者编号接球者编号x + 所有接球者编号
2
1213
2103
3025
4216
输入:receiver = [2,0,1], k = 4
输出:6
解释:上表展示了从编号为 x = 2 开始的游戏过程。
从表中可知,f(2) 等于 6 。
6 是能得到最大的函数值。
所以输出为 6 。

提示:

  • 1 <= receiver.length == n <= 105
  • 0 <= receiver[i] <= n - 1
  • 1 <= k <= 1010
class Solution {
    
    public long getMaxFunctionValue(List<Integer> receiver, long k) {
    int n = receiver.size();
    // 祖先倍增 以及 求和倍增
    int[][] pa = new int[n+1][40];
    long[][] sum = new long[n+1][40];
    for(int i=0;i<n;i++){
        pa[i][0] = receiver.get(i);
        sum[i][0] = receiver.get(i);
    }    
    int m = 64 - Long.numberOfLeadingZeros(k);
    for(int i=0;i<m;i++){
        for(int j = 0;j<n;j++){
            pa[j][i+1] = pa[pa[j][i]][i];
            sum[j][i+1] = sum[j][i] + sum[pa[j][i]][i];
        }
    }
    long ans = 0;
    for(int i=0;i<n;i++){
        int x = i;
        long s = x;
        for(int j = 0;(k>>j)>0;j++){
            if((k>>j)%2 == 1){
                s  = s+sum[x][j];
                x = pa[x][j];
            }
        }
        ans = Math.max(ans,s);
    }
    return ans;
    }
}

差分数组

原理:

对于数组 a,定义其差分数组(difference array)为
d [ i ] = { a [ 0 ] , i = 0 a [ i ] − a [ i − 1 ] , i ≥ 1 d[i] = \begin{cases} a[0], &i = 0\\ a[i] - a[i-1], &i \geq 1 \end{cases} d[i]={a[0],a[i]a[i1],i=0i1
性质 1:从左到右累加 d 中的元素,可以得到数组 a*。

性质 2:如下两个操作是等价的。

  • **区间操作:**把a的子数组a[i],a[i+1],…,a[j]都加上x。
  • **单点操作:**把d[i]增加α,把dj+1]减少α。特别地,如果j+1=n,则只需把d[i]增加:α。(n为数组α的长度)
// 你有一个长为 n 的数组 a,一开始所有元素均为 0。
// 给定一些区间操作,其中 queries[i] = [left, right, x],
// 你需要把子数组 a[left], a[left+1], ... a[right] 都加上 x。
// 返回所有操作执行完后的数组 a。
int[] solve(int n, int[][] queries) {
    int[] diff = new int[n]; // 差分数组
    for (int[] q : queries) {
        int left = q[0], right = q[1], x = q[2];
        diff[left] += x;
        if (right + 1 < n) {
            diff[right + 1] -= x;
        }
    }
    for (int i = 1; i < n; i++) {
        diff[i] += diff[i - 1]; // 直接在差分数组上复原数组 a
    }
    return diff;
}

与车相交的点

给你一个下标从 0 开始的二维整数数组 nums 表示汽车停放在数轴上的坐标。对于任意下标 inums[i] = [starti, endi] ,其中 starti 是第 i 辆车的起点,endi 是第 i 辆车的终点。

返回数轴上被车 任意部分 覆盖的整数点的数目。

示例 1:

输入:nums = [[3,6],[1,5],[4,7]]
输出:7
解释:从 1 到 7 的所有点都至少与一辆车相交,因此答案为 7 。

示例 2:

输入:nums = [[1,3],[5,8]]
输出:7
解释:1、2、3、5、6、7、8 共计 7 个点满足至少与一辆车相交,因此答案为 7 。

提示:

  • 1 <= nums.length <= 100
  • nums[i].length == 2
  • 1 <= starti <= endi <= 100
class Solution {
    public int numberOfPoints(List<List<Integer>> nums) {
        int[] d = new int[105];
        for(int i=0;i<nums.size();i++){
            d[nums.get(i).get(0)] += 1;
            d[nums.get(i).get(1)+1] -= 1;
        }
        int ans = 0;
        int pre = 0;
        for(int i=1;i<=100;i++){
            pre += d[i];
            if(pre>0)ans++;
        }
        return ans;
    }
}

线段树

单点修改问题

给你一个数组 nums ,请你完成两类查询。

  1. 其中一类查询要求 更新 数组 nums 下标对应的值
  2. 另一类查询要求返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 ,其中 left <= right

实现 NumArray 类:

  • NumArray(int[] nums) 用整数数组 nums 初始化对象
  • void update(int index, int val)nums[index] 的值 更新val
  • int sumRange(int left, int right) 返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 (即,nums[left] + nums[left + 1], ..., nums[right]
class NumArray {

    private int[] sum;
    private int[] arrs;
    private int len;

    public NumArray(int[] nums) {
        arrs = nums.clone();
        len = arrs.length;
        sum = new int[len*4+10];
        build(1,0,len-1);
    }
    
    public void update(int index, int val) {
        update_arr(1,0,len-1,index,val);
    }
    
    public int sumRange(int left, int right) {
        return getSum(1,0,len-1,left,right);
    }
	// 建树
    private void build(int id,int l,int r){
        if(l==r){
            sum[id] = arrs[l];
            return ;
        }

        int m = l+((r-l)>>1);
        
        build(id*2,l,m);
        build(id*2+1,m+1,r);
        sum[id] = sum[id*2]+sum[id*2+1];
    }
    // 单点修改-无懒惰标记
    private void update_arr(int id,int l,int r,int pos,int val){
        if(l==r){
            sum[id] = val;
            return;
        }
        int m = l+((r-l)>>1);
        if(pos<=m)update_arr(id*2,l,m,pos,val);
        else update_arr(id*2+1,m+1,r,pos,val);
        sum[id] = sum[id*2]+sum[id*2+1];
    }
    // 区间求和
    private int getSum(int id,int l,int r,int L,int R){
        if(L<=l&&R>=r){
            return sum[id];
        }
        int m = l+((r-l)>>1);
        int s = 0;
        if(L<=m){
            s+=getSum(id*2,l,m,L,R);
        }
        if(R>m){
            s+=getSum(id*2+1,m+1,r,L,R);
        }
        return s;
    }
}

树状数组

计算右侧小于当前元素的个数

给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。

示例 1:

输入:nums = [5,2,6,1]
输出:[2,1,1,0] 
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104
class Solution {
    private final int MAXN = (int)(2e+4)+5;
    private int[] c;
    public List<Integer> countSmaller(int[] nums) {
        c = new int[MAXN];
        List<Integer> ans = new ArrayList<>();
        // 逆序遍历
        for(int i = nums.length-1;i>=0;i--){
            int pos = nums[i]+(int)((1e+4)+1);
            add(pos);
            ans.add(getSum(pos-1));
        }
        Collections.reverse(ans);
        return ans;
    }
    public int lowbit(int x){
        return x&(-x);
    }
    public void add(int k){
        while(k<MAXN){
            c[k] += 1;
            k += lowbit(k); // 寻找父节点去更新
        }
    }
    public int getSum(int k){
        int ans = 0;
        while(k>0){
            ans += c[k];
            k -= lowbit(k);
        }
        return ans;
    }
}

字典树

实现Tire

Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false
class Trie {

    private final int MAXN = (int)(1e+5);
    private int[][] nex;
    private boolean[] flag;
    private int cnt;
    public Trie() {
        nex = new int[MAXN][26];
        flag = new boolean[MAXN];
        cnt = 0;
    }
    // 插入字典树
    public void insert(String word) {
        int p = 0;
        char[] arr = word.toCharArray();
        for(int i=0;i<arr.length;i++){
            int num = arr[i]-'a';
            if(nex[p][num]==0){
                nex[p][num] = ++cnt;
            }
            p = nex[p][num];
        }
        flag[p] = true;
    }
    // 查询字典树
    public boolean search(String word) {
        int p = 0;
        char[] arr = word.toCharArray();
        for(int i=0;i<arr.length;i++){
            int num = arr[i]-'a';
            if(nex[p][num]==0){
                return false;
            }
            p = nex[p][num];
        }
        if(flag[p])return true;
        return false;
    }
    
    public boolean startsWith(String prefix) {
        int p = 0;
        char[] arr = prefix.toCharArray();
        for(int i=0;i<arr.length;i++){
            int num = arr[i]-'a';
            if(nex[p][num]==0){
                return false;
            }
            p = nex[p][num];
        }
        return true;
    }
}

前K个高频单词

给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。

返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。

示例 1:

输入: words = ["i", "love", "leetcode", "i", "love", "coding"], k = 2
输出: ["i", "love"]
解析: "i" 和 "love" 为出现次数最多的两个单词,均为2次。
    注意,按字母顺序 "i" 在 "love" 之前。

示例 2:

输入: ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4
输出: ["the", "is", "sunny", "day"]
解析: "the", "is", "sunny" 和 "day" 是出现次数最多的四个单词,
    出现次数依次为 4, 3, 2 和 1 次。

注意:

  • 1 <= words.length <= 500
  • 1 <= words[i] <= 10
  • words[i] 由小写英文字母组成。
  • k 的取值范围是 [1, **不同** words[i] 的数量]
class Solution {
    class Node{
        int[] nex;
        int cnt;
        public Node(){
            nex = new int[26];
            Arrays.fill(nex,-1);
            cnt = 0;
        }
    }
    final int MAXN = 5050;
    Node[] tire = new Node[MAXN];
    int len = 0;
    PriorityQueue<String> pq ;
    List<String> ans = new ArrayList<>();
    public List<String> topKFrequent(String[] words, int k) {
        tire[0] = new Node();
        // 建字典树
        for(int i=0;i<words.length;i++){
            insert(words[i]);
        }
        // 定义优先队列
        pq = new PriorityQueue<>(new Comparator<String>(){
            public int compare(String a,String b){
                int cnt_a = query(a);
                int cnt_b = query(b);
                if(cnt_a==cnt_b)return b.compareTo(a);
                return cnt_a-cnt_b;
            }
        });
        // 最差是nlogk
        for(String s:ans){
            if(pq.size()<k)pq.add(s);
            else {
                String fir = pq.peek();
                int cnt1 = query(s);
                int cnt2 = query(fir);
                if(cnt1>cnt2||cnt1==cnt2&&s.compareTo(fir)<0){
                    pq.poll();
                    pq.add(s); 
                }
            }
        }
        // 此时优先队列pq中保存的就是所有解
        ans.clear();
        while(!pq.isEmpty()){
            ans.add(pq.poll());
        }
        Collections.reverse(ans);
        return ans;
    }
    // 插入字典树
    public void insert(String s){
        int p = 0;
        for(int i=0;i<s.length();i++){
            char c = s.charAt(i);
            if(tire[p].nex[c-'a']!=-1){
                p = tire[p].nex[c-'a'];
            }
            else{
                tire[++len] = new Node();
                tire[p].nex[c-'a'] = len;
                p = len;
            }
        }
        if(tire[p].cnt==0)ans.add(s);
        tire[p].cnt++;
    }
    // 查询字典树
    public int query(String s){
        int p = 0;
        for(int i=0;i<s.length();i++){
            char c = s.charAt(i);
            p = tire[p].nex[c-'a'];
        }
        return tire[p].cnt;
    }
    
}

链表

寻找重复数

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,返回 这个重复的数

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

示例 1:

输入:nums = [1,3,4,2,2]
输出:2

示例 2:

输入:nums = [3,1,3,4,2]
输出:3

提示:

  • 1 <= n <= 105
  • nums.length == n + 1
  • 1 <= nums[i] <= n
  • nums只有一个整数 出现 两次或多次 ,其余整数均只出现 一次

Floyd判圈法:

我们先设置慢指针 slow和快指针 fast ,慢指针每次走一步,快指针每次走两步,根据「Floyd 判圈算法」两个指针在有环的情况下一定会相遇,此时我们再将 slow 放置起点 0,两个指针每次同时移动一步,相遇的点就是答案。

image-20231025184333236
class Solution {
    public int findDuplicate(int[] nums) {
        int fast = 0,low = 0;
        do{
            low = nums[low];
            fast = nums[nums[fast]];
        }while(low!=fast);
        low = 0;
        while(low!=fast){
            low = nums[low];
            fast = nums[fast];
        }
        return low;
    }
}

K个一组反转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1:

输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode Head = new ListNode(0);
        Head.next = head;
        ListNode l = Head;
        ListNode r = head;
        while(true){
            int cnt = k;
            ListNode p = l;
            while(cnt>0&&p!=null){
                p = p.next;
                if(p!=null)
                    r = p.next;
                cnt--;
            }
            if(p==null)break;// 剩余不够k个
            l = reverse(l,r);
        }
        return Head.next;
    }
    // 反转(l,r)区间的节点,开区间
    public ListNode reverse(ListNode l,ListNode r){
        ListNode ret = l.next;
        ListNode p = l.next;
        ListNode nex = p;
        while(nex!=r){
            nex = p.next;
            p.next = l.next;
            l.next = p;
            p = nex;
        }
        ret.next = r;
        return ret; // 返回下一个要逆转区间的头结点的前一个节点
    }
}

前 K 个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

输入: nums = [1], k = 1
输出: [1]

提示:

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

**进阶:**你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

//HashMap复杂度为O(1)
//PriorityQueue的使用
// 此题另一种解法是桶排序:实质就是一种思维
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer,Integer> mp = new HashMap<>();
        for(int num:nums){
            int cnt = mp.getOrDefault(num,0)+1;
            mp.put(num,cnt);
        }
        // 优先队列的声明加自定义排序规则
        PriorityQueue<Integer> pq = new PriorityQueue<>(
            new Comparator<Integer>(){
                public int compare(Integer a,Integer b){
                    return mp.get(a)-mp.get(b);
                }
            }
        );
        Set<Integer> s = mp.keySet();
        // 使用堆来维持前k个最大的出现频次对应的数
        // n*logk
        for(int num:s){
            if(pq.size()<k)
                pq.add(num);
            else{
                int first = pq.peek();
                if(mp.get(first)<mp.get(num)){
                    pq.remove();
                    pq.add(num);
                }
            }
        }
        int[] ans = new int[k];
        int id = 0;
        while(!pq.isEmpty()){
            ans[id++] = pq.peek();
            pq.remove();
        }
        return ans;
    }
}

排序

排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

示例 1:

img

输入:head = [4,2,1,3]
输出:[1,2,3,4]

示例 2:

img

输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目在范围 [0, 5 * 104]
  • -105 <= Node.val <= 105
class Solution {
    public ListNode sortList(ListNode head) {
        int cnt = getCnt(head);
        if(cnt<=1)return head;
        int mid = cnt/2;
        ListNode p = head;
        while(mid>1){
            p = p.next;
            mid--;
        }
        ListNode second = p.next;
        p.next = null; // 断开链表
        ListNode first = sortList(head);
        second = sortList(second);
        return merge(first,second);
    }
    public int getCnt(ListNode head){
        ListNode p = head;
        int sum = 0;
        while(p!=null){
            p = p.next;
            sum++;
        }
        return sum;
    }
    public ListNode merge(ListNode p1,ListNode p2){
        ListNode head = new ListNode(0);
        ListNode p = head;
        while(p1!=null&&p2!=null){
            if(p1.val<=p2.val){
                p.next = p1;
                p1 = p1.next;
            }
            else{
                p.next = p2;
                p2 = p2.next;
            }
            p = p.next;
        }
        if(p1!=null)p.next = p1;
        else if(p2!=null)p.next = p2;
        return head.next;
    }
}

算法尾部

java笔记

Map

image-20230725153217975

List

  • 实例化

List<String> ans = new ArrayList<>();

  • 初始化

    List<Integer> list = Arrays.asList(1,2,3,4); -- 不可变;

    List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3,4)); -- 可变;

  • 定义list数组

    //创建List数组
      List<Integer> lis[]=new ArrayList[n+1];
      //初始化list数组
      for (int i = 1; i < lis.length; i++) {
       lis[i]=new ArrayList<>();
      }
    
  • 遍历

    //使用for循环遍历
    for(int i = 0; i < list.size(); i++){
        String str = list.get(i);
        System.out.println(str);
    }
    //使用foreach循环遍历
    for(String str : list){
        System.out.println(str);
    }
    

    注意:在程序中函数返回数组类型比返回List类型快得多

数组

  • 初始化

    String[] arr = new String[]{"scmsa","snja"};

    String[] arr = {"scmsa","snja"};

    int[] arr = new int[]{345,453,46,328};

    int[] arr = {345,453,46,328};

Set

数据类型

转换

// Integer->long->Long
//Integer:32位 Long:64位
Integer a = 10;
lonng b = a.longValue();
Long c = Long.valueOf(b);

char[]->String

// char[]->String
char[] c = {'a','b','c','d'};
String s = String.valueOf(c);//传入的是char[]类型参数
// char -> String
char c = 'a';
String s = Character.toString(c);//传入的是char类型的参数

String

split

点分割split("\\.")

StringBuilder

//反转字符串
String str = "hello world";
StringBuilder sb = new StringBuilder(str);
String reversedStr = sb.reverse().toString();
System.out.println(reversedStr);

StringBuffer

//修改字符串的值
StringBuffer sb = new StringBuffer("hello world");
sb.replace(6,11,"Java");
System.out.println(sb.toString()); // 输出 hello Java

使用StringBuilder(或StringBuffer)的reverse方法,可以很容易地反转一个字符串。StringBuilder是线程不安全的,但执行速度快,适合单线程使用,而StringBuffer是线程安全的,适合多线程使用。

PriorityQueue()

构造方法说明
PriorityQueue()不带参数,默认容量为11
PriorityQueue(int initialCapacity)参数为初始容量,该初始容量不能小于1
PriorityQueue(Collection<? extends E> c)参数为一个集合
import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;
 
public class TestPriorityQueue {
    public static void main(String[] args) {
        PriorityQueue<Integer> p1 = new PriorityQueue<>(); //容量默认为11
        PriorityQueue<Integer> p2 = new PriorityQueue<>(10); //参数为初始容量
        List<Integer> list = new ArrayList<>();
        list.add(0);
        list.add(1);
        list.add(2);
        PriorityQueue<Integer> p3 = new PriorityQueue<>(list); //使用集合list作为参数构造优先 
                                                               // 级队列
    }
}
方法说明
boolean offer(E e)插入元素e,返回是否插入成功,e为null,会抛异常
E peek()获取堆(后面介绍堆)顶元素,如果队列为空,返回null
E poll()删除堆顶元素并返回,如果队列为空,返回null
int size()获取有效元素个数
void clear()清空队列
boolean isEmpty()判断队列是否为空
PriorityQueue<Integer> p = new PriorityQueue<>();
p.offer(1);
p.offer(2);
p.offer(3);
System.out.println(p.size());
p.offer(null);

自定义比较器(

对象比较的方法(3种)

1. equals方法比较

Object类是每一个类的基类,其提供了equals()方法来进行比较内容是否相同,但是Object中的equals方法默认是用==来比较的,也就是比较两个对象的地址 ,所以想让自定义类型可以比较,可以重写基类的equals()方法

class Student{
    String name;
    int age;
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
	//比较对象的内容,而不是地址值
    @Override
    public boolean equals(Object obj) {
        if(this == obj){
            return true;
        }
        if(obj==null || !(obj instanceof Student)){
            return false;
        }
        Student s = (Student) obj;
        return this.age==s.age && this.name.equals(s.name);
    }
}

2. 基于Comparable接口的比较

对于引用类型,如果想按照大小的方式进行比较,在定义类时实现Comparable接口,然后在类中重写compareTo方法

class Person implements Comparable<Person>{
    String name;
    int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    @Override
    public int compareTo(Person o) {
        if(o == null){
            return 1;
       	}
    return this.age-o.age;
	}
}

使用Comparable接口使得Student类型的对象可以插入到优先级队列中

3. 基于Comparator接口的比较

按照比较器的方式比较具体步骤如下:

  • 创建一个比较器类,实现Comparator接口
  • 重写compare方法

使用比较器使得Student类型的对象可以插入到优先级队列中

import java.util.Comparator;
import java.util.PriorityQueue;
 
class Student {
    String name;
    int age;
 
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
class StudentComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        if(o1 == o2){
            return 0;
        }
        if(o1 == null){
            return -1;
        }
        if(o2 == null){
            return 1;
        }
        return o1.age-o2.age;
    }
}
public class Test {
    public static void main(String[] args) {
        Student s1 = new Student("张三",25);
        Student s2 = new Student("李四",31);
        Student s3 = new Student("李四",35);
        PriorityQueue<Student> p = new PriorityQueue<>(new StudentComparator());
        p.offer(s1);
        p.offer(s2);
        p.offer(s3);
    }
}

4. 三种比较方式对比

重写的方法说明
Object.equals只能比较两个对象的内容是否相等,不能比较大小
Comparable.compareTo类要实现接口,对类的侵入性较强,破坏了原来类的结构
Comparator.compare需实现一个比较器类,对类的侵入性较弱,不破坏原来的类

java尾部

| 删除堆顶元素并返回,如果队列为空,返回null |
| int size() | 获取有效元素个数 |
| void clear() | 清空队列 |
| boolean isEmpty() | 判断队列是否为空 |

PriorityQueue<Integer> p = new PriorityQueue<>();
p.offer(1);
p.offer(2);
p.offer(3);
System.out.println(p.size());
p.offer(null);

自定义比较器(

对象比较的方法(3种)

1. equals方法比较

Object类是每一个类的基类,其提供了equals()方法来进行比较内容是否相同,但是Object中的equals方法默认是用==来比较的,也就是比较两个对象的地址 ,所以想让自定义类型可以比较,可以重写基类的equals()方法

class Student{
    String name;
    int age;
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
	//比较对象的内容,而不是地址值
    @Override
    public boolean equals(Object obj) {
        if(this == obj){
            return true;
        }
        if(obj==null || !(obj instanceof Student)){
            return false;
        }
        Student s = (Student) obj;
        return this.age==s.age && this.name.equals(s.name);
    }
}

2. 基于Comparable接口的比较

对于引用类型,如果想按照大小的方式进行比较,在定义类时实现Comparable接口,然后在类中重写compareTo方法

class Person implements Comparable<Person>{
    String name;
    int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    @Override
    public int compareTo(Person o) {
        if(o == null){
            return 1;
       	}
    return this.age-o.age;
	}
}

使用Comparable接口使得Student类型的对象可以插入到优先级队列中

3. 基于Comparator接口的比较

按照比较器的方式比较具体步骤如下:

  • 创建一个比较器类,实现Comparator接口
  • 重写compare方法

使用比较器使得Student类型的对象可以插入到优先级队列中

import java.util.Comparator;
import java.util.PriorityQueue;
 
class Student {
    String name;
    int age;
 
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
class StudentComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        if(o1 == o2){
            return 0;
        }
        if(o1 == null){
            return -1;
        }
        if(o2 == null){
            return 1;
        }
        return o1.age-o2.age;
    }
}
public class Test {
    public static void main(String[] args) {
        Student s1 = new Student("张三",25);
        Student s2 = new Student("李四",31);
        Student s3 = new Student("李四",35);
        PriorityQueue<Student> p = new PriorityQueue<>(new StudentComparator());
        p.offer(s1);
        p.offer(s2);
        p.offer(s3);
    }
}

4. 三种比较方式对比

重写的方法说明
Object.equals只能比较两个对象的内容是否相等,不能比较大小
Comparable.compareTo类要实现接口,对类的侵入性较强,破坏了原来类的结构
Comparator.compare需实现一个比较器类,对类的侵入性较弱,不破坏原来的类

java尾部

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值