目录
DAY22
59-Ⅰ:滑动窗口的最大值
思路:单调队列。窗口的边界为 i 和 j ,注意左边界的起始位置和结果数组的长度。双端队列实现高效操作头尾元素,算法保证队列中元素始终按照从大到小排列,所以头部元素就是最大值。
- 在第一个滑动窗口形成之前只考虑将大的元素放入队列,无需考虑删除队列中窗口丢掉的元素
- 从第一个窗口形成时开始记录每滑动一次,窗口中的最大值
- 滑动的时候,如果左边要丢掉的数据是滑动前窗口中最大值,则要同时删除队列中的头元素。右边新扩充进来的元素需要和队列里的元素比较,循环删除队列中比新元素小的元素,此时新元素便是队列中最小的元素,将其添加至队尾即可(若没有就不用删除,直接添加到队尾)
- 因为队列是从大到小的排列顺序,所以将队列头元素添加至结果数组就是当前窗口的最大值
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 0 || k == 0) return new int[0];
Deque<Integer> deque = new LinkedList<>(); //双端队列,里面的数字从大到小排列
int[] res = new int[nums.length - k + 1];
for(int j = 0, i = 1 - k; j < nums.length; i++, j++){
//如果窗口滑动删除的元素是滑动前窗口里最大的元素,则要将队列里的一起删除
if(i > 0 && deque.peekFirst() == nums[i - 1])
deque.removeFirst();
//删除队列中比新扩充进来的元素小的
while(!deque.isEmpty() && deque.peekLast() < nums[j])
deque.removeLast();
deque.addLast(nums[j]);
if(i >= 0)
res[i] = deque.peekFirst();
}
return res;
}
}
59-Ⅱ:队列的最大值
思路:辅助队列。队列queue负责存放数据,deque负责存放最大值
入队:
- 将元素直接加入queue中
- 当deque不为空时,将双端队列中比新元素小的都移除,然后再将其添加至队尾。(若没有直接添加至队尾)。以保证双端队列里的元素从小到大排列
最大值
- deque不为空的时候返回头元素即可
移除元素
- 需要判断移除的是不是当前最大值,若queue中移除的元素与deque的头元素相同,那么要同时移除deque中的元素
- 若不是当前最大值直接移除返回即可
class MaxQueue {
Queue<Integer> queue;
Deque<Integer> deque; //保证从大到小
public MaxQueue() {
queue = new LinkedList<>();
deque = new LinkedList<>();
}
public int max_value() {
if(deque.isEmpty()) return -1;
return deque.peekFirst();
}
public void push_back(int value) {
while(!deque.isEmpty() && deque.peekLast() < value)
deque.pollLast(); //返回并移除
deque.offerLast(value); //添加成功返回true,否则false
queue.offer(value);
}
public int pop_front() {
if(queue.isEmpty()) return -1;
/* int rmv = queue.poll();
if(rmv == deque.peekFirst()) deque.pollFirst();
return rmv; 更快*/
if(queue.peek().equals(deque.peekFirst())) deque.pollFirst();
return queue.poll();
}
}
/**
* Your MaxQueue object will be instantiated and called as such:
* MaxQueue obj = new MaxQueue();
* int param_1 = obj.max_value();
* obj.push_back(value);
* int param_3 = obj.pop_front();
*/
DAY23
37:序列化二叉树(太难了,第一轮刷放过了。下一轮再看)
思路:BFS
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if(root == null) return "[]";
StringBuilder res = new StringBuilder("[");
Queue<TreeNode> queue = new LinkedList<>(){{add(root);}};
while(!queue.isEmpty()){
TreeNode node = queue.poll();
if(node != null){
res.append(node.val + ",");
queue.add(node.left);
queue.add(node.right);
}else res.append("null,");
}
//System.out.println(res.toString());
res.deleteCharAt(res.length() - 1);
res.append("]");
return res.toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if(data.equals("[]")) return null;
String[] vals = data.substring(1, data.length() - 1).split(",");
TreeNode root = new TreeNode(Integer.parseInt(vals[0])); //字符串第一个是root
Queue<TreeNode> queue = new LinkedList<>(){{add(root);}};
int i = 1;
while(!queue.isEmpty()){
TreeNode node = queue.poll();
if(!vals[i].equals("null")){
node.left = new TreeNode(Integer.parseInt(vals[i]));
queue.add(node.left);
}
i++;
if(!vals[i].equals("null")){
node.right = new TreeNode(Integer.parseInt(vals[i]));
queue.add(node.right);
}
i++;
}
return root;
}
}
// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.deserialize(codec.serialize(root));
38:字符串的排列(不行 太难了 回看)
思路:DFS。如下图,可看成一个二叉树。x为固定位。
- 将c[ i ]放入set中,遇到重复的跳过进行下一轮循环
- 交换c[ i ]和c[ x ],即固定c[ i ]为当前字符
- 递归,调用dfs(x + 1),即开始固定x +1 个字符(累加固定)
- 还原c[ i ] 和c [ x ]
class Solution {
List<String> res = new LinkedList<>();
char[] c;
public String[] permutation(String s) {
c = s.toCharArray();
dfs(0);
return res.toArray(new String[res.size()]);
}
void dfs(int x){
if(x == c.length - 1){
res.add(String.valueOf(c)); //将c转换成字符串添加到res里
return;
}
HashSet<Character> set = new HashSet<>();
for(int i = x; i < c.length; i++){
if(set.contains(c[i])) continue; //已经存在的 剪枝
set.add(c[i]);
swap(i, x);
dfs(x + 1);
swap(i, x);
}
}
void swap(int a, int b){
char temp = c[a];
c[a] = c[b];
c[b] = temp;
}
}
DAY24
19:正则表达式匹配(回看)
49:丑数(多领悟)
思路:动态规划。首先要知道丑数一定是通过前面的数乘以2、3或5得来的。但是如果给每个数都分别乘以2,3,5排下去就不一定是按顺序的了。
为什么乘以几就要让几对应的指针+1?因为p2表示:dp[p2]之前的数字都已经和2乘过了,下一个需要乘2的就是dp[p2](这个数只是还没和2相乘,有没有和3/5相乘不知道)。p3和p5同理表示。
下一个丑数一定是通过前面的丑数*2/3/5得来的,所以只需要挑出里面最小的作为下一个丑数即可。得到丑数后我们就需要判断这个丑数是dp[p几]*几。
当一个丑数可以通过多个dp[p]*p得到,那么可以得到该丑数的p应该同时+1.
class Solution {
public int nthUglyNumber(int n) {
int[] dp = new int[n + 1];
dp[1] = 1; //dp[0] = 0
int p2 = 1, p3 = 1, p5 = 1;
for(int i = 2; i <= n; i++){
int num2 = dp[p2] * 2, num3 = dp[p3] * 3, num5 = dp[p5] * 5;
dp[i] = Math.min(Math.min(num2, num3), num5);
if(dp[i] == num2){
p2++;
}
if(dp[i] == num3){
p3++;
}
if(dp[i] == num5){
p5++;
}
}
return dp[n];
}
}
60:n个骰子的点数
思路:动态规划。
只有1个骰子时,dp[1]是代表当骰子点数之和为2时的概率,它会对当有2个骰子时的点数之和为3、4、5、6、7、8产生影响,因为当有一个骰子的值为2时,另一个骰子的值可以为1~6,产生的点数之和相应的就是3~8;比如dp[2]代表点数之和为3,它会对有2个骰子时的点数之和为4、5、6、7、8、9产生影响;所以k在这里就是对应着第i个骰子出现时可能出现六种情况。
class Solution {
public double[] dicesProbability(int n) {
double[] dp = new double[6];
Arrays.fill(dp, 1.0 / 6.0); //当只有一个骰子时1~6概率都为1/6
for(int i = 2; i <= n; i++){
//每次的点数之和范围会有点变化,点数之和的值最大是i*6,最小是i*1,i之前的结果值是不会出现的;
//比如i=3个骰子时,最小就是3了,不可能是2和1,所以点数之和的值的个数是6*i-(i-1),化简:5*i+1
double[] sum = new double[5*i + 1];
for(int j = 0; j < dp.length; j++){
for(int k = 0; k <6; k++){
sum[j + k] += dp[j] / 6.0;
}
}
dp = sum;
}
return dp;
}
}
DAY25
17:打印从1到最大的n位数
打印数的公式是
思路一:普通解法
思路二:全排列(不如思路一快)
- 数组num存放的就是每个数字,所以num[0]的取值范围是1~9,首位不能为0
- dfs中的for循环就是给num中每一位一次添加数字,当遇到终止条件时就将此时num的字符串转化为成数字返回。
思路一:
class Solution {
public int[] printNumbers(int n) {
int[] res = new int[(int)Math.pow(10, n) - 1];
for(int i = 0; i < res.length; i++){
res[i] = i + 1;
}
return res;
}
}
思路二
class Solution {
int[] res;
int count = 0;
public int[] printNumbers(int n) {
res = new int[(int)Math.pow(10, n) - 1];
for(int i = 1; i <= n; i++){ //i是位数
for(char j = '1'; j <= '9'; j++){ //j是首位,首位不能为0
char[] num = new char[i];
num[0] = j;
dfs(1, num, i);
}
}
return res;
}
void dfs(int index, char[] num, int digit){
if(index == digit){
//当遍历到最后一位后,将数组中的字符转化为整数类型输出
res[count++] = Integer.parseInt(String.valueOf(num));
return;
}
for(char i = '0'; i <= '9'; i++){ //除了首位意外取值范围都是0~9
num[index] = i;
dfs(index + 1, num, digit);
}
}
}
51:
思路:分支思想。归并排序。分割到每组只剩下两个,然后小的在前大的在后,若交换了位置说明原本是逆序的,计数器+1。依次合并,排序换位置的次数为原本逆序的对数,计数。
递归合并算法:指针 i 在左部分遍历,j 在右部分遍历,每部分都是从小到大排列的。
- 当左部分的第 i 个数大于右部分第 j 个数,那么 左部分i 后面的数全部都大于第 j 个数,计数,并移动指针 i,将第 j 个数放入最终有序的大数组中 。
- 当左部分的第 i 个数小于右部分第 j 个数,则移动指针 i ,计数,并将第 i 个数放入最终有序大数组中,继续判断。
nums是原始数组,最后便利结束后变成了从小到大排列的顺序数组;temp是中间需要分割比较的数组。(暂存数组 nums闭区间 [i, r] 内的元素至辅助数组temp)
class Solution {
int[] nums, temp;
public int reversePairs(int[] nums) {
this.nums = nums;
temp = new int[nums.length];
return mergeSort(0, nums.length - 1);
}
int mergeSort(int left, int right){
if(left >= right) return 0; //终止条件
int m = (left + right) / 2; //从中间分割
int res = mergeSort(left, m) + mergeSort(m + 1, right); //递归划分
//合并
int i = left, j = m + 1;
for(int k = left; k <= right; k++)
temp[k] = nums[k];
for(int k = left; k <= right; k++){
if(i == m + 1)
//左子数组已合并完,因此添加右子数组当前元素 tmp[j],并执行j=j+1
nums[k] = temp[j++];
else if(j == right + 1 || temp[i] <= temp[j])
//右子数组已合并完,因此添加左子数组当前元素 tmp[i],并执行i=i+1
//temp[i] <= temp[j]的操作也是i+=1,所以合并成一个if
nums[k] = temp[i++];
else{
nums[k] = temp[j++];
res += m - i + 1; //从i到m的数都比第j个数大
}
}
return res;
}
}
DAY26
14-Ⅱ:剪绳子
思路:与Ⅰ的区别就是绳长的范围变大了,所以需要有大数取余的过程。
由算术几何均值不等式得:当且仅当时候等号成立。
推论1:将绳子以相等的长度等分为多段 ,得到的乘积最大。
推论2:尽可能将绳子以长度3等分为多段时,乘积最大。
所以可得切分规则:
- 把绳子尽可能切为多个长度为3的片段,留下的最后一段绳子长度可能是0,1,2
- 若最后一段为2,直接乘即可,相当于和上一段凑成了3*2,比将它拆开1*1大
- 若最后一段为1,与上一段合并为4,拆分成2*2比3*1要大
大数越界:当a增大时,最后返回的3^a大小以指数级别增长,可能超出 int32
甚至 int64
的取值范围,导致返回值错误。
求余问题:在仅使用 int32
类型存储的前提下,正确计算 x^a 对p求余的值。(越界后除以p取余数继续验算,对最后的结果无影响!!)
class Solution {
public int cuttingRope(int n) {
if( n <= 3) return n - 1; //n从2开始
int b = n % 3, p = 1000000007; //将绳子切成每段长度为3的,最后余下的那一段的长度b可能是0/1/2
long res = 1;
int a = n / 3; //一共切成了a段
for(int i = 1; i < a; i++)
res = (res * 3) % p; //从第一段开始验算3^a是否越界,一共验算a-1次
if(b == 0) return (int)(res * 3 % p);
if(b == 1) return (int)(res * 4 % p); //余1的时候可以将多余的和前一个3合并为4,4拆成2*2更大
return (int)(res * 6 % p); //余2的时候和上一段合并为5 拆成2*3
}
}
// 求 (x^a) % p —— 循环求余法。固定搭配建议背诵
public long remainder(int x,int a,int p){ //x为底数,a为幂,p为要取的模
long rem = 1 ;
for (int i = 0; i < a; i++) {
rem = (rem * x) % p ;
}
return rem;
}
43:1~n整数中1出现的次数
思路:将1~n的个位、十位、百位...的1的次数相加即可。
- 当前值为0时,此位 1 的出现次数只由高位 high 决定,计算公式为:high×digit
- 当前值为1时,计算公式为:high×digit+low+1
- 当前值>1时,计算公式为(high+1)*digit
用23041举例,我的理解是:
当前cur在百位为0,现在要求百位1的个数,那么取值范围应该是00100~22199。也就是说高位部分有00~22共23种可取值,而地位部分从00到99都可以取不影响,共100种,所以低位部分可取值范围就是digit,根据排列组合可得共有23*100种,也就是high*digit
用23141举例:
当前cur在百位为1,那么取值范围应该是00100~23141。高位部分可能取值是00~23,这里要注意的是当高位部分等于23的时候,低位部分的可取值范围就不是00~99了,而是00~41,所以才和低位是有关系的。这样就可以得出高位是00~22的时候低位是00~99,共high*digit中,再加上高位是23时,低位是00~41共42种可能,也就是low + 1。所以计算公式为high*digit+low+1
用23441举例
当前cur在百位为4,取值范围应该是00100~23199。高位部分可取范围是00~23,这个与上一个不同的是当高位部分取23的时候,低位部分可取值范围是00~99,所以与低位无关了。那么计算公式就是(high+1)*digit。
class Solution {
public int countDigitOne(int n) {
int res = 0, digit = 1; // 表示位,从个位开始
int high = n / 10, cur = n % 10, low = 0; //高位从十位开始,cur从个位开始
while(high != 0 || cur != 0){
if(cur == 0) res += high *digit;
else if(cur == 1) res += high * digit + low + 1;
else res += (high + 1) * digit;
low += cur * digit; //low是cur后面数字组成的数
cur = high % 10;
high /= 10; //high是cur前面的数字组成的数,不用乘位数
digit *= 10;
}
return res;
}
}
44:数组序列中某一位的数字
数位数量就例如是2位数90个共占用180个数位
- 先计算出第n为数字是属于几位数部分的
- 然后再计算出这位是属于那个数字
- 最后计算这位是属于数字里的第几位
看着蛮简单的,但是中间计算过程好费脑。首先判断n是几位数的时候用它循环减去一位两位三位数...所占的位数,当n递减的值不再大于某位数占用的位数时候则说明它属于某位数。
注意数字和n都是从0开始的。所以后面是n-1
比如n=13时候,先减去一位数占的位数9之后剩下4,二位数占180个位,所以第十三位是属于两位数的。
而现在n的值4就可以计算出它所在的数字是多少。每位数的起始位是0/10/100...,所以用刚才循环里记录的start,加上(n-1)除以位数(2),也就是每两位组成一个数字,便可得到它所属的数字是多少。
最后要判断它是这个数字里的第几位(从0开始),(n-1)除以位数的余数就是它所在数字中的位数,将这个作为索引输出转化为字符串的num对应的位就是结果。
class Solution {
public int findNthDigit(int n) {
int digit = 1;
long start = 1;
long count = 9;
while(n > count){
n -= count; //减去个位数占得位数,百位数占的位数...
digit += 1; //位数每次+1,即一位数到两位数到三位数到...
start *= 10; //n位数的起始就是1/10/100/1000 start*10即可
count = digit * start * 9; //数位数量
}
long num = start + (n - 1) / digit; //得到第n位所在的数字是多少
//将数字转化为字符,输出它的第0/1/2/3..位
return Long.toString(num).charAt((n - 1) % digit) - '0';
}
}
46:把数字翻译成字符串
思路:动态规划。对于num里的一个数有两种情况:
- 只翻译自己
- 和前一个数字组成10~25之间的两位数翻译
类同青蛙跳台阶,只不过这个一次跳两阶有条件限制
- 当大于10小于25,dp[ i ] = dp[ i - 1 ] + dp[ i - 2 ]
- 不在范围内则是dp[ i ] = dp[ i - 1 ]
class Solution {
public int translateNum(int num) {
String s = String.valueOf(num); //转换成字符串
int a = 1, b = 1;
for(int i = 2; i <= s.length(); i++){
String temp = s.substring(i - 2, i);
//当两位数大于10小于25的时候也可以翻译,否则就只能单独翻译
int c = temp.compareTo("10") >= 0 && temp.compareTo("25") <= 0 ? a + b : a;
/*
int value=str1.compareTo(str2);
当str1小于str2时,返回小于0的值,
当str1与str2相同时,返回0,
当str1大于str2时,返回大于0的值。
*/
b = a;
a = c;
}
return a;
}
}
48:最长不含重复字符的子字符串
思路:动态规划。
- 字符不重复:长度+1,此时最大子串长度 = 上一位的最大子串长度 + 1
- 字符重复:不考虑中间存在其他重复的前提下,最大子串长度 = 现在的位置 - 相同字符上次出现的位置
/*
字符 a b c c c b a d
长度 1 2 3 1 1 2 3 4
*/
class Solution {
public int lengthOfLongestSubstring(String s) {
int ans = 0, n = s.length(), last = 0;
HashMap<Character, Integer> map = new HashMap<>(n);
for(int i = 0; i < n; i++){
Character c = s.charAt(i);
last = Math.min(i - map.getOrDefault(c, -1), last + 1);
ans = Math.max(ans, last);
map.put(c, i);
}
return ans;
}
}