@[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:
输入: 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:
输入: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
。
返回和为 target
的 nums
子序列中,子序列 长度的最大值 。如果不存在和为 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
个节点的树,标记为 0
到 n - 1
。给定数字 n
和一个有 n - 1
条无向边的 edges
列表(每一个边都是一对标签),其中 edges[i] = [ai, bi]
表示树中节点 ai
和 bi
之间存在一条无向边。
可选择树中任何一个节点作为根。当选择节点 x
作为根节点时,设结果树的高度为 h
。在所有可能的树中,具有最小高度的树(即,min(h)
)被称为 最小高度树 。
请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。
树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。
示例 1:
输入:n = 4, edges = [[1,0],[1,2],[1,3]]
输出:[1]
解释:如图所示,当根是标签为 1 的节点时,树的高度是 1 ,这是唯一的最小高度树。
示例 2:
输入: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的多状态进行转换
给你两个整数 k
和 n
,返回所有有效的涂色 方案数 。
示例 1:
输入: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)
给你正整数 low
,high
和 k
。
如果一个数满足以下两个条件,那么它是 美丽的 :
- 偶数数位的数目与奇数数位的数目相同。
- 这个整数可以被
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。由于优惠的存在,你可能做出一些冲动消费导致最终买游戏的总费用超过预算,但只要满足获得的总优惠金额不低于超过预算的总金额,那在心理上就不会觉得吃亏。现在你希望在心理上不觉得吃亏的前提下,获得尽可能多的快乐值。
大礼包(记忆化搜索)
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记录每一个区间的先手使差值最大化的取值策略,即:
-
选左边
-
选右边
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][r−1])
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;
}
}
树
二叉搜索树
给出两棵二叉搜索树的根节点 root1
和 root2
,请你从两棵树中各找出一个节点,使得这两个节点的值之和等于目标值 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
中对应的数值都相等。 - 对于任意两个组
g1
和g2
,两个组中 下标数量 的 差值不超过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 != j
、i != k
且 j != 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]
,其中 profiti
和 categoryi
分别表示第 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:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:
输入: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
p
和q
均存在于给定的二叉树中。
算法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 | |||
1 | 2 | 1 | 3 |
2 | 1 | 0 | 3 |
3 | 0 | 2 | 5 |
4 | 2 | 1 | 6 |
输入: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[i−1],i=0i≥1
性质 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
表示汽车停放在数轴上的坐标。对于任意下标 i
,nums[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
,请你完成两类查询。
- 其中一类查询要求 更新 数组
nums
下标对应的值 - 另一类查询要求返回数组
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]
范围内(包括 1
和 n
),可知至少存在一个重复的整数。
假设 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,两个指针每次同时移动一步,相遇的点就是答案。
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:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
示例 2:
输入: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
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 | 需实现一个比较器类,对类的侵入性较弱,不破坏原来的类 |