数组嵌套
题目:索引从0开始长度为N的数组A,包含0到N - 1的所有整数。找到最大的集合S并返回其大小,其中 S[i] = {A[i], A[A[i]], A[A[A[i]]], ... }且遵守以下的规则。假设选择索引为i的元素A[i]为S的第一个元素,S的下一个元素应该是A[A[i]],之后是A[A[A[i]]]... 以此类推,不断添加直到S出现重复的元素。
示例 1:
输入: A = [5,4,0,3,1,6,2]
输出: 4
解释:
A[0] = 5, A[1] = 4, A[2] = 0, A[3] = 3, A[4] = 1, A[5] = 6, A[6] = 2.
其中一种最长的 S[K]:
S[0] = {A[0], A[5], A[6], A[2]} = {5, 6, 2, 0}
提示:
N是[1, 20,000]之间的整数。
A中不含有重复的元素。
A中的元素大小在[0, N-1]之间。
思路:说实话,这个题我见过类似的。就是前两周的一次周赛,当时那个题的名字叫做朋友圈啊。其实这个题我觉得还挺简单的。首先确定所有元素都是非负数了。然后从0开始遍历(因为0不能用负数标记,所以要记得0已经用过了)遍历一圈用到的元素变成负数。这些算一个圈子的,记录长度。然后继续遍历第一个非标记的元素。它的圈子也记录比较长度。直到所有的都标记完了,这个时候长度就是我们比较出的最长的了。我去代码实现了
思路没问题,而且性能也挺好的,我直接附上代码:
class Solution {
public int arrayNesting(int[] nums) {
int res = 1;//这里为了防止只有一个元素,所以起始就是1
for(int i = 0;i<nums.length;i++) {
if(nums[i]<=0)continue;
int len = 1;
int temp = nums[i];
//用过的元素标记为负数
nums[i] = - nums[i];
while(temp>0) {
//当这个值小于等于0.说明已经被用过了
if(nums[temp]<0) break;
int cur = temp;
temp = nums[temp];
//用过的元素标记为负数
nums[cur] = -nums[cur];
len++;
}
res = Math.max(res, len);
}
return res;
}
}
思路其实就是可以互相跳的是一个圈子。最终选择元素最多的圈子就可以了。因为每个元素只会出现一次,所以不会出现交叉的圈子。反正思路就是这样,下一题。
字符串的排列
题目:给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。换句话说,第一个字符串的排列之一是第二个字符串的子串。
示例1:
输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
示例2:
输入: s1= "ab" s2 = "eidboaoo"
输出: False
注意:
输入的字符串只包含小写字母
两个字符串的长度都在 [1, 10,000] 之间
思路:这个包含排列,就是s2中有一段子串,是s1中的元素。这个排列之一说的还挺明确的。我读两遍才明白题意。因为两个字符串的长度都在1w以内,所以穷举是不可能了。其次是s1如果长于s2也可以直接false。但是剩下的比对。遇到不存在的直接下一个开始,存在的滑窗。暴力法虽然性能有问题,但是好用啊。我是试试先
额,我居然过了,而且性能超过我的想象,哈哈,直接贴代码:
class Solution {
public boolean checkInclusion(String s1, String s2) {
int len = s1.length();
int len2 = s2.length();
if (len > len2)
return false;
int[] arr = new int[26];
for (char c : s1.toCharArray())
arr[c - 'a']++;
int start = 0;
int sum = 0;
char[] c2 = s2.toCharArray();
for (int i = 0; i < len2; i++) {
char c = c2[i];
if (arr[c - 'a'] > 0) {//有这个字符,正常往下走
arr[c - 'a']--;
sum++;
if (sum == len)
return true;
} else if (s1.indexOf(c) == -1) {// 没有这个字符判断是次数没了还是单纯没有
sum = 0;
for (int j = start; j < i; j++) arr[c2[j] - 'a']++;// 之前怎么减的现在怎么加回来
start = i + 1;// 从下个开始从头算
} else {// 说明s1中有这个串,单纯的次数多了,可以从前开始减
for (int j = start; j < i; j++) {
if (c2[j] == c2[i]) {
start = j+1;
break;
}
sum--;
arr[c2[j] - 'a']++;// 之前怎么减的现在怎么加回来
}
}
}
return false;
}
}
我是个人感觉我备注写的挺全了,本来以为我写这么墨迹性能不会好呢,哈哈,有点小开心,今天的几道题刷的很顺啊。反正思路就是这样,其实也算是个变相的滑窗了,区别就是遇到s1中没有的字符直接这个窗口里的都不要了,思路不难,代码墨迹,细节点很多,我反正是各种小细节忘了写,然后面向测试案例编程,不断完善的。就不多说了,下一题。
出界的路径数
题目:给定一个 m × n 的网格和一个球。球的起始坐标为 (i,j) ,你可以将球移到相邻的单元格内,或者往上、下、左、右四个方向上移动使球穿过网格边界。但是,你最多可以移动 N 次。找出可以将球移出边界的路径数量。答案可能非常大,返回 结果 mod 109 + 7 的值。
思路:这个题怎么说呢,我个人的想法就是递归。周围一圈都是1.说明到了那下一步就可以出去了。其次如果当前大于1步,还可以继续往上下左右走。反正思路有了,我去实现下试试。
哎,其实这个题怎么说呢,在最开始出现了取模的时候我就应该想到这个是个dp的题目。令f(i,j,N)是位于i,j,总共有N步的总数。那么向左走一步就是f(i-1,j,N-1)。这里关键在于可以向四个方向走,所以状态转移方程就是把四个方向上的加起来的总和:
f(i,j,N) = f(i+1,j,N-1)+f(i-1,j,N-1)+f(i,j+1,N-1)+f(i,j-1,N-1);
至此这个题的思路就很清楚了。然後实现代码:
class Solution {
int m;
int n;
int modularity = 1000000007;
Long[][][] mem;
public int findPaths(int m, int n, int N, int i, int j) {
mem = new Long[N + 1][m][n];
this.m = m;
this.n = n;
return (int) (f(N, i, j) % modularity);
}
public Long f(int N, int i, int j) {
if (N < 0) return 0L;
if (N == 0) {
if (i < 0 || i >= m || j < 0 || j >= n) return 1L;
return 0L;
}
if (i < 0 || i >= m || j < 0 || j >= n) return 1L;
if (mem[N][i][j] != null) return mem[N][i][j];
Long i1 = (f(N - 1, i + 1, j)
+ f(N - 1, i - 1, j)
+ f(N - 1, i, j + 1)
+ f(N - 1, i, j - 1)) % modularity;
mem[N][i][j] = i1;
return i1;
}
}
怎么说呢,这个题说简单也不简单,说难也不算难。只要思路明确了。递归条件找好了就行了,感觉思路还是挺清楚的。就不多说了,下一题。
两个字符串的删除操作
题目:给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
示例:
输入: "sea", "eat"
输出: 2
解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
提示:
给定单词的长度不超过500。
给定单词中的字符只含有小写字母。
思路:这个题应该是找两个字符串的最大子序列吧。然後除了这个子序列的都删除。重点就是这两个最大子序列。但是幸好我以前做过类似的题目。用的是dp。
ac了,就是性能略差。我这里先贴代码:
class Solution {
public int minDistance(String word1, String word2) {
int l1 = word1.length();
int l2 = word2.length();
//判断w1和w2的最大子序列
int[][] d = new int[l1+1][l2+1];
for(int i = 0;i<l1;i++) {
for(int j = 0;j<l2;j++) {
//需要注意的是这个d的下标要在i和j的基础上都+1
if(word1.charAt(i)==word2.charAt(j)) {
d[i+1][j+1] = d[i][j]+1;
}else if(d[i][j+1]>d[i+1][j]) {
d[i+1][j+1] = d[i][j+1];
}else {
d[i+1][j+1] = d[i+1][j];
}
}
}
return l1+l2-(d[l1][l2]*2);
}
}
这种题目是我喜欢的那种题,稍微有点难度,但是我能做出来。而且中间有思考的过程。反正这个是很标准的一个dp。本质上,如果两个两个字符是相等的,那么这个公共子序列就会多一个,所以是上一个公共子序列的基础上+1.反之如果不相等,要判断两边到i,和到j那个结果最长,选择最长的那个。
性能问题我觉得可能是细节处理没处理好,我去试试换成数组。
没有换数组,简单的换了下遍历的下标(之前的以两次字符串为准,现在是以dp数组下标为准)性能就上来了:
所以这个题就这样吧,下一题了。
分数加减运算
题目:给定一个表示分数加减运算表达式的字符串,你需要返回一个字符串形式的计算结果。 这个结果应该是不可约分的分数,即最简分数。 如果最终结果是一个整数,例如 2,你需要将它转换成分数形式,其分母为 1。所以在上述例子中, 2 应该被转换为 2/1。
示例 1:
输入:"-1/2+1/2"
输出: "0/1"
示例 2:
输入:"-1/2+1/2+1/3"
输出: "1/3"
示例 3:
输入:"1/3-1/2"
输出: "-1/6"
示例 4:
输入:"5/3+1/3"
输出: "2/1"
说明:
输入和输出字符串只包含 '0' 到 '9' 的数字,以及 '/', '+' 和 '-'。
输入和输出分数格式均为 ±分子/分母。如果输入的第一个分数或者输出的分数是正数,则 '+' 会被省略掉。
输入只包含合法的最简分数,每个分数的分子与分母的范围是 [1,10]。 如果分母是1,意味着这个分数实际上是一个整数。
输入的分数个数范围是 [1,10]。
最终结果的分子与分母保证是 32 位整数范围内的有效整数。
思路:这个题怎么说呢,我感觉思路是清楚的,就是对字符串的处理上稍显麻烦。首先用1/1作为系数。一个分数一个分数的算就行了。我去试试代码吧。
这个题多次调试后ac了,而且超出我预计的是性能还挺好,我直接贴代码:
class Solution {
public String fractionAddition(String expression) {
int fz = 0;
int fm = 1;
boolean flag = true;
char[] c = expression.toCharArray();
for(int i = 0;i<c.length;i++) {
if(c[i] == '-') {
flag = false;
}else if (c[i] == '+') {
flag = true;
}else {//走到这一定是分式了,所以直接解析这个分式
int z = c[i++]-48;
while(c[i] != '/') z = z*10+(c[i++]-48);
i++;//把除号跳过去
int m = c[i++]-48;
while(i<c.length && c[i] != '+' && c[i] != '-') m = m*10+(c[i++]-48);
i--;
//至此这个表达式的分母和分子都知道了,
int[] res = count(fz, fm, z, m, flag);
fz = res[0];
fm = res[1];
}
}
if(fz == 0) return "0/1";
if(fz>0) {
int y = getValues(fz, fm);
return (fz/y)+"/"+(fm/y);
}else {
fz = -fz;
int y = getValues(fz, fm);
return "-"+(fz/y)+"/"+(fm/y);
}
}
public int getValues(int a,int b){
int small=Math.min(a,b);
int big=Math.max(a,b);
if (big%small==0){
return small;
}
return getValues(big%small,small);
}
//i1是第一个数分子,i2是第二个数分母。j1是第二个数分子,j2是第二个数分母。flag是true+。false-
public int[] count(int i1,int i2,int j1,int j2,boolean flag) {
int b = 0;
int z = 0;
for(int k = 1;k<=j2;k++) {//以j2为准,因为j2不会大于10.找出两个分母的最小公倍数
if((i2*k)%j2==0) {
b = i2*k;
break;
}
}
if(flag) {
z = i1*(b/i2)+j1*(b/j2);
}else {
z = i1*(b/i2)-j1*(b/j2);
}
return new int[] {z,b};
}
}
这个代码写的,没啥难的,就是墨迹。一点点把思路用代码写出来就ok了。第一步是分解出每一个公式。然后做加减法。然后最后的值再化简。不过这些用到吗实现还是很麻烦的。反正因为性能不错所以就这样了,我直接去看看性能排行第一的代码:
class Solution {
public String fractionAddition(String expression) {
int n = expression.length();
char[] chars = expression.toCharArray();
int[] molecule = new int[n], denominator = new int[n];
Arrays.fill(denominator, 1);
molecule[0] = Character.isDigit(chars[0]) ? findNumber(chars, 0): 0;
int p_molecule = 0;
for (int i = 0; i < n; i++) {
if (chars[i] == '-') {
molecule[i + 1] = -1 * findNumber(chars, i + 1);
p_molecule = i + 1;
} else if (chars[i] == '+') {
molecule[i + 1] = findNumber(chars, i + 1);
p_molecule = i + 1;
} else if (chars[i] == '/') {
denominator[p_molecule] = findNumber(chars, i + 1);
}
}
int lcm = denominator[0];
for (int i : denominator) {
lcm = lcm(lcm, i);
}
int molecule_sum = 0;
for (int i = 0; i < n; i++) {
int m = lcm / denominator[i];
molecule_sum += molecule[i] * m;
}
int d = gcd(Math.abs(molecule_sum), lcm);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(molecule_sum / d);
stringBuilder.append('/');
stringBuilder.append(lcm / d);
return stringBuilder.toString();
}
private int findNumber(char[] chars, int index) {
int n = chars.length;
int res = 0;
for (int i = index; i < n; i++) {
if (Character.isDigit(chars[i])) {
res *= 10;
res += chars[i] - '0';
} else break;
}
return res;
}
private int gcd(int a, int b) {
return a % b == 0 ? b : gcd(b, a % b);
}
private int lcm(int a, int b) {
return (a * b) / gcd(a, b);
}
}
相比于我自己的代码,这个的好多处理优雅了点,不过也没觉得特别惊艳,毕竟都是硬生生的计算,唯一算得上算法的也就求两个数的最大公约数和最小公倍数了吧?反正这个题就这样了,下一题。
有效的正方形
题目:给定二维空间中四点的坐标,返回四点是否可以构造一个正方形。一个点的坐标(x,y)由一个有两个整数的整数数组表示。
示例:
输入: p1 = [0,0], p2 = [1,1], p3 = [1,0], p4 = [0,1]
输出: True
注意:
所有输入整数都在 [-10000,10000] 范围内。
一个有效的正方形有四个等长的正长和四个等角(90度角)。
输入点没有顺序。
思路:其实我觉得这个题还好吧,有个简单的思路。任选三个点都是等腰直角三角形就可以了。勾股定理应该就能解决。有个简单的思路,我去代码实现试试。
先贴代码:
public class Solution {
public boolean validSquare(int[] p1, int[] p2, int[] p3, int[] p4) {
//任选三个点 都是 一个等腰直角三角形
return isRightTriangle(p1, p2, p3) && isRightTriangle(p1, p2, p4) && isRightTriangle(p1, p3, p4) && isRightTriangle(p2, p3, p4);
}
public boolean isRightTriangle(int[] p1, int[]p2, int[] p3){
int d1 = (p1[0] - p2[0]) * (p1[0] - p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1]);
int d2 = (p2[0] - p3[0]) * (p2[0] - p3[0]) + (p2[1] - p3[1]) * (p2[1] - p3[1]);
int d3 = (p3[0] - p1[0]) * (p3[0] - p1[0]) + (p3[1] - p1[1]) * (p3[1] - p1[1]);
if(d1 > d2 && d2 == d3 && d2 + d3 == d1 ||
d2 > d1 && d1 == d3 && d1 + d3 == d2 ||
d3 > d1 && d1 == d2 && d1 + d2 == d3){
return true;
}
return false;
}
}
这个代码怎么说呢,不复杂, 就是麻烦。我这个思路还算是清晰。但是自己懒得写代码。。。咳咳,反正这个题就这样吧。实现的方式也是多种多样的。我这样判断可以、我看还有的说判断对角线和边。也有的是一个点90度边长旋转。转一圈的。反正就差不多这样。不多说了,下一题。
分割数组为连续子序列
题目:给你一个按升序排序的整数数组 num(可能包含重复数字),请你将它们分割成一个或多个子序列,其中每个子序列都由连续整数组成且长度至少为 3 。如果可以完成上述分割,则返回 true ;否则,返回 false 。
示例 1:
输入: [1,2,3,3,4,5]
输出: True
解释:
你可以分割出这样两个连续子序列 :
1, 2, 3
3, 4, 5
示例 2:
输入: [1,2,3,3,4,4,5,5]
输出: True
解释:
你可以分割出这样两个连续子序列 :
1, 2, 3, 4, 5
3, 4, 5
示例 3:
输入: [1,2,3,4,4,5]
输出: False
提示:
输入的数组长度范围为 [1, 10000]
思路:重点是分成连续的升序子数组。一种是出现某种情况一定会false(比如重复元素数大于总数/3)。还有就是遇到重复元素单出一条线。然后往下遍历满足每一条线,最终有怎么也满足不了的线false。有简单的想法,我去实现下。
这个题怎么说呢,实现了,虽然性能不是很满意,我先贴代码:
class Solution {
public boolean isPossible(int[] nums) {
List<List<Integer>> list = new ArrayList<List<Integer>>();
for(int i = 0;i<nums.length;i++) {
boolean flag = false;
//新生成的在最后,所以从后往前追加
int size = list.size()-1;
for(int j = size;j>=0;j--) {
if(isAppend(nums[i], list.get(j)) ) {
list.get(j).add(nums[i]);
flag = true;
break;//这个元素已经用了,直接开始下一个元素
}else {
if(list.get(j).get(list.get(j).size()-1)!=nums[i] && list.get(j).size()<3) {
//走到这说明这个线路不能追加当前元素,最后一个不是当前元素且线路不满足3,直接false
return false;
}else if(list.get(j).get(list.get(j).size()-1)!=nums[i]){
list.remove(j);//到这步说明当前线路不能追加了,并且已经超过三个,直接删除
}
}
}
if(!flag) {//说明这个元素根本没用到,单开一条线路
List<Integer> list2 = new ArrayList<Integer>();
list2.add(nums[i]);
list.add(list2);
}
}
for(List<Integer> i:list) {
if(i.size()<3) return false;
}
return true;
}
//能不能追加
public boolean isAppend(int i,List<Integer> list) {
if(list.get(list.size()-1)+1 != i ) return false;
return true;
}
}
不知道为啥最近的题都代码贼多。反正这个题思路是有的。就是能续之前的线就续之前的线。续不了就开新线。而且上面的代码有几个优化点(虽然这么优化性能也不咋地。):一个是如果遇到不符合条件的线直接false。可以少做很多无用功。还有一个大家注意,我这个list是倒叙遍历的,因为后生成的线肯定是较短的(不要抬杠一边长)。我自己最满意的一点就是如果够长了,而且往后断了的线直接remove,这样不用遍历好多无用的线。然后我这么满意的代码,性能一塌糊涂。。。我感觉这个已经不是单纯的思路上的优化能解决的了,可能是list本身就不行。我感觉list可以被替换成二维数组。一个表示当前最后一位数字,一个表示当前长度。我去试试性能会不会提升吧。
我踏马,实现是实现了,性能越来越差了,简直日狗了,不过辛苦写出来的代码不贴出来对不起自己:
class Solution {
public boolean isPossible(int[] nums) {
int[][] d = new int[10000][2];//0代表最后一个数字,1代表长度
d[0] = new int[]{nums[0],1};
int idx = 0;
for(int i = 1;i<nums.length;i++) {
boolean flag = false;
//新生成的在最后,所以从后往前追加
for(int j = idx;j>=0;j--) {
if(d[j][0]+1 == nums[i]) {
d[j][0]++;
d[j][1]++;
flag = true;
break;
}else {
if(d[j][0] == nums[i]) continue;
//走到这说明是续不上了
if(d[j][1]<3) return false;
}
}
if (!flag) d[++idx] = new int[] {nums[i],1};
}
for(int i = 0;i<=idx;i++) {
if(d[i][1]<3) return false;
}
return true;
}
}
只能安慰安慰自己代码量减少了,我去看看性能排行第一的代码吧:
好吧,我先自闭一波,看了人家的题解感觉我就是个废物!首先不说代码,就说思路:
这道题得采用贪心策略,就是使序列尽可能的长。但是这种策略好像给人一种错误的感觉,比如[1,2,3,3,4,5],如果采用此策略,将会是[1,2,3,4,5]和剩余的[3]。其实这个策略并不是这么简单的
比如它扫描到’1’的时候,由于它的前一个元素’0’不存在以’0’结尾的连续子序列,所以它这是向后寻找两个元素,凑成子序列[1,2,3](这时1,2,3各被消耗了一个)。接着我们就应该访问到’3’,我们又去查询以’2’结尾的有没有合法的连续序列,但是没有,所以它此时只能向后寻找两个元素,凑出连续子序列[3,4,5](3,4,5个被消耗了一次),结束访问。
如果输入[1,2,3,3,4,4,5,5],刚开始访问'1',建立[1,2,3],接着访问'3',建立[3,4,5]接着访问'4',由于第一步建立了[1,2,3]以4 - 1结尾的连续子序列,所以它放入,得到[1,2,3,4]接着访问'5',由于第一步建立了[1,2,3,4]以5 - 1结尾的连续子序列,所以它放入,得到[1,2,3,4,5]
这个思路是很清晰明了的,虽然废物如我看了这个还用了好久才写出代码。但是毕竟写出来了,所以记录一下。
class Solution {
public boolean isPossible(int[] nums) {
//每个数字出现的次数
Map<Integer,Integer> countMap = new HashMap<Integer, Integer>();
Map<Integer,Integer> endMap = new HashMap<Integer, Integer>();
//计数
for(int i : nums) countMap.put(i, countMap.getOrDefault(i, 0)+1);
for(int i :nums) {
if(countMap.get(i)<1) continue;
countMap.put(i, countMap.get(i)-1);
int n = endMap.getOrDefault(i-1, 0);
if(n>0) {//说明前面有子序列可以用,直接追加。
endMap.put(i-1, endMap.get(i-1)-1);//以i-1结尾的子序列少一个
endMap.put(i, endMap.getOrDefault(i, 0)+1);//以i结尾的子序列多一个
}else {
//说明没有子序列可用,只能自己开一条
int i1 = countMap.getOrDefault(i+1, 0);
int i2 = countMap.getOrDefault(i+2, 0);
if(i1<1 || i2<1) return false;//说明这个元素凑不齐三个。
endMap.put(i+2, endMap.getOrDefault(i+2, 0)+1);//以i+2结尾的子序列多了一个
countMap.put(i+2,countMap.get(i+2)-1);//i+2用了一个
countMap.put(i+1,countMap.get(i+1)-1);//i+1用了一个
}
}
return true;
}
}
我脑壳疼,一样一样的思路,人家打败九十多,我打败九多。。。算了算了,我还是直接贴性能第一的代码吧:
class Solution {
public boolean isPossible(int[] nums) {
int n = nums.length, i = 0;
if (n < 3) return false;
// 当前长度为1,2的序列的数量,当前能用的多于2的序列数量
int one = 0, two = 0, more = 0;
// 初始化one
while (i < n && nums[i] == nums[0]) {
one++;
i++;
}
while (i < n) {
int pre = nums[i - 1];
// 下一个数与上一个数断了的情况,即count为0的特殊情况
if (nums[i] - pre > 1) {
// 有one或two则直接返回false
if (one > 0 || two > 0) {
return false;
}
// more全部断
more = 0;
int start = nums[i];
// 初始化one
while (i < n && nums[i] == start) {
one++;
i++;
}
} else {
int count = 0;
// 查找比上一个数大1的数的数量
while (i < n && nums[i] == pre + 1) {
count++;
i++;
}
int remain = count - one - two, temp;
if (remain < 0) {
// 长度1和长度为2的是一定需要被满足的,所以说如果count不够,返回false
return false;
} else {
// 满足长度1和2后,贪心算法,如果有more没必要另起一个one,所以remain尽可能给more
// 原来的two也变成more,remain如果小于more,那么有一部分more断了不再讨论
temp = remain - more;
more = Math.min(remain, more) + two;
}
// 原来的one变成two
two = one;
// 如果remain大于more,则多的另起one,否则没有one
one = Math.max(0, temp);
}
}
return one + two == 0;
}
}
这个题简直写的我心痛。反正就这样了吧。解法这么多,我写的没几个好的。
这篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利,周末愉快!