242. 有效的字母异位词(Easy)
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1:
输出: true
示例 2:
输出: false
说明:
- 你可以假设字符串只包含小写字母。
进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
题解
这道题可以使用HashMap来存储两个字符串中字符出现的次数,然后比较即可,字符串只包含小写英文的话可以使用数组来存储出现次数
class Solution {
public boolean isAnagram(String s, String t) {
int[] counts = new int[26];
//遍历s,统计每个字符出现的次数
for (char c : s.toCharArray()) {
counts[c - 'a']++;
}
//遍历t,统计出现次数,出现一次就减一
for (char c : t.toCharArray()) {
counts[c - 'a']--;
}
//遍历counts,如果全为0,则相等
for (int count : counts) {
if (count != 0) {
return false;
}
}
return true;
}
}
409. 最长回文串(Easy)
给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。 在构造过程中,请注意区分大小写。比如 "Aa" 不能当做一个回文字符串。
注意: 假设字符串的长度不会超过 1010。
示例 1:
"abccccdd"
输出:
7
解释: 我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。
题解
回文串是一个对称的结构,中间的那个字符可以只有一个,其他的得是偶数才行,使用长度为 256 的整型数组来统计每个字符出现的个数,每个字符有偶数个可以用来构成回文字符串。因为回文字符串最中间的那个字符可以单独出现,所以如果有单独的字符就把它放到最中间。
class Solution {
public int longestPalindrome(String s) {
int[] cnts = new int[256];
for (char c : s.toCharArray()) {
cnts[c]++;
}
int palindrome = 0;
for (int cnt : cnts) {
//把奇数转换成偶数个
palindrome += (cnt / 2) * 2;
}
if (palindrome < s.length()) {
palindrome++; // 这个条件下 s 中一定有单个未使用的字符存在,可以把这个字符放到回文的最中间
}
return palindrome;
}
}
205. 同构字符串
给定两个字符串 s 和 t,判断它们是否是同构的。如果 s 中的字符可以被替换得到 t ,那么这两个字符串是同构的。
所有出现的字符都必须用另一个字符替换,同时保留字符的顺序。两个字符不能映射到同一个字符上,但字符可以映射自己本身。
示例 1:
输出: true
示例 2:
输出: false
示例 3:
输出: true
说明: 你可以假设 s 和 t 具有相同的长度。
解法一
“特征值”解法,两个字符同构,那么对应位置的字符出现的位置相同,比如上面的示例3,paper
可以表示为12134
,title
也可以表示为12134
,只需要比较最后的特征值是否相同即可。
class Solution {
public boolean isIsomorphic(String s, String t) {
//准备两个数组,记录每个字符的特征值
int[] schar = new int[256];
int[] tchar = new int[256];
StringBuilder sStr = new StringBuilder();
StringBuilder tStr = new StringBuilder();
int sCount = 0;
int tCount = 0;
for (int i = 0; i < s.length(); i++) {
//如果当前字符没出现过,sCount++,再存储进去,然后添加进sStr
if (schar[s.charAt(i)] == 0) {
schar[s.charAt(i)] = ++sCount;
sStr.append(schar[s.charAt(i)]);
} else {
sStr.append(schar[s.charAt(i)]);
}
if (tchar[t.charAt(i)] == 0) {
tchar[t.charAt(i)] = ++tCount;
tStr.append(tchar[t.charAt(i)]);
} else {
tStr.append(tchar[t.charAt(i)]);
}
}
return sStr.toString().equals(tStr.toString());
}
}
解法二
记录一个字符上次出现的位置,如果两个字符串中的字符上次出现的位置一样,那么就属于同构。这种解法和上种解法有着异曲同工之妙,都是一样的思想,关键点都是相对应的字符,特征值得相同,这个解法的特征值是当前字符前面是否出现过
public boolean isIsomorphic(String s, String t) {
int[] preIndexOfS = new int[256];
int[] preIndexOfT = new int[256];
for (int i = 0; i < s.length(); i++) {
char sc = s.charAt(i), tc = t.charAt(i);
if (preIndexOfS[sc] != preIndexOfT[tc]) {
return false;
}
//需要更新为i+1,如果不更新的话就无法判断当前字符前面是否出现过,比如“ab”和“aa”
preIndexOfS[sc] = i + 1;
preIndexOfT[tc] = i + 1;
}
return true;
}
647. 回文子串(Medium)
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被计为是不同的子串。
示例 1:
输入: "abc"
输出: 3
解释: 三个回文子串: "a", "b", "c".
示例 2:
输入: "aaa"
输出: 6
说明: 6个回文子串: "a", "a", "a", "aa", "aa", "aaa".
注意: 输入的字符串长度不会超过1000。
解法一
很容易就想到使用暴力解法来解题,题目要求出回文串的数量,那么就需要判断出区间[i, j]
里的子串是不是回文子串,遍历所有的子串,统计出结果。步骤:
- 定义一个判断区间
[i, j]
是不是回文子串的函数 - 遍历所有的区间
class Solution {
public int countSubstrings(String s) {
char[] chars = s.toCharArray();
int result = 0;
//外层的循环控制区间的start
for (int i = 0; i < s.length(); i++) {
//内层控制end,单个字符本身就是一个回文子串,所以end取i+1,最后再加上字符串的长度
for (int j = i + 1; j < s.length() && j < s.length(); j++)
if (isPalindrome(chars, i, j))
result++;
}
return result + s.length();
}
//判断是否为回文子串
private boolean isPalindrome(char[] chars, int start, int end) {
for (int i = start, j = end; j > i; i++, j--) {
if (chars[i] != chars[j])
return false;
}
return true;
}
}
解法二:动态规划。
解法一有很多的重复,且本题问回文子串数量,因此可以考虑使用动态规划来进行优化。一个子串是回文串有两种情况。
- 只有一个字符
- 去掉首尾两个字符之后仍然是回文串,所以转移方程可得:
flag[i][j] = true if (chars[i] == chars[j] && (j - i < 2 || flag[i + 1][j - 1]))
public int countSubstrings2(String s) {
char[] chars = s.toCharArray();
int result = 0;
boolean[][] flag = new boolean[chars.length][chars.length];
//控制右边界
for (int j = 0; j < chars.length; j++) {
//控制左边界
for (int i = j; i >= 0; i--) {
if (chars[i] == chars[j] && (j - i < 2) || flag[i + 1] == flag[j - 1]) {
flag[i][j] = true;
result++;
}
}
}
return result;
}
解法三:中心扩展
以字符串中的每一个字符都当作回文串中间的位置,然后向两边扩散,每当成功匹配两个左右两个字符,结果 res 自增1,然后再比较下一对。注意回文字符串有奇数和偶数两种形式,如果是奇数长度,那么中心位置就是中间那个字符的位置,所以左右两遍都从i开始遍历;如果是偶数长度的,那么中心是最中间两个字符的左边那个,右边那个就是 中心+1,这样就能 cover 所有的情况,而且都是不同的回文子字符串
public int countSubstrings3(String s) {
int res = 0;
//遍历所有的中心点
int n = s.length();
for (int i = 0; i < n; i++) {
//子串长度为奇数
helper(s, i, i, res);
//子串长度为偶数
helper(s, i, i + 1, res);
}
return res;
}
private void helper(String s, int i, int j, int res) {
while (i >= 0 && j < s.length() && s.charAt(i) == s.charAt(j)) {
//往外不断扩展
i--;
j++;
res++;
}
}
696. 计数二进制子串(Easy)
给定一个字符串 s,计算具有相同数量0和1的非空(连续)子字符串的数量,并且这些子字符串中的所有0和所有1都是组合在一起的。 重复出现的子串要计算它们出现的次数。
示例 1 :
输出: 6
解释: 有6个子串具有相同数量的连续1和0:“0011”,“01”,“1100”,“10”,“0011” 和 “01”。
请注意,一些重复出现的子串要计算它们出现的次数。
另外,“00110011”不是有效的子串,因为所有的0(和1)没有组合在一起。
示例 2 :
输入: "10101"
输出: 4
解释: 有4个子串:“10”,“01”,“10”,“01”,它们具有相同数量的连续1和0。
注意:
s.length 在1到50,000之间。
s 只包含“0”或“1”字符。
暴力解法
这种解法和上题的暴力解法类似,都是准备一个函数验证某个子串是不是有效的,然后遍历所有子串。这种方法会超时。
public int countBinarySubstrings(String s) {
int result = 0;
if (s.length() < 2) {
return result;
}
//遍历所有子串,外循环是子串长度
for (int i = 2; i <= s.length(); i += 2) {
//内循环是子串开始的下标
for (int j = 0; j + i - 1 < s.length(); j++) {
if (helper(s.toCharArray(), j, j + i - 1)) {
result++;
}
}
}
return result;
}
//判断是否为有效子串
public boolean helper(char[] chars, int i, int j) {
boolean flag = false;
//左右两边的字符相等,肯定不是有效子串
char left = chars[i];
char right = chars[j];
if (left == right) {
return flag;
}
int lCount = 0;
int mCount = (j - i + 1) / 2;
//统计左边连续相等字符的个数
int index = i;
while (index < j) {
if (chars[index++] == left) {
lCount++;
} else {
break;
}
}
//左边数量==右边数量,说明是有效子串
if (lCount == mCount) {
flag = true;
}
return flag;
}
往外扩展
和上题的“中心扩展”类似,找到“01”和“10”字符串往外扩展
private int count = 0;
public int countBinarySubstrings(String s) {
//遍历开始点
for (int i = 1; i < s.length(); i++) {
//出现“01”的情况
if (s.charAt(i - 1) == '0' && s.charAt(i) == '1') {
BinarySubstring(s, i-1, i);
}
// 出现 ‘10’ 的情况
if(s.charAt(i-1) == '1' && s.charAt(i) == '0') {
BinarySubstring(s, i-1, i);
}
}
return count;
}
private void BinarySubstring(String s, int start, int end) {
char f = s.charAt(start);
char e = s.charAt(end);
//往外扩展
while (start >= 0 && end < s.length() && s.charAt(start) == f && s.charAt(end) == e) {
start++;
end--;
count++;
}
}
分组计数
- 给定的字符串s是由连续的多组
0
和1
构成的,0
和1
的组是交替出现的,假设有4个1
与3个0
相连,1111000
可以构成的子串有111000
,1100
,10
,共3个;假设有3个0与2个1相连00011
,可以构成的子串有0011
,01
,共两个。 - 由此可以发现,两个相邻的
0
和1
组,可以构成的有效子串是比较短的那组的长度。 - 所以只需要通过一次遍历,记录相连两分组的长度即可。preCount表示前一个分组的长度,curCount表示当前分组的长度。
public int countBinarySubstrings(String s) {
int result = 0;
char[] chars = s.toCharArray();
//准备两个变量,curCount=1是因为当前元素就是当前组的第一个元素
int preCount = 0;
int curCount = 1;
for (int i = 1; i < chars.length; i++) {
//前一个元素和当前元素相等
if (chars[i - 1] == chars[i]) {
curCount++;
} else {
//当前组的数量已经统计完,计算有效子串的数量
result += Math.min(preCount, curCount);
//更新
preCount = curCount;
curCount = 1;
}
}
//循环结束后还有最后一组要和前一组计数
return result + Math.min(preCount, curCount);
}
9. 回文数(Easy)
判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
示例 1:
输入: 121
输出: true
示例 2:
输入: -121
输出: false
解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
示例 3:
输入: 10
输出: false
解释: 从右向左读, 为 01 。因此它不是一个回文数。
进阶:
你能不将整数转为字符串来解决这个问题吗?
转换为字符串
- 负数不是回文数
- 转换为字符串之后采用双指针的方式来逐个比较首尾
public boolean isPalindrome(int x) {
if (x < 0) {
return false;
}
boolean flag = true;
//转换为字符串
String s = Integer.toString(x);
int start = 0;
int end = s.length() - 1;
//首尾逐个比较
while (start < end) {
if (s.charAt(start) == s.charAt(end)) {
start++;
end--;
} else {
flag = false;
break;
}
}
return flag;
}
反转数字
- 采用和反转链表类似的方法,将后半段数字反转。同样的,负数不能为回文数字
- 个位为0但是本身不为0的数字(
x % 10 == 0 && x != 0
)不为回文数字,因为除了0没有任何数的最高位为0
public boolean isPalindrome(int x) {
//x为负数,或者x的个位为0且不为0,如果x不等于0,那么其他数的高位不可能为0
if (x < 0 || (x % 10 == 0 && x != 0)) {
return false;
}
int halfReverse = 0;
//如果x < halfReverse,说明已经反转了一半的数字
while (x > halfReverse) {
halfReverse = halfReverse * 10 + x % 10;
x /= 10;
}
//x长度为奇数时,可以通过halfReverse / 10的方式去除中间的那个数
return x == halfReverse || x == halfReverse / 10;
}