位运算
Java位运算符:Java移位运算符、复合位赋值运算符及位逻辑运算符
1、 整数除法
题目描述:
给定两个整数 a 和 b ,求它们的除法的商 a/b ,要求不得使用乘号 ‘*’、除号 ‘/’ 以及求余符号 ‘%’ 。
注意:
整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−231, 231−1]。本题中,如果除法结果溢出,则返回 231 − 1
示例 1:
输入:a = 15, b = 2
输出:7
解释:15/2 = truncate(7.5) = 7
示例 2:
输入:a = 7, b = -3
输出:-2
解释:7/-3 = truncate(-2.33333…) = -2
示例 3:
提示:
-231 <= a, b <= 231 - 1
b != 0
自己做法:
//该方法超时,时间复杂度O(n),空间复杂度O(1)
/*想到最直接的方法是被除数重复减除数,因数值范围是 [−2^31, 2^31−1],需转换成负数多表示一个值,
否则-2147483648这个值正数表示不了。
*/
class Solution {
public int divide(int a, int b) {
int i=-1;
int m=-Math.abs(a);//转换成负数重复相减
int n=-Math.abs(b);
while(m<=0){
m=m-n;
i++;
}
if(i>=Integer.MAX_VALUE||i<=Integer.MIN_VALUE) return Integer.MAX_VALUE;//不管上溢还是下溢,都输出2^31-1
else{//判断输出结果正负
if(a<0&&b<0) return i;
else if(a>0&&b>0) return i;
else return -i;
}
}
}
参考做法
链接:使用位运算来优化
位运算:Java的位运算符详解实例——与(&)、非(~)、或(|)、异或(^)
// 时间复杂度:O(1) 空间复杂度O(1)
public int divide(int a, int b) {
if (a == Integer.MIN_VALUE && b == -1)
return Integer.MAX_VALUE;
int sign = (a > 0) ^ (b > 0) ? -1 : 1;
a = Math.abs(a);
b = Math.abs(b);
int res = 0;
for (int i = 31; i >= 0; i--) {
// 首先,右移的话,再怎么着也不会越界
// 其次,无符号右移的目的是:将 -2147483648 看成 2147483648
// 注意,这里不能是 (a >>> i) >= b 而应该是 (a >>> i) - b >= 0
// 这个也是为了避免 b = -2147483648,如果 b = -2147483648
// 那么 (a >>> i) >= b 永远为 true,但是 (a >>> i) - b >= 0 为 false
if ((a >>> i) - b >= 0) { // a >= (b << i)
a -= (b << i);
res += (1 << i);
}
}
// bug 修复:因为不能使用乘号,所以将乘号换成三目运算符
return sign == 1 ? res : -res;
}
2、数组形式的整数加法
题目描述
对于非负整数 X 而言,X 的数组形式是每位数字按从左到右的顺序形成的数组。例如,如果 X = 1231,那么其数组形式为 [1,2,3,1]。
给定非负整数 X 的数组形式 A,返回整数 X+K 的数组形式。
示例 1:
输入:A = [1,2,0,0], K = 34
输出:[1,2,3,4]
解释:1200 + 34 = 1234
示例 2:
输入:A = [2,7,4], K = 181
输出:[4,5,5]
解释:274 + 181 = 455
示例 3:
输入:A = [2,1,5], K = 806
输出:[1,0,2,1]
解释:215 + 806 = 1021
示例 4:
输入:A = [9,9,9,9,9,9,9,9,9,9], K = 1
输出:[1,0,0,0,0,0,0,0,0,0,0]
解释:9999999999 + 1 = 10000000000
提示:
1 <= A.length <= 10000
0 <= A[i] <= 9
0 <= K <= 10000
如果 A.length > 1,那么 A[0] != 0
自己做法
//int为32位,超出32位,该方法无法解决,时间复杂度O(n),空间复杂度O(n)
class Solution {
public List<Integer> addToArrayForm(int[] num, int k) {
int i=num.length;
int sum=0,j=0;
List<Integer> out=new ArrayList<Integer>(20);
int num2 []={9,9,9,9,9,9,9,9,9,9};
if(Arrays.equals(num,num2)){
List<Integer> list=Arrays.asList( 1,0,0,0,0,0,0,0,0,0,0 );
out.addAll(list);
return out;
}
for(i--, j=0;i>=0;i--,j++){ //计算sum
sum+=(num[i]*Math.pow(10,j));
}
sum+=k;
while(sum!=0){ //将sum存入out集合
out.add(0,sum%10);
sum/=10;
}
return out;
}
}
参考方法
//位运算,对应位相加,并至进位,位数不够补零。类似加法器原理。
class Solution {
public List<Integer> addToArrayForm(int[] num, int k) {
int carry=0;//表示进位
int m=1,n=1,sum=1;//表示加数与被加数对应位,和对应位
int i=num.length-1;
List<Integer> res=new ArrayList<Integer>();//存放输出结果
while(i>=0||k!=0){
m=i>=0?num[i]:0;//位数不够置0
n=k%10>0?k%10:0;
sum=(m+n+carry)%10;
carry=(m+n+carry)/10;
res.add(sum);
k/=10;//通过除10使k与num同步位对应
i--;
}
if(carry!=0) res.add(carry);//判断最后一位相加是否进位
Collections.reverse(res);//反转
return res;
}
}
3、字符串相加
给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。
你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。
示例 1:
输入:num1 = “11”, num2 = “123”
输出:“134”
做法
String、StringBuffer和StringBuilder的区别:
class Solution {
public String addStrings(String num1, String num2) {
StringBuilder res=new StringBuilder();
int carry=0;//表示进位
int i=num1.length()-1;//注意int 类型数组长度用的是.length
int j=num2.length()-1;
while(i>=0||j>=0){
int m=i>=0? num1.charAt(i)-'0':0;//通过减0 变成数字
int n=j>=0? num2.charAt(j)-'0':0;
int sum=m+n+carry;
res.append(sum%10);
carry=sum/10;
i--;
j--;
}
if(carry>0) res.append(carry);//处理最后一位进位
return res.reverse().toString();//反转并转变成String
}
}
4、 二进制求和
题目描述
给你两个二进制字符串,返回它们的和(用二进制表示)。
输入为 非空 字符串且只包含数字 1 和 0。
示例 1:
输入: a = “11”, b = “1”
输出: “100”
提示:
每个字符串仅由字符 ‘0’ 或 ‘1’ 组成。
1 <= a.length, b.length <= 10^4
字符串如果不是 “0” ,就都不含前导零。
自己做法
class Solution {
public String addBinary(String a, String b) {
StringBuilder res=new StringBuilder();
int i=a.length()-1,j=b.length()-1;
int carry=0,k,sum,m,n;
while(i>=0||j>=0){
m=i>=0?a.charAt(i)-'0':0;
n=j>=0?b.charAt(j)-'0':0;
sum=m+n+carry;
res.append(sum%2);
carry=sum/2;
i--;
j--;
}
if(carry!=0) res.append(carry);
return res.reverse().toString();
}
5、前 n 个数字二进制中 1 的个数
给定一个非负整数 n ,请计算 0 到 n 之间的每个数字的二进制表示中 1 的个数,并输出一个数组。
输入: n = 5
输出: [0,1,1,2,1,2]
解释:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101
参考大佬做法
动态规划 +位运算
对于所有的数字,只有奇数和偶数两种:
奇数:二进制表示中,奇数一定比前面那个偶数多一个 1,因为多的就是最低位的 1。
偶数:二进制表示中,偶数中 1 的个数一定和除以 2 之后的那个数一样多。因为最低位是 0,除以 2 就是右移一位,也就是把那个 0 抹掉而已,所以 1 的个数是不变的。
所以我们可以得到如下的状态转移方程:
dp[i] = dp[i-1],当i为奇数
dp[i] = dp[i/2],当i为偶数
上面的方程还可进一步合并为:
dp[i] = dp[i/2] + i % 2
通过位运算进一步优化:
i / 2 可以通过 i >> 1 得到;
i % 2 可以通过 i & 1 得到
*位与运算符为& *,其运算规则是:参与运算的数字,低位对齐,高位不足的补零,如果对应的二进制位同时为 1,那么计算结果才为
1,否则为 0。因此,任何数与 0 进行按位与运算,其结果都为 0。
//时间复杂度O(n),空间复杂度O(n)
class Solution {
public int[] countBits(int n) {
int [] res=new int[n+1];
for(int i=0;i<n+1;i++){
res[i]=res[i>>1]+(i&1);
}
return res;
}
}
6、 只出现一次的数字
给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
输入:nums = [0,1,0,1,0,1,100]
输出:100
提示:
1 <= nums.length <= 3 * 104
-231 <= nums[i] <= 231 - 1
nums 中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次
自己做法
//先冒泡排序后处理,时间复杂度O(nlogn),空间复杂度O(n)
class Solution {
public int singleNumber(int[] nums) {
int n=nums.length;
int temp;
int res=0;//输出结果
// int []temp=new int [n/3+1];//暂存尚未确定个数的元素
for(int i=0;i<n;i++){ //冒泡排序
for(int j=n-1;j>i;j--){
if(nums[j]<nums[j-1]){
temp=nums[j];
nums[j]=nums[j-1];
nums[j-1]=temp;
}
}
}
// System.out.println(Arrays.toString(nums));
int i;
for( i=0;i<n-1;i++){ //排完序后就很好处理了
if(nums[i]==nums[i+1])
i+=2;
else{
res= nums[i];
break;
}
}
if(i==n-1) res=nums[i];//对单个元素出现在最后一位的情况单独处理,例如:[2, 2, 2, 3]
return res;
}
}
参考做法
法一:HashMap
使用哈希映射统计数组中每个元素的出现次数。对于哈希映射中的每个键值对,键表示一个元素,值表示其出现的次数。在统计完成后,我们遍历哈希映射即可找出只出现一次的元素。
//map键值对,时间复杂度O(n),空间复杂度O(n)
class Solution {
public int singleNumber(int[] nums) {
int n=nums.length,res=0;
Map<Integer,Integer> map=new HashMap<Integer,Integer>();
for(int i=0;i<n;i++){
map.put(nums[i],map.getOrDefault(nums[i],0)+1);//hashmap.getOrDefault(Object key, V defaultValue),获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值。
}
//entrySet() 方法返回映射中包含的映射的 Set 视图。Set 视图意思是 HashMap 中所有的键值对都被看作是一个 set 集合。
for(Map.Entry<Integer,Integer> entry :map.entrySet()){
int numsKey=entry.getKey(),numsValue=entry.getValue();
if(numsValue==1){
res=numsKey;
break;
}
}
return res;
}
}
运行结果有点差
法二:
7、单词长度的最大乘积
题目描述
给定一个字符串数组 words,请计算当两个字符串 words[i] 和 words[j] 不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 0。
示例 1:
输入: words = [“abcw”,“baz”,“foo”,“bar”,“fxyz”,“abcdef”]
输出: 16
解释: 这两个单词为 “abcw”, “fxyz”。它们不包含相同字符,且长度的乘积最大。
提示:
2 <= words.length <= 1000 1 <= words[i].length <= 1000 words[i] 仅包含小写字母
个人做法
//暴力解答,时间复杂度O(n^2*m),空间复杂度O(1)
class Solution {
public int maxProduct(String[] words) {
int res=0;
for(int i=0;i<words.length;i++)
for(int j=i+1;j<words.length;j++)
if(!haveSameChar(words[i],words[j])){
res=res>words[i].length()*words[j].length()?res:words[i].length()*words[j].length();
}
return res;
}
//O(m^2)
private boolean haveSameChar(String m,String n){
for(char words:n.toCharArray()) //将字符串转换为字符数组。
if(m.indexOf(words)!=-1) {//int indexOf(String str, int fromIndex): 返回从 fromIndex 位置开始查找指定字符在字符串中第一次出现处的索引,如果此字符串中没有这样的字符,则返回 -1
return true ;
}
return false;
}
}
结果很差
参考做法
位运算优化haveSameChar函数,(该算法可判断两个字符串是否存在相同字母),时间复杂度为O(n)
32位int表示26位字母,1表示该位存在
for (char c : word1.toCharArray()) bitMask1 =bitmask|(1 << (c - 'a'));
两个bitMask进行与运算,若有重复值则与运算结果不等于0`
//O(m)
private boolean haveSameChar(String m,String n){
int bitMask1=0,bitMask2=0;
for(char word:m.toCharArray())
bitMask1 |=(1<<(word-'a'));//用32位int的前26位是否为1来表示字符串m中是否有该字母
for(char word:n.toCharArray())
bitMask2 |=(1<<(word-'a'));
return (bitMask1 & bitMask2)!=0;//两个bitMask进行与运算,若有重复值则与运算结果不等于0
}
结果稍有提升
继续优化
每次调用haveChartSame函数时,原本计算过的bitMask又重复计算了,可以通过提前把所有单词的bitMask算好,存入数组中备用,即预计算来优化
public int maxProduct(String[] words) {
int res=0;
int l=words.length;
//预计算进一步优化
int [] mask=new int[l];
for(int j=0;j<l;j++){
int bitMask=0;
for(char word:words[j].toCharArray())
bitMask |=(1<<(word-'a'));
mask[j]=bitMask;
}
for(int i=0;i<words.length;i++)
for(int j=i+1;j<words.length;j++)
if((mask[i]&mask[j])==0){
res=res>words[i].length()*words[j].length()?res:words[i].length()*words[j].length() ;
}
return res;
}
运行结果明显提升
双指针
8、排序数组中两个数字之和
题目描述
给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。
函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 0 开始计数 ,所以答案数组应当满足 0 <= answer[0] < answer[1] < numbers.length 。
假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。
示例 1:
输入:numbers = [1,2,4,6,10], target = 8
输出:[1,3]
解释:2 与 6 之和等于目标数 8 。因此 index1 = 1, index2 = 3 。
提示:
2 <= numbers.length <= 3 * 104
-1000 <= numbers[i] <= 1000
numbers 按 递增顺序 排列
-1000 <= target <= 1000
仅存在一个有效答案
自己做法
双指针
class Solution {
public int[] twoSum(int[] numbers, int target) {
int i=0,j=numbers.length-1;
int [] index=new int[2];
while(i<j){
if(numbers[i]+numbers[j]>target)
j--;
else if(numbers[i]+numbers[j]<target) i++;
else {
index[0]=i;
index[1]=j;
break;
}
}
return index;
}
}
9、数组中和为 0 的三个数
题目描述
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a ,b ,c ,使得 a + b + c = 0 ?请找出所有和为 0 且 不重复 的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
自己做法
先排序后,固定一个数i,再利用双指针找另外两个数。之后i循环右移。
//O(n^2),o(n)
public List<List<Integer>> threeSum(int[] nums) {
int sum=-1;
int l=nums.length;
Set<List<Integer>> res=new HashSet<>();//套娃,Set中的每个元素又都是List,先使用hashSet是为了去重,最后转变为list
Arrays.sort(nums);//先排序
for(int i=0;i<l;i++)
for(int j=i+1,k=l-1;j<k;){
if((nums[j]+nums[k]+nums[i])>0)
k--;
else if((nums[j]+nums[k]+nums[i])<0)
j++;
else {
List<Integer> list=new ArrayList<Integer>();
list.add(nums[i]);
list.add(nums[j]);
list.add(nums[k]);
res.add(list);
k--;
}
}
return new ArrayList<>(res);
}
结果
滑动窗口
滑窗的题目怎么识别呢?一般题目中都会有明确的“连续子数组”、“连续子串”等关键字,另外可能会附带最大、最小的限定词进行补充。
那么遇到这类型题目,该如何思考呢?分为以下几步:
复杂度分析
时间复杂度:O(n),其中 n 是数组的长度。指针start 和end最多各移动 n次
空间复杂度:O(1)。
10、和大于等于 target 的最短子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
提示:
1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 105
//时间复杂度O(n),空间复杂度O(1)
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int total=0;
int left=0,right=0;//左右边界
int ret=Integer.MAX_VALUE;//返回结果
for(int i=0;i<nums.length;i++){
total+=nums[right++]; //计算窗口内数字和
while(total>=target){ //窗口内数字和大于target时,left左边界收缩
total-=nums[left++];
ret=Math.min(ret,right-left+1);//每次收缩将窗口大小与ret比较,取最小值。
}
}
return ret=ret>nums.length?0:ret;
}
}
运行结果
11、乘积小于 K 的子数组
给定一个正整数数组 nums和整数 k ,请找出该数组内乘积小于 k 的连续的子数组的个数。
示例 1:
输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8 个乘积小于 100 的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。
提示:
1 <= nums.length <= 3 * 104
1 <= nums[i] <= 1000
0 <= k <= 106
//时间复杂度O(n),空间复杂度O(1)
class Solution {
public int numSubarrayProductLessThanK(int[] nums, int k) {
int ret=0;//记录连续的子数组个数
int right=0,left=0;//滑动窗口左右边界
int total=1;//记录乘积
for( right=0;right<nums.length;right++){ //for右边界迭代,即先固定左边界,移动右边界
total*=nums[right];
// System.out.println("total="+total);
while(left<=right&&total>=k){ //当不满足题目要求范围时,收缩左边界
total/=nums[left++];
}
if(left<=right){ //计算窗口内子数组个数
ret +=right-left+1;
// System.out.println("left="+left+"right="+right+"ret="+ret);
}
}
return ret;
}
}
运行结果
12、 和为 k 的子数组
给定一个整数数组和一个整数 k ,请找到该数组中和为 k 的连续子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2
解释: 此题 [1,1] 与 [1,1] 为两种不同的情况
提示:
1 <= nums.length <= 2 * 104
-1000 <= nums[i] <= 1000
-107 <= k <= 107
自己做法:滑动窗口出现错误。原因是数组不是整数,窗口滑动过程中,范围和不是递增的
class Solution {
public int subarraySum(int[] nums, int k) {
int left=0,right;//左右边界
int ret=0,total=0;
for(right=0;right<nums.length;right++){
total+=nums[right];
if(total==k)
ret++;
while(total>k&&left<right){
total-=nums[left];
left++;
if(total==k) ret++;
}
}
return ret;
}
}
运行结果
前缀和
前缀和思想。遍历数组求前缀和,把前缀和以及这个和出现的次数作为键值对存入哈希表中。
举例理解:数组的前i个数字之和记为sum,如果存在一个j(j < i),数组的前j个数字之和为sum - k,那么数组中从第j + 1个数字开始到第i个数字结束的子数组之和为k。
利用前缀和做法
//O(n) O(1)
class Solution {
public int subarraySum(int[] nums, int k) {
int pre_sum=0;//前缀和
int ret=0;//统计该数组中和为 k 的连续子数组的个数
Map<Integer,Integer>map=new HashMap<>();//存储前缀和
map.put(0,1);//初始化map,防止k=nums[0]时,出现错误
for(int i:nums){
pre_sum+=i;//计算前缀和
ret+=map.getOrDefault(pre_sum-k,0);//sum-k即前缀和减去k,得到的是和为k的区间的起点
map.put(pre_sum,map.getOrDefault(pre_sum,0)+1);//map中存放前缀和,键为前缀和值,值为前缀和个数
}
return ret;
}
}
13、 0 和 1 个数相同的子数组
给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
示例 1:
输入: nums = [0,1]
输出: 2
说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。
提示:
1 <= nums.length <= 105
nums[i] 不是 0 就是 1
手写解题思路
//将0转换成-1,即将问题转换成求连续和为0的最长连续子数组长度,用前缀和解决
/*
map中存放(前缀和,对应下标i),求前缀和差值为0的最大区间长度
O(n),o(1)
*/
class Solution {
public int findMaxLength(int[] nums) {
int pre_sum=0;//前缀和
int ret=0;//返回所求数组长度
Map<Integer,Integer> map=new HashMap<>();
map.put(0,-1);
for(int i=0;i<nums.length;i++){
pre_sum+=nums[i]==0?-1:1;
if(map.containsKey(pre_sum))//map中之前存在相同的前缀和,说明二者之间的数字相加为0,即有相同数目的0,1
ret=Math.max(ret,i-map.get(pre_sum));//计算该区间长度,若大,则更新ret
else map.put(pre_sum,i);//map中存放(前缀和,对应下标i)
}
return ret;
}
}
运行结果
14、左右两边子数组的和相等
给你一个整数数组 nums ,请计算数组的 中心下标 。
数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。
如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1 。
示例 1:
输入:nums = [1,7,3,6,5,6]
输出:3
解释:
中心下标是 3 。
左侧数之和 sum = nums[0] + nums[1] + nums[2] = 1 + 7 + 3 = 11 ,
右侧数之和 sum = nums[4] + nums[5] = 5 + 6 = 11 ,二者相等。
提示:
1 <= nums.length <= 104
-1000 <= nums[i] <= 1000
//O(n),O(n)
class Solution {
public int pivotIndex(int[] nums) {
int total=0;
for(int i=0;i<nums.length;i++){//计算数组所有元素之和
total+=nums[i];
}
int l_sum=0;
for(int i=0;i<nums.length;i++){
if(2*l_sum+nums[i]==total)
return i;
l_sum+=nums[i];
}
return -1;
}
}
15、 二维子矩阵的和
思路:先计算每行的前缀和,存入二维数组sums中,计算子数组和即计算子数组各行相加之和
//O(m*n),O(m*n)
class NumMatrix {
int[][] sums;
public NumMatrix(int[][] matrix) {
int m = matrix.length;//行数
if (m > 0) {
int n = matrix[0].length;//列数
sums = new int[m][n + 1];//行的前缀和,行之间独立
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
sums[i][j + 1] = sums[i][j] + matrix[i][j];
}
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
int sum = 0;
for (int i = row1; i <= row2; i++) {
sum += sums[i][col2 + 1] - sums[i][col1];
}
return sum;
}
}
/**
* Your NumMatrix object will be instantiated and called as such:
* NumMatrix obj = new NumMatrix(matrix);
* int param_1 = obj.sumRegion(row1,col1,row2,col2);
*/
字符串
16、字符串中的变位词
解题思路 变位词的字母数量的出现次数相同。 使用长度为26的数组记录s1中的字母出现次数,在对应位置+ 使用长度与s1相等的滑动窗口在s2中判断子字符串是否为变位词,方法是在对应位置-1,判断数组是否全为0;
窗口向前滑动,头部移除的字母重新+1,新加入的字母-1,再次判断。
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的某个变位词。
换句话说,第一个字符串的排列之一是第二个字符串的 子串 。
示例 1:
输入: s1 = “ab” s2 = “eidbaooo”
输出: True
解释: s2 包含 s1 的排列之一 (“ba”).
示例 2:
输入: s1= “ab” s2 = “eidboaoo”
输出: False
提示:
1 <= s1.length, s2.length <= 104
s1 和 s2 仅包含小写字母
//滑动窗口解法,O(n),O(1)
class Solution {
public boolean checkInclusion(String s1, String s2) {
int l1=s1.length();//s1的长度
if(l1>s2.length()) return false;//s1长度若大于s2,直接返回false
int []str1=new int[26];
int []str2=new int[26];//记录s1,s2各字母出现的次数
for(int i=0;i<l1;i++){ //初始统计s1,各字母出现的次数,s2,0~l1长度字母出现频次
str1[s1.charAt(i)-'a']++;
str2[s2.charAt(i)-'a']++;
}
for(int left=0,right=l1-1;right<s2.length()-1;){
if(Arrays.equals(str1,str2)) return true;
str2[s2.charAt(left++)-'a']--;//左窗口收缩时,去除的字母的频次应减一
str2[s2.charAt(++right)-'a']++;//右窗口扩张时,新增的字母的频次应加一
}
if(Arrays.equals(str1,str2)) return true; //"adc","dcda",对类似用例的最后边界判断
else return false;
}
}
17、字符串中的所有变位词
给定两个字符串 s 和 p,找到 s 中所有 p 的 变位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
变位词 指字母相同,但排列不同的字符串。
示例 1:
输入: s = “cbaebabacd”, p = “abc”
输出: [0,6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的变位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的变位词。
示例 2:
输入: s = “abab”, p = “ab”
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的变位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的变位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的变位词。
提示:
1 <= s.length, p.length <= 3 * 104
s 和 p 仅包含小写字母
//滑动窗口,O(n)o(1)
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int []s1=new int [26];
int []p1=new int [26];
List<Integer> ret=new ArrayList<Integer>();//记录返回结果
int k=0,left=0;
if(s.length()<p.length()) return ret;//长度小,不需要直接不满足
for(int i=0;i<p.length();i++){//统计p字母出现频次,s前p.length长度字母出现频次
p1[p.charAt(i)-'a']++;
s1[s.charAt(i)-'a']++;
}
for(int right=p.length()-1;right<s.length()-1;){
if(Arrays.equals(s1,p1))
ret.add(left);
s1[s.charAt(left++)-'a']--;
s1[s.charAt(++right)-'a']++;
}
if(Arrays.equals(s1,p1)) ret.add(left);
return ret;
}
}
18、不含重复字符的最长子字符串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长连续子字符串 的长度。
示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子字符串是 “abc”,所以其长度为 3。
示例 2:
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子字符串是 “b”,所以其长度为 1。
示例 3:
输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
示例 4:
输入: s = “”
输出: 0
提示:
0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成
初始思路
用位运算来解决,类似题7,但是只能处理s时字符串的情况,且类似以下测试不通过:
class Solution {
public int lengthOfLongestSubstring(String s) {
if(s.equals("")) return 0;//s为空时没有必要进行计算
if(s.equals(" ")) return 1;//s为空时没有必要进行计算
int temp=0,i=0;
int ret=1;//返回结果
int bitmask=0;//位标记,用32位int变量的低26位对应标记s所含有的字母,1表示含有
for(char c:s.toCharArray()){
temp=bitmask;//暂存bitmask
bitmask|=(1<<(c-'a'));//每取s一个字母转变成对应数字后都与先前的bitmask进行或运算
// System.out.println(bitmask);
if(temp==bitmask) {//若bitmask或运算之后不变,说明字串存在重复字母,应该重新计数
ret=i>ret?i:ret;//不重复字串长度较大时,更新ret
i=1;
bitmask=1<<(c-'a');
}
else i++;
}
ret=i>ret?i:ret;
return ret;
}
}
参考做法
滑动窗口+map
这道题同样是通过滑动窗口来解题,只不过这次的边界获取要通过哈希表来实现。
1、首先我们创建一个哈希表calc,并且初始化左边界left = 0,默认返回值ret = 0 下来我们从0开始遍历字符串
2、每当遍历到字符串中的一个字符时,首先需要判断该字符是否在哈希表calc中
如果该字符串没有在哈希表中,表示该字符不重复,无需移动左边界,将该字符串及对应下标加入哈希表中
如果该字符存在哈希表中,表示找到了重复的元素,此时我们需要移动左边界left1、若left小于哈希表中该字符对应的index下标,则移动至index + 1(因为index已经重复了,需要跳过)
2、若left大于哈希表中该字符对应的index下标,表示重复的内容在左边界以外,忽略即可 将当前字符串对应的下标更新哈希表中该字符串对应的下标每次更新左边界后,比较当前滑窗长度与返回值大小并更新返回值 最终返回ret即可。
//O(n),O(n)
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character,Integer> str=new HashMap<>();
int left=0;//滑动窗口左边界
int ret=0;
for(int i=0;i<s.length();i++){
if(str.get(s.charAt(i))!=null){ //当子串中出现重复字母时,窗口左边界left需要处理
int m=str.get(s.charAt(i));
left=left>m?left:m+1; //str中字母对应索引值大于左边界left时才需要移动left
}
str.put(s.charAt(i),i);
ret=ret>(i-left+1)?ret:(i-left+1);
}
return ret;
}
}
19、含有所有字符的最短字符串
给定两个字符串 s 和 t 。返回 s 中包含 t 的所有字符的最短子字符串。如果 s 中不存在符合条件的子字符串,则返回空字符串 “” 。
如果 s 中存在多个符合条件的子字符串,返回任意一个。
注意: 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
示例 1:
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
解释:最短子字符串 “BANC” 包含了字符串 t 的所有字符 ‘A’、‘B’、‘C’
示例 2:
输入:s = “a”, t = “a”
输出:“a”
示例 3:
输入:s = “a”, t = “aa”
输出:""
解释:t 中两个字符 ‘a’ 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。
提示:
1 <= s.length, t.length <= 105
s 和 t 由英文字母组成
解题思路:
滑动窗口+map;
在右窗口每次右移的过程中处理两件事:
1、记录有效字符个数,方便之后计算ret
2、判断left是否需要收缩
//O(n*m),O(n)
class Solution {
public String minWindow(String s, String t) {
if(s.length()<t.length()) return "";//母串长度小于子串,一定不符合
Map<Character,Integer> hs=new HashMap<>();//hs用作滑动窗口滑动过程中统计字母出现频次
Map<Character,Integer> ht=new HashMap<>(); //ht统计t各字母个数
int left=0,right=0;//滑动窗口左右边界
int cnt=0;//统计滑动窗口s中满足条件的有效字符个数
String ret=s;//返回结果
for(char c:s.toCharArray()){ //防止后面比较大小时出现空值报错,提前将s中各字母录入t,并设默认值为0
ht.put(c,ht.getOrDefault(c,0));
}
for(char c:t.toCharArray()){ //统计t各字母出现频次
ht.put(c,ht.getOrDefault(c,0)+1);
}
for(;right<s.length();right++){//每轮循环滑动窗口right右移扩张
char c=s.charAt(right);
hs.put(c,hs.getOrDefault(c,0)+1);
if(hs.get(s.charAt(right))<=ht.get(s.charAt(right))) cnt++;//统计滑动窗口s中满足条件的有效字符个数
while(left<right&&hs.get(s.charAt(left))>ht.get(s.charAt(left))){
hs.put(s.charAt(left),hs.getOrDefault(s.charAt(left),0)-1);
left++;
}
if(cnt==t.length()&&right-left+1<ret.length()){ //当有效字符长度等于子串t,且窗口大小变小时才更新ret
ret=s.substring(left,right+1);
}
}
return ret=cnt>=t.length()?ret:"";
}
}
20、有效的回文
给定一个字符串 s ,验证 s 是否是 回文串 ,只考虑字母和数字字符,可以忽略字母的大小写。
本题中,将空字符串定义为有效的 回文串 。
示例 1:
输入: s = “A man, a plan, a canal: Panama”
输出: true
解释:“amanaplanacanalpanama” 是回文串
示例 2:
输入: s = “race a car”
输出: false
解释:“raceacar” 不是回文串
提示:
1 <= s.length <= 2 * 105
字符串 s 由 ASCII 字符组成
解题思路:双指针
//双指针 O(n),O(1)
class Solution {
public boolean isPalindrome(String s) {
s=s.toLowerCase();//将字符串转换成小写
int left=0,right=s.length()-1;//双指针初始指向首尾
while(left<right){
if((isTrue(s.charAt(left))&&isTrue(s.charAt(right)))&&s.charAt(left)==s.charAt(right)){//都是字母或者数字,且左右相等
left++;
right--;
}
else if(!isTrue(s.charAt(left))&&isTrue(s.charAt(right))) left++;
else if(!isTrue(s.charAt(right))&&isTrue(s.charAt(left))) right--;
else if(!isTrue(s.charAt(left))&&!isTrue(s.charAt(right))) {left++;right--;}
else return false;
}
return true;
}
public boolean isTrue(char c){ //判断是否是数字或者字母
if((c>='a'&&c<='z')||(c>='A'&&c<='Z')||(c>='0'&&c<='9'))
return true;
else return false;
}
}
21、最多删除一个字符得到回文
给定一个非空字符串 s,请判断如果 最多 从字符串中删除一个字符能否得到一个回文字符串。
示例 1:
输入: s = “aba”
输出: true
示例 2:
输入: s = “abca”
输出: true
解释: 可以删除 “c” 字符 或者 “b” 字符
示例 3:
输入: s = “abc”
输出: false
提示:
1 <= s.length <= 105
s 由小写英文字母组成
//双指针,O(n),O(1)
class Solution {
public boolean validPalindrome(String s) {
int left=0,right=s.length()-1;
while(left<right){
if(s.charAt(left)==s.charAt(right)) {left++;right--;}
else return isPalindorme(s,left+1,right)||isPalindorme(s,left,right-1);
}
return true;
}
public boolean isPalindorme(String str,int left,int right){ //判断str是否为回文串
while(left<right){
if(str.charAt(left)==str.charAt(right)) { left++;right--;}
else return false;
}
return true;
}
}
22、 回文子字符串的个数
给定一个字符串 s ,请计算这个字符串中有多少个回文子字符串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:s = “abc”
输出:3
解释:三个回文子串: “a”, “b”, “c”
示例 2:
输入:s = “aaa”
输出:6
解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”
//双指针 暴力解答:o(n*2),o(1)
class Solution {
public int countSubstrings(String s) {
int left=0,right=s.length()-1;//双指针分别指向首尾
int ret=0;//回文子字符串个数
for(;left<s.length();left++){
for(right=left;right<s.length();right++){
if(isPalindorme(s,left,right)) ret++;
}
}
return ret;
}
public boolean isPalindorme(String str , int left,int right){ //判断字符串str区间【left,right】字串是否为回文串
while(left<right){
if(str.charAt(left)==str.charAt(right)){
left++;
right--;
}
else return false;
}
return true;
}
}
链表
关注一下链表的定义,链表与数组的区别
23、删除链表的倒数第 n 个结点
解题思路
遇到这种题多了其实就是公式,数组中有快慢指针,链表同样也可以创建快慢两个指针。 初始右指针先跑N,然后左指针在和右指针开始同步向前。
当右指针到达末尾时: left.next = left.next.next即可!
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
//O(n),o(n)
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode headNode=new ListNode(0);
headNode.next=head;//创建带头结点(初始为空)的链表,便于删除操作
ListNode firstNode=headNode,secondNode=headNode;
//第一个节点先前进n+1个结点
for(int i=0;i<n+1;i++){
firstNode=firstNode.next;
}
//当一个结点为空时,第二个结点就是倒数第n+1个结点
while(firstNode!=null){
firstNode=firstNode.next;
secondNode=secondNode.next;
}
secondNode.next=secondNode.next.next;//删除第n个节点
return headNode.next;//由于创造了辅助头结点,所以head.next才是链表的头结点
}
}
24、链表中环的入口节点
给定一个链表,返回链表开始入环的第一个节点。 从链表的头节点开始沿着 next 指针进入环的第一个节点为环的入口节点。如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
提示:
链表中节点的数目范围在范围 [0, 104] 内
-105 <= Node.val <= 105
pos 的值为 -1 或者链表中的一个有效索引
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
//快慢双指针,o(n),o(1)
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast=head,slow=head;//设置一快一满两指针
while(fast!=null&&fast.next!=null){
fast=fast.next.next; //slow指针每走一步,fast指针走两步
slow=slow.next;
if(fast==slow){//当两指针相遇说明存在环路
ListNode ptr=head;
while(slow!=ptr){//找环路入口,设置ptr指向头结点,由推导公式知道ptr,slow一起再走x步就相遇
slow=slow.next;
ptr=ptr.next;
}
return ptr;
}
}
return null;
}
}
复杂度分析
时间复杂度:O(N),其中 N 为链表中节点的数目。在最初判断快慢指针是否相遇时,slow
指针走过的距离不会超过链表的总长度;随后寻找入环点时,走过的距离也不会超过链表的总长度。因此,总的执行时间为 O(N)+O(N)=O(N)空间复杂度:O(1)。我们只使用了 slow,fast,ptr 三个指针。
25、反转链表
迭代处理
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
//迭代O(n) O(1)
class Solution {
public ListNode reverseList(ListNode head) {
ListNode curr=head;//当前结点
ListNode prev=null;//前一个结点
//头插法
while(curr!=null){
ListNode next=curr.next;//事先存储下一个结点,防止链断裂找不到
curr.next=prev;//指针翻转
//prev ,curr前移
prev=curr;
curr=next;
}
return prev;
}
}
递归
//递归 O(n),o(n)
class Solution{
public ListNode reverseList(ListNode head) {
if(head==null||head.next==null){//递归终止条件
return head;
}
ListNode p = reverseList(head.next);//递,分解问题
//归,处理问题
head.next.next=head;
head.next=null;
return p;
}
}
26、链表的中间结点
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
//快慢指针找中间结点,O(n),o(1)
class Solution {
public ListNode middleNode(ListNode head) {
ListNode fast=head;
ListNode slow=head;
while(fast!=null&&fast.next!=null){//慢指针走一步,快指针走两步
slow=slow.next;
fast=fast.next.next;
}
return slow;
}
}
27、链表中的两数相加
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
//先反转,后相加O(max(m,n)),O(max(m,n))
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode p1=reverseList(l1);
ListNode p2=reverseList(l2);
ListNode ret=new ListNode();//创建头结点为空的链表储存返回结果
ListNode p=ret; //定义p指针
int carry=0;//进位数
//低位对齐相加,类似之前的题目字符串相加
while(p1!=null||p2!=null){
int sum=(p1.val+p2.val+carry)%10;
carry=(p1.val+p2.val+carry)/10;
//链表ret末尾加入新元素
ListNode temp=new ListNode(sum);
p.next=temp;
p=temp;
//p1,p2后移,不够的位用0补齐
if(p1.next==null&&p2.next!=null) { p1=add(p1);p2=p2.next;}
else if(p2.next==null&&p1.next!=null) { p2=add(p2);p1=p1.next;}
else {p1=p1.next;p2=p2.next;}
}
if(carry==1) {
ListNode temp=new ListNode(carry);
p.next=temp;
p=temp;
}
return reverseList(ret.next);
}
//翻转链表
public ListNode reverseList(ListNode head){
ListNode curr=head;//当前结点
ListNode prev=null;//前一个结点
while(curr!=null){
ListNode next=curr.next;//暂存下一个结点,防止断链
curr.next=prev;//链箭头翻转
//prev,curr前移
prev=curr;
curr=next;
}
return prev;
}
//尾部添加一个值为0的结点,且指针后移一位
public ListNode add(ListNode n){
ListNode m=new ListNode(0);
n.next=m;
n=m;
return n;
}
}
28、重排链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
//O(n),O(n)
class Solution {
public void reorderList(ListNode head) {
if(head==null||head.next==null) return ;
ListNode p=findMidle(head);//找到中间结点
p=reverseList(p);//将链表后半部分翻转
ListNode fro=head,beh=p;
while(beh!=null&&beh.next!=null){ //交替拼接前后两部分
ListNode nextFro=fro.next , nextBeh=beh.next;//记录后续节点
fro.next=beh;//连接
beh.next=nextFro;
fro=nextFro;//后移
beh=nextBeh;
}
}
//快慢指针寻找中间结点
public ListNode findMidle(ListNode head){
ListNode fast=head;
ListNode slow=head;
// System.out.println(head.val);
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
}
return slow;
}
//迭代法翻转链表
public ListNode reverseList(ListNode head){
ListNode curr=head,pre=null;
while(curr!=null){
ListNode next=curr.next;
curr.next=pre;
pre=curr;
curr=next;
}
return pre;
}
}
29、回文链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
//O(n),O(1)
class Solution {
public boolean isPalindrome(ListNode head) {
ListNode frop=head;
ListNode behp=findMiddle(head);//找到中间结点
behp=reversList(behp);//将链表后半部分翻转
while(behp!=null&&behp.val==frop.val){//比较链表前半部分与后半部分值是否相等
behp=behp.next;
frop=frop.next;
}
return behp==null?true:false;
}
//快慢指针找中间结点
public ListNode findMiddle(ListNode head){
ListNode fast=head,slow=head;
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
}
return slow;
}
//将链表翻转
public ListNode reversList(ListNode head){
ListNode curr=head,pre=null;
while(curr!=null){
ListNode next=curr.next;
curr.next=pre;
pre=curr;
curr=next;
}
return pre;
}
}
30、排序的循环链表
给定循环单调非递减列表中的一个点,写一个函数向这个列表中插入一个新元素 insertVal ,使这个列表仍然是循环升序的。
给定的可以是这个列表中任意一个顶点的指针,并不一定是这个列表中最小元素的指针。
如果有多个满足条件的插入位置,可以选择任意一个位置插入新的值,插入后整个列表仍然保持有序。
如果列表为空(给定的节点是 null),需要创建一个循环有序列表并返回这个节点。否则。请返回原先给定的节点。
示例 2:
输入:head = [], insertVal = 1
输出:[1]
解释:列表为空(给定的节点是 null),创建一个循环有序列表并返回这个节点。
示例 3:
输入:head = [1], insertVal = 0
输出:[1,0]
提示:
0 <= Number of Nodes <= 5 * 10^4
-10^6 <= Node.val <= 10^6
-10^6 <= insertVal <= 10^6
/*
// Definition for a Node.
class Node {
public int val;
public Node next;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, Node _next) {
val = _val;
next = _next;
}
};
*/
class Solution {
public Node insert(Node head, int insertVal) {
//列表为空(给定的节点是 null),需要创建一个循环有序列表并返回这个节点。
if(head==null){
Node p=new Node(insertVal);
head=p;
head.next=head;
return head;
}
Node curr=head;//指向当前结点
Node next=curr.next;//指向下一个结点
Node max=new Node(0),maxNext=new Node(0);//最大值结点、最大值下一个结点
/*
1、curr.val<插入值<next.val时插入;
2、插入值不满足情况1时,插入值为最大值或者最小值,一定插入在原链表最大值后面
*/
do{
if(curr.val>=max.val) {max=curr;maxNext=curr.next; }//记录原链表最大值,便于情况2处理
if(insertVal>=curr.val&&insertVal<=next.val){//情况1
Node tem=new Node(0);
tem.val=insertVal;
curr.next=tem;
tem.next=next;
return head;
}
else{
curr=curr.next;
next=next.next;
}
}while(curr!=head);
//处理情况2
Node tem=new Node(0);
tem.val=insertVal;
max.next=tem;
tem.next=maxNext;
return head;
}
}
字符串数组
31、变位词组
给定一个字符串数组 strs ,将 变位词 组合在一起。 可以按任意顺序返回结果列表。
注意:若两个字符串中每个字符出现的次数都相同,则称它们互为变位词。
示例 1:
输入: strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]
输出: [[“bat”],[“nat”,“tan”],[“ate”,“eat”,“tea”]]
示例 2:
输入: strs = [""]
输出: [[""]]
示例 3:
输入: strs = [“a”]
输出: [[“a”]]
提示:
1 <= strs.length <= 104
0 <= strs[i].length <= 100
strs[i] 仅包含小写字母
解题思路
//O(n),O(n)
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
HashMap<String,ArrayList<String>> map=new HashMap<>();
for(String str:strs){
char []tem=str.toCharArray();
Arrays.sort(tem); //将strs中的字符串排序
String key=new String(tem);//排序后作为键值
ArrayList list=map.getOrDefault(key,new ArrayList<String>());//查看map中是否有相同key,有则赋给list,无则创建新的list
list.add(str);
map.put(key, list);
}
return new ArrayList<List<String>>(map.values());
}
}
32、外星语言是否排序
某种外星语也使用英文小写字母,但可能顺序 order 不同。字母表的顺序(order)是一些小写字母的排列。
给定一组用外星语书写的单词 words,以及其字母表的顺序 order,只有当给定的单词在这种外星语中按字典序排列时,返回 true;否则,返回 false。
示例 1:
输入:words = [“hello”,“leetcode”], order = “hlabcdefgijkmnopqrstuvwxyz”
输出:true
解释:在该语言的字母表中,‘h’ 位于 ‘l’ 之前,所以单词序列是按字典序排列的。
示例 2:
输入:words = [“word”,“world”,“row”], order = “worldabcefghijkmnpqstuvxyz”
输出:false
解释:在该语言的字母表中,‘d’ 位于 ‘l’ 之后,那么 words[0] > words[1],因此单词序列不是按字典序排列的。
示例 3:
输入:words = [“apple”,“app”], order = “abcdefghijklmnopqrstuvwxyz”
输出:false
解释:当前三个字符 “app” 匹配时,第二个字符串相对短一些,然后根据词典编纂规则 “apple” > “app”,因为 ‘l’ > ‘∅’,其中 ‘∅’ 是空白字符,定义为比任何其他字符都小(更多信息)。
提示:
1 <= words.length <= 100
1 <= words[i].length <= 20
order.length == 26
在 words[i] 和 order 中的所有字符都是英文小写字母。
//O(n*2),O(n)
class Solution {
public boolean isAlienSorted(String[] words, String order) {
HashMap<Character,Integer> map=new HashMap<>();
for(int i=0;i<order.length();i++){//将order中的字母存入map中,key为字母,value为对应字母在order中的顺序
map.put(order.charAt(i),i);
}
for(int i=0;i<words.length-1;i++){
String strL=words[i],strR=words[i+1];//取words一前一后两个字母
int left=strL.length(),right=strR.length();
for(int j=0;j<Math.max(left,right);j++){//比较两个字母在字典order中的排序
int leftChar=j>=left?-1:map.get(strL.charAt(j));
int rightChar=j>=right?-1:map.get(strR.charAt(j));
if(leftChar<rightChar) break;
if(leftChar>rightChar) return false;//
}
}
return true;
}
}
环形队列最小间距
解题思路
先排序,后计算两两之间差值,注意,最大值与最小值差值之间要特殊处理
33、最小时间差
给定一个 24 小时制(小时:分钟 “HH:MM”)的时间列表,找出列表中任意两个时间的最小时间差并以分钟数表示。
示例 1:
输入:timePoints = [“23:59”,“00:00”]
输出:1
示例 2:
输入:timePoints = [“00:00”,“23:59”,“00:00”]
输出:0
提示:
2 <= timePoints <= 2 * 104
timePoints[i] 格式为 “HH:MM”
解题思路
//O(n),O(n)
class Solution {
public int findMinDifference(List<String> timePoints) {
int len=timePoints.size();//长度
if(len>1440) return 0;
List<Integer> listInt=new ArrayList<Integer>();
for(int i=0;i<len;i++){//转换成分钟数存入listInt
String num=timePoints.get(i);
listInt.add(Integer.parseInt(num.substring(0,2))*60+Integer.parseInt(num.substring(3,5)));
}
//冒泡法排序(升序)
for(int i=0;i<len;i++){
for(int j=len-1;j>i;j--){
if(listInt.get(j)<listInt.get(j-1)){
int tem=listInt.get(j);
listInt.set(j,listInt.get(j-1));
listInt.set(j-1,tem);
}
}
}
// Collections.sort(listInt);//内置优化排序减小时间复杂度
int d_val=1440-(listInt.get(len-1)-listInt.get(0));//因为是环形结构求最小差值,所以要考虑最大值与最小值之间差值
for(int i=0;i<len-1;i++){//计算最小差值
int tem=listInt.get(i+1)-listInt.get(i);
if(tem<d_val) d_val=tem;
}
return d_val;
}
}
栈
34、 后缀表达式
根据 逆波兰表示法,求该后缀表达式的计算结果。
有效的算符包括 +、-、、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
整数除法只保留整数部分。
给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
示例 1:
输入:tokens = [“2”,“1”,"+",“3”,""]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入:tokens = [“4”,“13”,“5”,"/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
输入:tokens = [“10”,“6”,“9”,“3”,"+","-11","","/","",“17”,"+",“5”,"+"]
输出:22
解释:
该算式转化为常见的中缀算术表达式为:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
提示:
1 <= tokens.length <= 104
tokens[i] 要么是一个算符("+"、"-"、"*" 或 “/”),要么是一个在范围 [-200, 200] 内的整数
解题思路
利用栈的FILO原则,若碰到数字字符就压进栈,若碰到运算符字符就把栈最上面两个数字元素做运算以后的结果压进栈,最后返回栈中剩下的数字就是结果。
//O(n),O(n)
class Solution {
public int evalRPN(String [] tokens) {
Stack<Integer> stackNum=new Stack<>();//数字栈,Java自带栈的数据结构
String []sign={"+","-","*","/"};
for(int i=0;i<tokens.length;i++){
if(!search(sign,tokens[i])) stackNum.push(Integer.parseInt(tokens[i]));//若是数字,则入栈
else{
stackNum.push(calc(tokens[i],stackNum.pop(),stackNum.pop()));//若是符号,则弹出栈顶两个元素,计算后入栈
}
}
return stackNum.pop();
}
public boolean search(String []strs,String target){ //判断目标字符串是否为符号
for(int i=0;i<strs.length;i++){
if(strs[i].equals(target)) return true;
}
return false;
}
public int calc(String op,int n,int m){//将字符串转换成对应运算符
switch(op){
case "+": return m+n;
case "-": return m-n;
case "*": return m*n;
case "/": return m/n;
}
return 1;
}
}
35、小行星碰撞
给定一个整数数组 asteroids,表示在同一行的小行星。
对于数组中的每一个元素,其绝对值表示小行星的大小,正负表示小行星的移动方向(正表示向右移动,负表示向左移动)。每一颗小行星以相同的速度移动。
找出碰撞后剩下的所有小行星。碰撞规则:两个行星相互碰撞,较小的行星会爆炸。如果两颗行星大小相同,则两颗行星都会爆炸。两颗移动方向相同的行星,永远不会发生碰撞。
示例 1:
输入:asteroids = [5,10,-5]
输出:[5,10]
解释:10 和 -5 碰撞后只剩下 10 。 5 和 10 永远不会发生碰撞。
示例 2:
输入:asteroids = [8,-8]
输出:[]
解释:8 和 -8 碰撞后,两者都发生爆炸。
示例 3:
输入:asteroids = [10,2,-5]
输出:[10]
解释:2 和 -5 发生碰撞后剩下 -5 。10 和 -5 发生碰撞后剩下 10 。
示例 4:
输入:asteroids = [-2,-1,1,2]
输出:[-2,-1,1,2]
解释:-2 和 -1 向左移动,而 1 和 2 向右移动。 由于移动方向相同的行星不会发生碰撞,所以最终没有行星发生碰撞。
提示:
2 <= asteroids.length <= 104
-1000 <= asteroids[i] <= 1000
asteroids[i] != 0
解题思路
首先,循环每一个元素时,在什么情况下无脑入栈呢?
栈为空
栈顶元素为负数(下一个为负数则一起向左,下一个为正数则分向两边)
当前元素为正数(栈顶为正一起向右,栈顶为负分向两边)
下来,我们需要看碰撞的场景又细分为什么情况:
栈顶元素大于abs(当前元素),当前元素被撞毁
栈顶元素等于abs(当前元素),栈顶弹出和当前元素抵消
栈顶元素小于abs(当前元素),栈顶弹出,并与新栈顶完成上述判断
//O(n),O(n)
class Solution {
public int[] asteroidCollision(int[] asteroids) {
Stack<Integer> stack=new Stack<>();//数据栈用来存取返回结果
stack.push(asteroids[0]);
for(int i=1;i<asteroids.length;){
if(stack.empty()) {//每次比较前先判断栈中元素是否为空,若空,则取数组元素入栈
stack.push(asteroids[i]);i++;
}
if(i>=asteroids.length) break;
if(stack.peek()>0&&asteroids[i]<0){//只有栈顶元素为正数,数组下一个处理的元素为负数时才会发生碰撞
int sum=stack.peek()+asteroids[i];
System.out.println(sum);
if(sum>0) i++;
else if(sum==0){i++;stack.pop();}//正负数绝对值相等,碰撞后互相销毁
else { stack.pop();}//负数绝对值大,碰撞后,栈中正数销毁,负数接着和栈顶元素比
}
else {stack.push(asteroids[i]);i++;}
}
int i=stack.size();
int []ret=new int [i];
while(!stack.empty()){
ret[i-1]=stack.pop();
i--;
}
return ret;
}
}