字符
520.检测大写字母(简单)
我们定义,在以下情况时,单词的大写用法是正确的:
全部字母都是大写,比如 "USA" 。
单词中所有字母都不是大写,比如 "leetcode" 。
如果单词不只含有一个字母,只有首字母大写, 比如 "Google" 。
给你一个字符串 word 。如果大写用法正确,返回 true ;否则,返回 false 。
class Solution {
//整理规则
//不管首字母是大小写,其余的字母需要与第二个字母相同
//若首字母小写,需要额外判断第二个字母为小写
public boolean detectCapitalUse(String word) {
int n = word.length();
if(n == 1) return true;
if(Character.isLowerCase(word.charAt(0)) && Character.isUpperCase(word.charAt(1))){
return false;
}
//判断其他字母与第二个字母相同
for(int i = 1; i < n; i++){
if(Character.isLowerCase(word.charAt(1)) ^ Character.isLowerCase(word.charAt(i))){
return false;
}
}
return true;
}
}
回文串的定义
125.验证回文串(简单)
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
输入: "A man, a plan, a canal: Panama"
输出: true
解释: "amanaplanacanalpanama" 是回文串
class Solution {
//使用双指针法,分别指向头和尾,逐个对比
//注意Character的各种方法
public boolean isPalindrome(String s) {
int len = s.length();
int left = 0;
int right = len - 1;
while(left < right){
while(left < right && (!Character.isLetterOrDigit(s.charAt(left)))){
left++;
}
while(left < right && (!Character.isLetterOrDigit(s.charAt(right)))){
right--;
}
if(left < right){
if(Character.toLowerCase(s.charAt(left)) != Character.toLowerCase(s.charAt(right))){
return false;
}
left++;
right--;
}
}
return true;
}
}
//也可以过滤一遍所有元素,把有效字符放在一个StringBuffer中
//使用reverse()将其逆序,字符串与逆序字符串相等时即为回文串
公共前缀
14.最长公共前缀(简单)
写一个函数来查找字符串数组中的最长公共前缀。若不存在,返回空字符串 ""。
输入: strs = ["flower","flow","flight"]
输出: "fl"
class Solution {
//找到最短字符串,然后对数组中的字符串逐一比较
public String longestCommonPrefix(String[] strs) {
int len = strs.length;
int minLen = strs[0].length();
String minStr = strs[0];
for(int i = 1; i < len; i++){ //找到最短的字符串
if(strs[i].length() < minLen){
minLen = strs[i].length();
minStr = strs[i];
}
}
for(int i = 0; i < minLen; i++){ //遍历最短字符串的每一个字符
char ch = minStr.charAt(i);
for(int j = 0; j < len; j++){ //遍历数组中所有字符串
if(strs[j].charAt(i) != ch){
return minStr.substring(0, i);
}
}
}
return minStr;
}
}
class Solution {
//逐个字符串比较,更新最长公共前缀
public String longestCommonPrefix(String[] strs) {
int len = strs.length;
if(len == 0) return "";
if(len == 1) return strs[0];
String prefix = strs[0];
for(int i = 1; i < len; i++){
prefix = longestCommonPrefix(prefix, strs[i]); //每次比较两个字符串时更新prefix
if(prefix == ""){
break;
}
}
return prefix;
}
public String longestCommonPrefix(String s1, String s2){
int n = Math.min(s1.length(), s2.length());
int index = 0;
while(index < n && s1.charAt(index) == s2.charAt(index)){
index++;
}
return s1.substring(0, index);
}
}
单词
434.字符串中的单词数(简单)
统计字符串中的单词个数,这里的单词指的是连续的不是空格的字符。
class Solution {
//当前字符不是字母,但前一个字符是字母时,算作一个单词(第一个字母另外判断)
//但是题目里面的意思是,前一个字符不是空格,后一个字符是空格就可以算作一个单词
public int countSegments(String s) {
int count = 0;
s = s.trim(); //去除字符串前后空格
for(int i = 0; i < s.length(); i++){
if(i == 0 || (s.charAt(i-1) != ' ' && s.charAt(i) == ' ')){
count++;
}
}
return count;
}
}
58.最后一个单词的长度(简单)
给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。s仅由字母和空格构成。
class Solution {
//从后往前遍历,找到第一个字母与非字母的分界处
public int lengthOfLastWord(String s) {
s = s.trim();
int len = s.length();
int count = 0; //记录单词的长度
for(int i = len - 1; i >= 0; i--){
if(!Character.isLetter(s.charAt(i))){ //当遇到第一个非字母时
count = len - 1 - i;
break;
}else if(i == 0){ //遍历到首个字符了,还是字母
count = len;
}
}
return count;
}
}
字符串的反转
344.反转字符串(简单)
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。必须原地反转。
class Solution {
//使用双指针法,交换字符
public void reverseString(char[] s) {
int n = s.length;
int i = 0;
int j = n-1;
while(i < j){
char temp = s[i];
s[i] = s[j];
s[j] = temp;
i++;
j--;
}
}
}
541.反转字符串2(简单)
给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。
如果剩余字符少于 k 个,则将剩余字符全部反转。
如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
输入: s = "abcdefg", k = 2
输出: "bacdfeg"
class Solution {
//整理一下规则,对于字符串s分为k段,第一段反转,第二段不变...直到最后不够k个且属于反转段,全部反转
public String reverseStr(String s, int k) {
int len = s.length();
int count = len / k; //把字符串按照k分段
char[] ch = s.toCharArray(); //字符串转换为字符数组,方便操作
for(int i = 0; i < count; i++){ //对每段进行处理
if(i%2 == 1){ //奇数号的段不处理
continue;
}else{
reverse(ch, i*k, (i+1)*k-1);//偶数号的段反转
}
}
if((len % k) > 0 && (count % 2) == 0){ //当剩余字符少于k个时且属于反转段时,全部反转
reverse(ch, count * k, len-1);
}
return String.valueOf(ch);
}
public void reverse(char[] ch, int begin, int end){
while(begin < end){
char temp = ch[begin];
ch[begin] = ch[end];
ch[end] = temp;
begin++;
end--;
}
}
}
//简单写法
//每个反转下标从2k的倍数开始,长度为k的子串。若该子串长度不足k,则反转整个子串。
class Solution {
public String reverseStr(String s, int k) {
int n = s.length();
char[] arr = s.toCharArray();
for (int i = 0; i < n; i += 2 * k) {
reverse(arr, i, Math.min(i + k, n) - 1);
}
return new String(arr);
}
public void reverse(char[] arr, int left, int right) {
while (left < right) {
char temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
}
557.反转字符串中的单词3(简单)
给定一个字符串 s ,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。开头结尾不含空格,所有单词都用一个空格隔开。
class Solution {
//使用双指针法记录单词的开始和结束,逐个单词进行反转
public String reverseWords(String s) {
int len = s.length();
int begin = 0;
int end = 0;
char[] ch = s.toCharArray();
while(end < len){
if(ch[end] == ' '){
reverse(ch, begin, end-1);
begin = end+1;
}
if(end == len-1){
reverse(ch, begin, end);
}
end++;
}
return new String(ch);
}
public void reverse(char[] ch, int begin, int end){
while(begin < end){
char temp = ch[begin];
ch[begin] = ch[end];
ch[end] = temp;
begin++;
end--;
}
}
}
151.颠倒字符串中的单词(中等)
给你一个字符串 s ,颠倒字符串中单词的顺序。单词是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的单词分隔开。
返回单词顺序颠倒且单词之间用单个空格连接的结果字符串。
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
输入: s = "the sky is blue"
输出: "blue is sky the"
class Solution {
//从后往前遍历,记录单词首字母和长度,然后放到StringBuffer中
public String reverseWords(String s) {
s = s.trim();
int len = s.length();
int begin = len-1; //记录单词首字母
int count = 0; //记录单词长度
StringBuffer sb = new StringBuffer();
while(begin >= 0){
if(s.charAt(begin) == ' ' && s.charAt(begin+1) != ' '){//当前字符是空格且上一个字符不是空格
sb.append(s.substring(begin+1, begin+count+1) + " ");
count = 0;
}
if(begin == 0){ //当到达最后一个字符时
sb.append(s.substring(begin, begin+count+1));
}
if(s.charAt(begin) != ' '){ //当前位置不是空格时才累加单词长度
count++;
}
begin--;
}
return sb.toString();
}
}
class Solution {
//API解法,先trim,再split,然后reverse,最后join
public String reverseWords(String s) {
s = s.trim();
List<String> list = Arrays.asList(s.split("\\s+"));
Collections.reverse(list);
return String.join(" ", list);
}
}
字符的统计
387.字符串中第一个唯一字符(简单)★
给定一个字符串 s ,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1 。
class Solution {
//使用HashMap存储每个字符出现的次数,再遍历一次字符串,找到第一个只出现一次的字符
public int firstUniqChar(String s) {
int len = s.length();
Map<Character, Integer> map = new HashMap<>();
for(int i = 0; i < len; i++){
char c = s.charAt(i);
map.put(c, map.getOrDefault(c, 0)+1);
}
for(int i = 0; i < len; i++){
if(map.get(s.charAt(i)) == 1){
return i;
}
}
return -1;
}
}
//由于本题只需要知道出现一次的字符,故可以把重复出现的字符的hashmap对应的值改为-1
//只出现一次的值设为索引,这样后面只需要再遍历一次hashmap即可。
//由于字符都为字母,故可以使用一个26长度的数组作为存储字母出现频次的容器
389.找不同(简单)★
给定两个字符串 s 和 t ,它们只包含小写字母。 字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。 请找出在 t 中被添加的字母。
class Solution {
//由于只有小写字母,故使用一个26长度的数组作为map,存储元素与出现次数
public char findTheDifference(String s, String t) {
int[] arr = new int[26];
for(int i = 0; i < s.length(); i++){
arr[s.charAt(i) - 'a'] += 1;
}
for(int i = 0; i < t.length(); i++){
int j = t.charAt(i) - 'a';
arr[j] -= 1;
if(arr[j] == -1){
return (char)(97+j); //97为a的ASCII码
}
}
return ' ';
}
}
class Solution {
//s数组所有数之和减去t数组所有数之和即为所求
//由于t与s的长度只差1,故可以同时遍历t与s
public char findTheDifference(String s, String t) {
int res = 0;
for(int i = 0; i < s.length(); i++){
res += t.charAt(i) - s.charAt(i);
}
res += (int)t.charAt(t.length() - 1);
return (char)res;
}
}
class Solution {
//相当于求s+t这个字符串中出现次数为奇数次的数,使用异或
public char findTheDifference(String s, String t) {
int res = 0;
for(int i = 0; i < s.length(); i++){
res ^= s.charAt(i);
res ^= t.charAt(i);
}
res ^= t.charAt(t.length() - 1);
return (char)res;
}
}
383.赎金信(简单)
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。 如果可以,返回 true ;否则返回 false 。 magazine 中的每个字符只能在 ransomNote 中使用一次。
class Solution {
//使用数组记录magazine中的字符出现的次数,再遍历ransomNote,减掉对应的字符出现次数
public boolean canConstruct(String ransomNote, String magazine) {
int[] arr = new int[26];
for(int i = 0; i < magazine.length(); i++){
arr[magazine.charAt(i) - 'a'] += 1;
}
for(int i = 0; i < ransomNote.length(); i++){
int j = ransomNote.charAt(i) - 'a';
arr[j] -= 1;
if(arr[j] < 0){
return false;
}
}
return true;
}
}
242.有效的字母异位词(简单)
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
class Solution {
//本题不能用异或解决,因为可能出现"aa"和"bb"这样的情况
//可以先排序,排序后两个字符一致即可
public boolean isAnagram(String s, String t) {
if(s.length() != t.length()) return false;
char[] sch = s.toCharArray();
char[] tch = t.toCharArray();
Arrays.sort(sch);
Arrays.sort(tch);
return Arrays.equals(sch, tch);
}
}
49.字母异位词分组(中等)★
给你一个字符串数组,请你将字母异位词组合在一起。可以按任意顺序返回结果列表。
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
class Solution {
//使用Hashmap存储每组的标志和每个组,使用排序来判断是否是字母异位词
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for(String s : strs){
char[] ch = s.toCharArray();
Arrays.sort(ch);
String key = new String(ch); //获取标志为hashmap的key
List<String> list = map.getOrDefault(key, new ArrayList<String>());
list.add(s); //把当前字符串添加到对应list中
map.put(key, list); //更新key对应的list
}
return new ArrayList<List<String>>(map.values()); //获取map中所有的list并返回
}
}
451.根据字符出现频率排序(中等)
给定一个字符串 s ,根据字符出现的 频率 对其进行 降序排序 。一个字符出现的 频率 是它出现在字符串中的次数。注意大小写为不同字符,字符中有字母和数字
输入: s = "tree"
输出: "eert"
class Solution {
//先映射到hashmap,存储每个字符出现的次数,然后把key放到列表中,对列表进行排序
//注意这道题不直接对map进行排序,而是转化为对list排序的思路
public String frequencySort(String s) {
StringBuffer sb = new StringBuffer();
Map<Character, Integer> map = new HashMap<>();
for(int i = 0; i < s.length(); i++){
map.put(s.charAt(i), map.getOrDefault(s.charAt(i), 0) + 1);
}
List<Character> list = new ArrayList<>(map.keySet()); //list只存储字符
Collections.sort(list, (a,b) -> map.get(b) - map.get(a)); //对list按存储在map中的出现次数排序
int len = list.size();
for(int i = 0; i < len; i++){
char key = list.get(i);
for(int j = map.get(key); j > 0; j--){
sb.append(key);
}
}
return sb.toString();
}
}
423.从英文中重建数字(中等)
给你一个字符串 s ,其中包含字母顺序打乱的用英文单词表示的若干数字(0-9)。按 升序 返回原始的数字。
输入: s = "owoztneoer"
输出: "012"
class Solution {
//本题可以计算所有字母在哪些数字单词中出现,从字母出现的次数中推导出出现了什么数字(详细推理见leetcode题解)
public String originalDigits(String s) {
Map<Character, Integer> c = new HashMap<Character, Integer>();
for (int i = 0; i < s.length(); ++i) {
char ch = s.charAt(i);
c.put(ch, c.getOrDefault(ch, 0) + 1);
}
int[] cnt = new int[10];
cnt[0] = c.getOrDefault('z', 0);
cnt[2] = c.getOrDefault('w', 0);
cnt[4] = c.getOrDefault('u', 0);
cnt[6] = c.getOrDefault('x', 0);
cnt[8] = c.getOrDefault('g', 0);
cnt[3] = c.getOrDefault('h', 0) - cnt[8];
cnt[5] = c.getOrDefault('f', 0) - cnt[4];
cnt[7] = c.getOrDefault('s', 0) - cnt[6];
cnt[1] = c.getOrDefault('o', 0) - cnt[0] - cnt[2] - cnt[4];
cnt[9] = c.getOrDefault('i', 0) - cnt[5] - cnt[6] - cnt[8];
StringBuffer ans = new StringBuffer();
for (int i = 0; i < 10; ++i) {
for (int j = 0; j < cnt[i]; ++j) {
ans.append((char) (i + '0'));
}
}
return ans.toString();
}
}
657.机器人能否返回原点(简单)
在二维平面上,有一个机器人从原点 (0, 0) 开始。给出它的移动顺序,判断这个机器人在完成移动后是否在 (0, 0) 处结束。
移动顺序由字符串 moves 表示。字符 move[i] 表示其第 i 次移动。机器人的有效动作有 R(右),L(左),U(上)和 D(下)。移动方向与面朝的方向无关。
class Solution {
public boolean judgeCircle(String moves) {
int Vcount = 0;
int Hcount = 0;
for(int i = 0; i < moves.length(); i++){
char ch = moves.charAt(i);
switch(ch){
case 'U':
Vcount++;
break;
case 'D':
Vcount--;
break;
case 'L':
Hcount++;
break;
case 'R':
Hcount--;
break;
default:
break;
}
}
if(Vcount == 0 && Hcount == 0){
return true;
}
return false;
}
}
551.学生出勤记录1(简单)
给你一个字符串 s 表示一个学生的出勤记录,其中的每个字符用来标记当天的出勤情况(缺勤、迟到、到场)。记录中只含下面三种字符:
'A':Absent,缺勤
'L':Late,迟到
'P':Present,到场
如果学生能够 同时 满足下面两个条件,则可以获得出勤奖励:
按 总出勤 计,学生缺勤('A')严格 少于两天。
学生 不会 存在 连续 3 天或 连续 3 天以上的迟到('L')记录。
如果学生可以获得出勤奖励,返回 true ;否则,返回 false
class Solution {
public boolean checkRecord(String s) {
int Acount = 0; //记录A出现的次数
int Lcount = 0; //记录连续的L的出现次数
for(int i = 0; i < s.length(); i++){
char ch = s.charAt(i);
if(ch == 'L'){
Lcount++;
if(Lcount >= 3){
return false;
}
}else{
Lcount = 0;
if(ch == 'A'){
Acount++;
if(Acount >= 2){
return false;
}
}
}
}
return true;
}
}
696.计数二进制字符串(简单)★
给定一个字符串 s,统计并返回具有相同数量 0 和 1 的非空(连续)子字符串的数量,并且这些子字符串中的所有 0 和所有 1 都是成组连续的。 重复出现(不同位置)的子串也要统计它们出现的次数。
输入:s = "00110011"
输出:6
解释:6 个子串满足具有相同数量的连续 1 和 0 :"0011"、"01"、"1100"、"10"、"0011" 和 "01" 。
注意,一些重复出现的子串(不同位置)要统计它们出现的次数。
另外,"00110011" 不是有效的子串,因为所有的 0(还有 1 )没有组合在一起。
class Solution {
//记录每段连续出现的字符的长度,相邻的两段出现的字符不同,这两段贡献的子串个数就是他们中的较小值
//可以用list来存储每段,若对“0011000”,list=[2,2,3],故0011这段的贡献为min(2,2)=2,11000这段的贡献为min(2,3)=2
//但可知当前段贡献的子串数只与自己和上一段有关,故可以只记录上一段的长度
public int countBinarySubstrings(String s) {
int len = s.length(), pre = 0, ans = 0, i = 0;//pre记录上一段长度
while(i < len){
char ch = s.charAt(i); //当前段的字符
int count = 0; //当前段的长度
while(i < len && s.charAt(i) == ch){
i++;
count++;
}
ans += Math.min(pre, count);
pre = count;
}
return ans;
}
}
467.环绕字符串中唯一的子字符串(中等)★
找出p中非空连续子串数量。(包含单个字符)
输入: p = "zab"
输出: 6
解释: 在字符串 s 中有 p 的六个子串 ("z", "a", "b", "za", "ab", "zab") 。
class Solution {
//题目本质就是要求最长连续子串,使用动态规划
//设dp[a]表示以a结尾的最长连续串的长度,则题目所求为dp[a]+...+dp[z]
public int findSubstringInWraproundString(String p) {
int[] dp = new int[26];
int len = 0;
for(int i = 0; i < p.length(); i++){
if(i > 0 && (p.charAt(i) - p.charAt(i-1) + 26) % 26 == 1){ //与前一个字符连续时
len++;
}else{
len = 1; //非连续字符时
}
dp[p.charAt(i) - 'a'] = Math.max(dp[p.charAt(i) - 'a'], len);
}
return Arrays.stream(dp).sum();
}
}
535.TinyURL的加密和解密(中等)
TinyURL 是一种 URL 简化服务, 比如:当你输入一个 URL leetcode.com/problems/de… 时,它将返回一个简化的URL tinyurl.com/id 。请你设计一个类来加密与解密 TinyURL 。
public class Codec {
//使用一个HashMap来模拟dataBase存储id与longurl的映射
//shorturl由固定字段和id组成
//关键在于id要如何生成,1.逐个计数累加,2.自定义实现hashcode,3.随机生成
//此处使用逐个计数累加
Map<Integer, String> dataBase = new HashMap<Integer, String>();
int id = 0;
// Encodes a URL to a shortened URL.
public String encode(String longUrl) {
id++;
dataBase.put(id, longUrl);
return "http://tinyurl.com/" + id;
}
// Decodes a shortened URL to its original URL.
public String decode(String shortUrl) {
int index = shortUrl.lastIndexOf('/') + 1;
int key = Integer.parseInt(shortUrl.substring(index));
return dataBase.get(key);
}
}
// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.decode(codec.encode(url));
数字与字符串间转换
299.猜数字游戏(中等)
写一个数字secret,猜测数字guess,对于guess中的每位数,若位置和数字都和secret一样,则算一个A,若数字包含在secret中但是位置不对应,则算一个B。
输入: secret = "1807", guess = "7810"
输出: "1A3B"
class Solution {
//遍历字符串,先计算出公牛个数,然后用两个数组记录两个字符串的非公牛数出现的次数
//由于出现次数多余的数无法匹配,故奶牛数为两字符串中对应数字出现次数少者
public String getHint(String secret, String guess) {
int[] mapS = new int[10];
int[] mapG = new int[10];
int countA = 0; //公牛数
int countB = 0; //奶牛数
int len = secret.length();
for(int i = 0; i < len; i++){
if(secret.charAt(i) == guess.charAt(i)){
countA++;
}else{
mapS[secret.charAt(i) - '0'] += 1;
mapG[guess.charAt(i) - '0'] += 1;
}
}
for(int i = 0; i < 10; i++){
countB += Math.min(mapS[i], mapG[i]);
}
return countA + "A" + countB + "B";
}
}
412.FizzBuzz(简单)
给你一个整数 n ,找出从 1 到 n 各个整数的 Fizz Buzz 表示,并用字符串数组 answer(下标从 1 开始)返回结果,其中:
answer[i] == "FizzBuzz" 如果 i 同时是 3 和 5 的倍数。
answer[i] == "Fizz" 如果 i 是 3 的倍数。
answer[i] == "Buzz" 如果 i 是 5 的倍数。
answer[i] == i (以字符串形式)如果上述条件全不满足。
class Solution {
public List<String> fizzBuzz(int n) {
List<String> res = new ArrayList<>();
for(int i = 1; i <= n; i++){
if(i % 15 == 0){
res.add("FizzBuzz");
}else if(i % 3 == 0){
res.add("Fizz");
}else if(i % 5 == 0){
res.add("Buzz");
}else{
res.add(String.valueOf(i));
}
}
return res;
}
}
506.相对名次(简单)
给你一个长度为 n 的整数数组 score ,其中 score[i] 是第 i 位运动员在比赛中的得分。所有得分都 互不相同 。
运动员将根据得分 决定名次 ,其中名次第 1 的运动员得分最高,名次第 2 的运动员得分第 2 高,依此类推。运动员的名次决定了他们的获奖情况:
名次第 1 的运动员获金牌 "Gold Medal" 。
名次第 2 的运动员获银牌 "Silver Medal" 。
名次第 3 的运动员获铜牌 "Bronze Medal" 。
从名次第 4 到第 n 的运动员,只能获得他们的名次编号(即,名次第 x 的运动员获得编号 "x")。
使用长度为n的数组answer返回获奖,其中 answer[i] 是第 i 位运动员的获奖情况。
输入: score = [5,4,3,2,1]
输出: ["Gold Medal","Silver Medal","Bronze Medal","4","5"]
class Solution {
//使用一个数组记录分数和名次
public String[] findRelativeRanks(int[] score) {
int len = score.length;
int[] sort = new int[len];
for(int i = 0; i < len; i++){
for(int j = 0; j < len; j++){
if(score[i] <= score[j]){ //遇到一个得分比自己高(或等于)的人,名次就+1
sort[i] += 1;
}
}
}
String[] res = new String[len];
for(int i = 0; i < len; i++){
int index = sort[i];
if(index == 1){
res[i] = "Gold Medal";
}else if(index == 2){
res[i] = "Silver Medal";
}else if(index == 3){
res[i] = "Bronze Medal";
}else{
res[i] = String.valueOf(index);
}
}
return res;
}
}
class Solution {
//问题的本质是在对得分数组进行排序之后,要保存排序之前的数据的下标
//使用一个二维数组,记录得分和下标
public String[] findRelativeRanks(int[] score) {
int len = score.length;
int[][] arr = new int[len][2];
String[] str = {"Gold Medal","Silver Medal","Bronze Medal"};
for(int i = 0; i < len; i++){
arr[i][0] = score[i]; //记录得分
arr[i][1] = i; //记录原数据下标
}
Arrays.sort(arr, (a, b) -> b[0] - a[0]);
String[] res = new String[len];
for(int i = 0; i < len; i++){
if(i >= 3){
res[arr[i][1]] = String.valueOf(i+1);
}else{
res[arr[i][1]] = str[i];
}
}
return res;
}
}
539.最小时间差(中等)★
给定一个 24 小时制(小时:分钟 "HH:MM" )的时间列表,找出列表中任意两个时间的最小时间差并以分钟数表示。
输入: timePoints = ["23:59","00:00"]
输出: 1
class Solution {
//将列表进行排序,最小时间差必然出现在相邻的两个元素或首尾两个元素
public int findMinDifference(List<String> timePoints) {
int len = timePoints.size();
if(len > 1440) return 0; //总共有24*60=1440个时间点,若长度大于1440,必然有两个相同的时间点
Collections.sort(timePoints);
int ans = Integer.MAX_VALUE;
int con = getMin(timePoints.get(0)); //getMin将单位都转化为分钟
int pre = con;
for(int i = 1; i < len; i++){
con = getMin(timePoints.get(i));
ans = Math.min(ans, con - pre);
pre = con;
}
return Math.min(ans, getMin(timePoints.get(0)) + 1440 - pre);
}
public int getMin(String time){
return ((time.charAt(0)-'0') * 10 + (time.charAt(1)-'0')) * 60 +
((time.charAt(3)-'0') * 10 + (time.charAt(4)-'0'));
}
}
553.最优解法(中等)
给定一组正整数,相邻的整数之间将会进行浮点除法操作。例如,[2,3,4] -> 2/3/4
但是,你可以在任意位置添加任意数目的括号,来改变算数的优先级。你需要找出怎么添加括号,才能得到最大的结果,并且返回相应的字符串格式的表达式。你的表达式不应该含有冗余的括号。
输入: [1000,100,10,2]
输出: "1000/(100/10/2)"
class Solution {
//要使结果最大应该使分子最大,分母最小,由于是除法,故无论如何加括号,最大的分子一定是首个数
//分母最小一定是从第二个一直除到最后一个数
//也就是说,结果一定是a/(b/c/d)
public String optimalDivision(int[] nums) {
int len = nums.length;
if(len == 1) return String.valueOf(nums[0]);
if(len == 2) return String.valueOf(nums[0]) + "/" + String.valueOf(nums[1]);
StringBuffer sb = new StringBuffer();
sb.append(String.valueOf(nums[0]) + "/(" + String.valueOf(nums[1]));
for(int i = 2; i < len; i++){
sb.append("/" + String.valueOf(nums[i]));
}
sb.append(")");
return sb.toString();
}
}
537.复数乘法(中等)
复数 可以用字符串表示,遵循 "实部+虚部i" 的形式,并满足下述条件:
实部 是一个整数,取值范围是 [-100, 100]
虚部 也是一个整数,取值范围是 [-100, 100]
i² == -1
给你两个字符串表示的复数 num1 和 num2 ,请你遵循复数表示形式,返回表示它们乘积的字符串。
class Solution {
public String complexNumberMultiply(String num1, String num2) {
String[] spnum1 = num1.split("\\+|i"); //使用正则表达式
String[] spnum2 = num2.split("\\+|i");
int num1Real = Integer.parseInt(spnum1[0]); //num1的实部
int num1Virtual = Integer.parseInt(spnum1[1]);//num1的虚部
int num2Real = Integer.parseInt(spnum2[0]); //num2的实部
int num2Virtual = Integer.parseInt(spnum2[1]);//num2的虚部
return (num1Real * num2Real - num1Virtual * num2Virtual) + "+"
+ (num1Real * num2Virtual + num2Real * num1Virtual) + "i";
}
}
592.分数加减运算(中等)★
给定一个表示分数加减运算的字符串 expression ,你需要返回一个字符串形式的计算结果。
这个结果应该是不可约分的分数,即最简分数。 如果最终结果是一个整数,例如 2,你需要将它转换成分数形式,其分母为 1。所以在上述例子中, 2 应该被转换为 2/1。
输入: expression = "-1/2+1/2"
输出: "0/1"
class Solution {
//模拟,遍历字符串得到分子与分母,两个分数相加即(top1 * bottom2 + top2 * bottom1)/(bottom1 * bottom2)
//最后再找出分子和分母的最大公约数,进行化简
public String fractionAddition(String expression) {
int len = expression.length();
long top = 0; //分子初始值为0,保存的是上一次计算的结果
long bottom = 1; //分母初始值为1
int index = 0;
while(index < len){
long top1 = 0; //存储的是当前扫描到的分子
long bottom1 = 0; //注意这里的分母跟bottom不一样,这里需要遍历每一位得到一个数,故初始为0
int sign = 1; //记录符号
if(expression.charAt(index) == '-' || expression.charAt(index) == '+'){
sign = expression.charAt(index) == '-' ? -1 : 1;
index++;
}
//读取分子
while(index < len && Character.isDigit(expression.charAt(index))){
top1 = top1 * 10 + (expression.charAt(index) - '0');
index++;
}
top1 *= sign;
index++;
//读取分母
while(index < len && Character.isDigit(expression.charAt(index))){
bottom1 = bottom1 * 10 + (expression.charAt(index) - '0');
index++;
}
top = top * bottom1 + top1 * bottom;
bottom *= bottom1;
}
if(top == 0) return "0/1";
long g = gcd(Math.abs(top), bottom);
return top/g + "/" + bottom/g;
}
public long gcd(long a, long b){
long temp = a % b;
while(temp != 0){
a = b;
b = temp;
temp = a%b;
}
return b;
}
}
640.求解方程(中等)★
求解一个给定的方程,将x以字符串 "x=#value" 的形式返回。该方程仅包含 '+' , '-' 操作,变量 x 和其对应系数。 如果方程没有解,请返回 "No solution" 。如果方程有无限解,则返回 “Infinite solutions” 。
题目保证,如果方程中只有一个解,则 'x' 的值是一个整数。
输入: equation = "x+5-3+x=6+x-2"
输出: "x=2"
class Solution {
//将全部项移到左边,故等号右边式子的系数都乘上-1,遍历字符串,记录x的系数和整数值
//无解的情况和无限解的情况都是x的系数为0,数值为0时无限解,数值不为0时无解
public String solveEquation(String equation) {
int len = equation.length();
int flag = 1; //等号左边的符号为1,当遇到=号时变为-1
int factor = 0; //x的系数
int val = 0; //常数项数值
int index = 0;
while(index < len){
if(equation.charAt(index) == '='){
flag = -1;
index++;
continue;
}
int sign = flag; //当前项的符号
int num = 0; //当前项数值
boolean vaild = false; //当前项的系数是否有效,因为x前面可能没有数值
if(equation.charAt(index) == '-' || equation.charAt(index) == '+'){
sign = equation.charAt(index) == '-' ? -sign : sign;
index++;
}
while(index < len && Character.isDigit(equation.charAt(index))){
num = num * 10 + (equation.charAt(index) - '0');
vaild = true;
index++;
}
if(index < len && equation.charAt(index) == 'x'){
factor += vaild ? sign * num : sign;
index++; //当前字符是x,需要继续指向下一个字符
}else{
val += sign*num; //当前字符是+-号,或者到达字符串末尾,不需要指向下一个字符
}
}
if(factor == 0){
return val == 0 ? "Infinite solutions" : "No solution";
}else{
return "x=" + (-val/factor);
}
}
}
38.外观数组(中等)
给定一个正整数 n ,输出外观数列的第 n 项。
「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。 你可以将其视作是由递归公式定义的数字字符串序列:
countAndSay(1) = "1"
countAndSay(n) 是对 countAndSay(n-1) 的描述,然后转换成另一个数字字符串。
前五项如下:
1. 1
2. 11
3. 21
4. 1211
5. 111221
第一项是数字 1
描述前一项,这个数是 1 即 “ 一 个 1 ”,记作 "11"
描述前一项,这个数是 11 即 “ 二 个 1 ” ,记作 "21"
描述前一项,这个数是 21 即 “ 一 个 2 + 一 个 1 ” ,记作 "1211"
描述前一项,这个数是 1211 即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ,记作 "111221"
class Solution {
//递归解法
public String countAndSay(int n) {
if(n == 1) return "1";
String str = countAndSay(n-1);
int len = str.length();
StringBuffer sb = new StringBuffer();
int index = 0;
while(index < len){
int count = 0;
char pre = str.charAt(index);
while(index < len && str.charAt(index) == pre){
count++;
index++;
}
sb.append(String.valueOf(count) + pre);
}
return sb.toString();
}
}
class Solution {
//遍历解法
public String countAndSay(int n) {
String str = "1";
for(int i = 2; i <= n; i++){
int len = str.length();
StringBuffer sb = new StringBuffer();
int start = 0;
int con = 0;
while(con < len){
while(con < len && str.charAt(start) == str.charAt(con)){
con++;
}
sb.append(Integer.toString(con-start) + str.charAt(start));
start = con;
}
str = sb.toString();
}
return str;
}
}
443.压缩字符串(中等)★
输入:chars = ["a","a","b","b","c","c","c"]
输出:返回 6 ,输入数组的前 6 个字符应该是:["a","2","b","2","c","3"]
注意长度为10时应该设为"1","0"
对字符数组进行操作,并返回新数组的长度,且原地计算。
class Solution {
//使用一个读指针和一个写指针,对字符数组边遍历边修改
public int compress(char[] chars) {
int len = chars.length;
if(len == 1) return 1;
int write = 0;
int start = 0; //记录连续字符的开始下标
for(int read = 0; read < len; read++){
if(read == len-1 || chars[read] != chars[read+1]){ //最后一个字符或非连续字符时才需要操作
chars[write++] = chars[read];
int num = read - start + 1;
start = read + 1;
if(num > 1){ //将num转化为字符串依次写入数组
String str = String.valueOf(num);
for(int i = 0; i < str.length(); i++){
chars[write++] = str.charAt(i);
}
}
}
}
return write;
}
}
8.字符串转化为整数(中等)★
题目的意思是要把第一个数字转化为int类型输出,但是有几个注意点:
1.去除前后空格
2.去除空格后第一个字符必须是数字或正负号,否则返回0
3.只返回第一个整数,返回值为32位整数范围,即[−2^31, 2^31 − 1],越界时截断
普通解法,代码臃肿,边界条件多
class Solution {
public int myAtoi(String s) {
s = s.trim();
int len = s.length();
if(len == 0) return 0;
if(!Character.isDigit(s.charAt(0)) && s.charAt(0) != '-' && s.charAt(0) != '+'){
return 0;
}
int sign = 1;
if(s.charAt(0) == '-'){
sign = -1;
}
int index = Character.isDigit(s.charAt(0)) ? 0 : 1;
int num = 0;
while(index < len && Character.isDigit(s.charAt(index))){
if(sign == 1 && (num > Integer.MAX_VALUE / 10 || (num == Integer.MAX_VALUE / 10 &&
(s.charAt(index) - '0') > 7))){
return Integer.MAX_VALUE;
}
if(sign == -1 && (num > Integer.MAX_VALUE / 10 || (num == Integer.MAX_VALUE / 10 &&
(s.charAt(index) - '0') > 8))){
return Integer.MIN_VALUE;
}
num = num * 10 + s.charAt(index) - '0';
index++;
}
return sign * num;
}
}
有限状态机解法★
class Solution {
public int myAtoi(String str) {
Automaton automaton = new Automaton();
int length = str.length();
for (int i = 0; i < length; ++i) {
automaton.get(str.charAt(i));
}
return (int) (automaton.sign * automaton.ans);
}
}
class Automaton {
public int sign = 1;
public long ans = 0;
private String state = "start";
private Map<String, String[]> table = new HashMap<String, String[]>() {{
put("start", new String[]{"start", "signed", "in_number", "end"});
put("signed", new String[]{"end", "end", "in_number", "end"});
put("in_number", new String[]{"end", "end", "in_number", "end"});
put("end", new String[]{"end", "end", "end", "end"});
}};
public void get(char c) {
state = table.get(state)[get_col(c)];
if ("in_number".equals(state)) {
ans = ans * 10 + c - '0';
ans = sign == 1 ? Math.min(ans, (long) Integer.MAX_VALUE) : Math.min(ans, -(long) Integer.MIN_VALUE);
} else if ("signed".equals(state)) {
sign = c == '+' ? 1 : -1;
}
}
private int get_col(char c) {
if (c == ' ') {
return 0;
}
if (c == '+' || c == '-') {
return 1;
}
if (Character.isDigit(c)) {
return 2;
}
return 3;
}
}
tip:charAt()会检查小标的合法性,故一般将字符串转换为字符数组进行操作,即str.toCharArray()
13.罗马数字转整数(简单)
罗马数字包含七种字符: I,V,X,L,C,D和M。分别表示1,5,10,50,100,500,1000
例如, 罗马数字 2 写做 II ,即为两个并列的 1 。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。
通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:
I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。
给定一个罗马数字,将其转换成整数。
class Solution {
//逐个字符遍历,遇到特殊情况时(前面数字比后面数字小)转化为前面数字取负号
public int romanToInt(String s) {
Map<Character, Integer> map = new HashMap<>(){{
put('I', 1);
put('V', 5);
put('X', 10);
put('L', 50);
put('C', 100);
put('D', 500);
put('M', 1000);
}};
int len = s.length();
if(len == 1) return map.get(s.charAt(0));
int sum = 0;
for(int i = 1; i < len; i++){
int pre = map.get(s.charAt(i-1));
int con = map.get(s.charAt(i));
if(pre < con){
sum -= pre;
}else{
sum += pre;
}
}
sum += map.get(s.charAt(len-1));
return sum;
}
}
12.整数转罗马数字(中等)
与上题相反
class Solution {
//模拟,把所有可能出现的罗马数字符号对应其数字
public String intToRoman(int num) {
int[] arr = {1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000};
String[] str = {"I", "IV", "V", "IX", "X", "XL", "L", "XC", "C", "CD", "D", "CM", "M"};
StringBuffer sb = new StringBuffer();
for(int i = arr.length-1; i >= 0; i--){
while(num >= arr[i]){
sb.append(str[i]);
num -= arr[i];
}
}
return sb.toString();
}
}
273.整数转换为英文表示(困难)
将非负整数 num 转换为其对应的英文表示。
输入:num = 1234567
输出:"One Million Two Hundred Thirty Four Thousand Five Hundred Sixty Seven"
class Solution {
//将每三位做一次划分,读取出三位数然后加上倍数即可,如12345的12,为12+倍数thousand
//声明预处理map
public static Map<Integer, String> map = new HashMap<>(){{
put(1, "One");put(2, "Two");put(3, "Three");put(4, "Four");
put(5, "Five");put(6, "Six");put(7, "Seven");put(8, "Eight");
put(9, "Nine");put(10, "Ten");put(11, "Eleven");put(12, "Twelve");
put(13, "Thirteen");put(14, "Fourteen");put(15, "Fifteen");put(16, "Sixteen");
put(17, "Seventeen");put(18, "Eighteen");put(19, "Nineteen");put(20, "Twenty");
put(30, "Thirty");put(40, "Forty");put(50, "Fifty");put(60, "Sixty");
put(70, "Seventy");put(80, "Eighty");put(90, "Ninety");
}};
public String numberToWords(int num) {
if(num == 0) return "Zero";
StringBuffer sb = new StringBuffer();
if(num / 1000000000 != 0){
sb.append(map.get(num/1000000000) + " Billion ");
num %= 1000000000;
}
if(num / 1000000 != 0){
sb.append(readThreeNum(num/1000000).trim() + " Million ");
num %= 1000000;
}
if(num / 1000 != 0){
sb.append(readThreeNum(num/1000).trim() + " Thousand ");
num %= 1000;
}
if(num != 0){
sb.append(readThreeNum(num));
}
return sb.toString().trim();
}
//实现读取三位数
public String readThreeNum(int num){
StringBuffer sb = new StringBuffer();
if(num / 100 != 0){
sb.append(map.get(num/100) + " Hundred ");
num %= 100;
}
if(num / 10 != 0){
if(num <= 20){
sb.append(map.get(num));
return sb.toString();
}else{
sb.append(map.get(num/10*10) + " ");
}
}
if(num%10 != 0){
sb.append(map.get(num%10));
}
return sb.toString();
}
}
更规范简洁的代码
class Solution {
String[] singles = {"", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"};
String[] teens = {"Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"};
String[] tens = {"", "Ten", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"};
String[] thousands = {"", "Thousand", "Million", "Billion"};
public String numberToWords(int num) {
if (num == 0) {
return "Zero";
}
StringBuffer sb = new StringBuffer();
for (int i = 3, unit = 1000000000; i >= 0; i--, unit /= 1000) {
int curNum = num / unit;
if (curNum != 0) {
num -= curNum * unit;
StringBuffer curr = new StringBuffer();
recursion(curr, curNum);
curr.append(thousands[i]).append(" ");
sb.append(curr);
}
}
return sb.toString().trim();
}
public void recursion(StringBuffer curr, int num) {
if (num == 0) {
return;
} else if (num < 10) {
curr.append(singles[num]).append(" ");
} else if (num < 20) {
curr.append(teens[num - 10]).append(" ");
} else if (num < 100) {
curr.append(tens[num / 10]).append(" ");
recursion(curr, num % 10);
} else {
curr.append(singles[num / 100]).append(" Hundred ");
recursion(curr, num % 100);
}
}
}
165.比较版本号(中等)★
输入:version1 = "1.01", version2 = "1.001"
输出:0
解释:忽略前导零,"01" 和 "001" 都表示相同的整数 "1"
输入:version1 = "0.1", version2 = "1.1"
输出:-1
解释:version1 中下标为 0 的修订号是 "0",version2 中下标为 0 的修订号是 "1"
0 < 1,所以 version1 < version2
class Solution {
public int compareVersion(String version1, String version2) {
String[] v1 = version1.split("\\.");
String[] v2 = version2.split("\\.");
for(int i = 0; i < v1.length || i < v2.length; i++){ //两个数组长度不一致时这样处理
int x = 0;
int y = 0;
if(i < v1.length){
x = Integer.parseInt(v1[i]); //parseInt方法可以直接去掉前导0
}
if(i < v2.length){
y = Integer.parseInt(v2[i]);
}
if(x > y) return 1;
if(x < y) return -1;
}
return 0;
}
}
//双指针法,不单纯按位数比较,而是转化为数字,比较大小
//使用双指针法的优势在于,不用花费额外两个数组存储切分好的修订号
class Solution {
public int compareVersion(String version1, String version2) {
int n = version1.length(), m = version2.length();
int i = 0, j = 0;
while (i < n || j < m) {
int x = 0;
for (; i < n && version1.charAt(i) != '.'; ++i) {
x = x * 10 + version1.charAt(i) - '0';
}
++i; // 跳过点号
int y = 0;
for (; j < m && version2.charAt(j) != '.'; ++j) {
y = y * 10 + version2.charAt(j) - '0';
}
++j; // 跳过点号
if (x != y) {
return x > y ? 1 : -1;
}
}
return 0;
}
}
481.神奇的字符串(中等)★
神奇字符串 s 仅由 '1' 和 '2' 组成,并需要遵守下面的规则:
s 的前几个元素是 s = "1221121221221121122……" 。如果将 s 中连续的若干 1 和 2 进行分组,可以得到 "1 22 11 2 1 22 1 22 11 2 11 22 ......" 。每组中 1 或者 2 的出现次数分别是 "1 2 2 1 1 2 1 2 2 1 2 2 ......" 。上面的出现次数正是 s 自身。
给你一个整数 n ,返回在神奇字符串 s 的前 n 个数字中 1 的数目。
class Solution {
//神奇字符串就是在原字符串的末尾不断添加末尾对应的元素
public int magicalString(int n) {
if(n <= 3) return 1;
StringBuffer str = new StringBuffer("122");
int i = 2; //i指向当前字符增加的次数
while(str.length() < n){
char lastCh = str.charAt(str.length()-1); //lastCh为当前末尾字符,增加的字符与其相反
if(str.charAt(i) == '1'){
if(lastCh == '1'){
str.append("2");
}else{
str.append("1");
}
}else{
if(lastCh == '1'){
str.append("22");
}else{
str.append("11");
}
}
i++;
}
int count = 0;
for(int j = 0; j < n; j++){ //注意这里是j<n,因为实际的str长度可能超过n
if(str.charAt(j) == '1'){
count++;
}
}
return count;
}
}
子序列
392.判断子序列(简单)★
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
进阶: 如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
class Solution {
//双指针法,分别指向s与t
public boolean isSubsequence(String s, String t) {
int index_s = 0; //指向s
int index_t = 0; //指向t
while(index_s < s.length() && index_t < t.length()){
if(s.charAt(index_s) == t.charAt(index_t)){
index_s++;
index_t++;
}else{
index_t++;
}
}
if(index_s == s.length()) return true;
return false;
}
}
双指针法在t字符串中需要不断寻找下一个字符出现的位置,若使用动态规划
设dp[i][j]为字符串t中i位置后字符j出现的位置,即可迅速跳到下一个字符的位置
且动态规划数组只与t有关,当有大量s时,不需要总是遍历字符串t,效率更高
class Solution {
//动态规划,dp[i][j]为字符串t第i个位置后出现字符j的下标
//若当前字符就是字符j,则dp[i][j]=i,若当前字符不是字符j,则dp[i][j]=dp[i+1][j]
//dp数组需要从后往前更新
public boolean isSubsequence(String s, String t) {
int n = s.length();
int m = t.length();
int[][] dp = new int[m+1][26];
for(int i = 0; i < 26; i++){
dp[m][i] = m; //设置边界条件,当某字符没有出现时,设为m
}
for(int i = m-1; i >= 0; i--){ //从后往前更新
for(int j = 0; j < 26; j++){
if(t.charAt(i) == j + 'a'){ //若当前字符就是要找的字符
dp[i][j] = i;
}else{
dp[i][j] = dp[i+1][j];
}
}
}
int begin = 0;
for(int i = 0; i < n; i++){ //从s的第一个字符开始
if(dp[begin][s.charAt(i) - 'a'] == m){
return false;
}
begin = dp[begin][s.charAt(i) - 'a'] + 1; //begin跳到字符出现位置的下一个位置
}
return true;
}
}
524.通过删除字母匹配到字典最长单词(中等)★
给你一个字符串 s 和一个字符串数组 dictionary ,找出并返回 dictionary 中最长的字符串,该字符串可以通过删除 s 中的某些字符得到。
如果答案不止一个,返回长度最长且字母序最小的字符串。如果答案不存在,则返回空字符串。
class Solution {
//问题是找出s的最长子串,对s构建动态规划数组,dp[i][j]指s字符串i位置后j字符的位置
//再对dictionary逐个判断是否为s的子串,是s的子串的情况下判断是否最长及字典序最小
//动态数组的构建方式与上题相同
public String findLongestWord(String s, List<String> dictionary) {
int n = s.length();
int[][] dp = new int[n+1][26];
Arrays.fill(dp[n], n);
for(int i = n-1; i >= 0; i--){
for(int j = 0; j < 26; j++){
if(s.charAt(i) == j + 'a'){
dp[i][j] = i;
}else{
dp[i][j] = dp[i+1][j];
}
}
}
String res = "";
for(String str : dictionary){
int len = str.length();
boolean flag = true; //是否匹配失败
int begin = 0;
for(int j = 0; j < len; j++){
if(dp[begin][str.charAt(j) - 'a'] == n){
flag = false; //匹配失败
break;
}
begin = dp[begin][str.charAt(j) - 'a'] + 1;
}
if(flag){
if(len > res.length() || (len == res.length()) && str.compareTo(res) < 0){
res = str;
}
}
}
return res;
}
}
521.最长特殊子串(简单)
给你两个字符串 a 和 b,请返回 这两个字符串中 最长的特殊序列 的长度。如果不存在,则返回 -1 。
「最长特殊序列」 定义如下:该序列为 某字符串独有的最长子序列(即不能是其他字符串的子序列) 。
字符串 s 的子序列是在从 s 中删除任意数量的字符后可以获得的字符串。例如,"abc" 是 "aebdc" 的子序列。
class Solution {
public int findLUSlength(String a, String b) {
int len1 = a.length();
int len2 = b.length();
if(len1 != len2) return len1 > len2 ? len1 : len2;//两字符串长度不等时,最长特殊子串是长的那个
if(a.equals(b)) return -1; //若两字符串长度相等,且a是b的子串,即a与b相同,则无特殊子串
return len1; //若两字符串长度相等,且a不是b的子串,则最长特殊子串为a
}
}
522.最长特殊序列2(中等)★
给定字符串列表 strs ,返回其中最长的特殊序列的长度。如果最长特殊序列不存在,返回 -1 。
特殊序列定义如下:该序列为某字符串 独有的子序列(即不能是其他字符串的子序列)。
s的子序列可以通过删去字符串s中的某些字符实现。 例如,"abc" 是 "aebdc" 的子序列
class Solution {
//使用双重循环,每次拿出一个字符串判断是否是其他字符串的子串
//若不是,则他本身就是特殊字符串,取其长度与max进行比较
//判断子串使用双指针法
public int findLUSlength(String[] strs) {
int len = strs.length;
int res = -1;
for(int i = 0; i < len; i++){
boolean flag = true; //标记str[i]不是strs[j]的子串
for(int j = 0; j < len; j++){
if(i != j && isSubstring(strs[i], strs[j])){
flag = false;
break;
}
}
if(flag){
res = Math.max(res, strs[i].length());
}
}
return res;
}
//判断str1是否为str2的子串
public boolean isSubstring(String str1, String str2){
int index1 = 0;
int index2 = 0;
while(index1 < str1.length() && index2 < str2.length()){
if(str1.charAt(index1) == str2.charAt(index2)){
index1++;
}
index2++;
}
return index1 == str1.length();
}
}
高精度运算
66.加一(简单)★
给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。 最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。 你可以假设除了整数 0 之外,这个整数不会以零开头。
class Solution {
public int[] plusOne(int[] digits) {
int len = digits.length;
List<Integer> res = new ArrayList<>();
int temp = 1; //进位,初始时为1表示+1
for(int i = len - 1; i >= 0; i--){ //从低位往高位
int num = digits[i];
num = num + temp;
temp = num / 10;
num = num % 10;
res.add(num); //注意这里低位在前,要逆序才能得到答案
}
if(temp != 0){
res.add(temp);
}
Collections.reverse(res);
return res.stream().mapToInt(Integer::intValue).toArray();//注意如何将List<Integer>转化为int[]
}
}
67.二进制求和(简单)
给你两个二进制字符串,返回它们的和(用二进制表示)。
输入为 非空 字符串且只包含数字 1 和 0。
class Solution {
//模拟
public String addBinary(String a, String b) {
StringBuffer res = new StringBuffer();
int index1 = a.length() - 1;
int index2 = b.length() - 1;
int temp = 0; //进位
int con = 0; //当前位
while(index1 >= 0 || index2 >= 0){
int num1 = index1 >= 0 ? a.charAt(index1) - '0' : 0;
int num2 = index2 >= 0 ? b.charAt(index2) - '0' : 0;
con = (num1 + num2 + temp) % 2;
temp = (num1 + num2 + temp) / 2;
res.append(String.valueOf(con));
index1--;
index2--;
}
if(temp != 0){
res.append(String.valueOf(temp));
}
res.reverse();
return res.toString();
}
}
415.字符串相加(简单)
给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。
class Solution {
public String addStrings(String num1, String num2) {
int index1 = num1.length() - 1;
int index2 = num2.length() - 1;
int temp = 0;
StringBuffer res = new StringBuffer();
while(index1 >= 0 || index2 >= 0){
int a = index1 >= 0 ? num1.charAt(index1) - '0' : 0;
int b = index2 >= 0 ? num2.charAt(index2) - '0' : 0;
int num = a+b+temp;
res.append(String.valueOf(num%10));
temp = num / 10;
index1--;
index2--;
}
if(temp != 0){
res.append(String.valueOf(temp));
}
return res.reverse().toString();
}
}
43.字符串相乘(中等)★★
实现非负整数字符串相乘
class Solution {
//可以分解为单个字符乘字符串,得到结果再进行字符串加法,比较麻烦
//实际上,可以把字符串转换为字符数组,方便计算,且满足规律:num1[i]×num2[j] 的结果位于 ansArr[i+j+1],如果 ansArr[i+j+1]≥10,则将进位部分加到 ansArr[i+j]
public String multiply(String num1, String num2) {
if(num1.equals("0") || num2.equals("0")) return "0";
int len1 = num1.length();
int len2 = num2.length();
int[] res = new int[len1+len2]; //结果最长不超过len1+len2
for(int i = len1 - 1; i >= 0; i--){
int x = num1.charAt(i) - '0';
for(int j = len2 - 1; j >= 0; j--){
int y = num2.charAt(j) - '0';
res[i+j+1] += x * y; //注意累加
}
}
for(int i = len1+len2-1; i > 0; i--){ //对进位进行处理
res[i - 1] += res[i] / 10;
res[i] %= 10;
}
int index = res[0] == 0 ? 1 : 0;
StringBuffer sb = new StringBuffer();
while(index < len1+len2){
sb.append(res[index]);
index++;
}
return sb.toString();
}
}
306.累加数(中等)★
累加数 是一个字符串,组成它的数字可以形成累加序列。 一个有效的 累加序列 必须 至少 包含 3 个数。除了最开始的两个数以外,序列中的每个后续数字必须是它之前两个数字之和。
给你一个只包含数字 '0'-'9' 的字符串,编写一个算法来判断给定输入是否是 累加数 。如果是,返回 true ;否则,返回 false 。
说明:累加序列里的数,除数字 0 之外,不会 以 0 开头,所以不会出现 1, 2, 03 或者 1, 02, 3 的情况。
输入:"199100199"
输出:true
解释:累加序列为: 1, 99, 100, 199。1 + 99 = 100, 99 + 100 = 199
class Solution {
//对于累加序列,只要第一个数和第二个数确定,整个序列也就可以确定了
//故需要枚举第一二个数所有的可能,设置firstbegin=0,firstend=secondbegin-1
//只需要遍历secondbegin和secondend即可
//同时要采样字符串加法,防止溢出,遍历到有前导0的数时可以终止本轮循环
public boolean isAdditiveNumber(String num) {
int len = num.length();
for(int secondbegin = 1; secondbegin < len/2 + 1; secondbegin++){//secondbegin不能超过len/2+1
if(num.charAt(0) == '0' && secondbegin != 1){ //第一个数有前导0时
break;
}
for(int secondend = secondbegin; secondend < len; secondend++){
if(num.charAt(secondbegin) == '0' && secondend != secondbegin){ //第二个数有前导0时
break;
}
if(valid(num, secondbegin, secondend)){
return true;
}
}
}
return false;
}
public boolean valid(String num, int secondbegin, int secondend){
int firstbegin = 0;
int firstend = secondbegin - 1;
int len = num.length();
while(secondend < len){
String third = addString(num, firstbegin, firstend, secondbegin, secondend);
int thirdbegin = secondend+1;
int thirdend = secondend + third.length();
if(thirdend >= len || !num.substring(thirdbegin, thirdend + 1).equals(third)){
return false; //若第三个数长度不能超过len,且与计算出来的要相等
}
if(thirdend == len-1) return true; //达到末尾,没有返回false,即匹配成功
firstbegin = secondbegin; //第二个数变为第一个数
firstend = secondend;
secondbegin = thirdbegin; //第三个数变为第二个数
secondend = thirdend;
}
return false;
}
public String addString(String num, int firstbegin, int firstend, int secondbegin,int secondend){
int index1 = firstend;
int index2 = secondend;
int con = 0; //当前位
int temp = 0; //进位
StringBuffer sb = new StringBuffer();
while(index1 >= firstbegin || index2 >= secondbegin){
int x = index1 >= firstbegin ? num.charAt(index1) - '0' : 0;
int y = index2 >= secondbegin ? num.charAt(index2) - '0' : 0;
con = (x+y+temp) % 10;
temp = (x+y+temp) / 10;
sb.append(con);
index1--;
index2--;
}
if(temp != 0){
sb.append(temp);
}
return sb.reverse().toString();
}
}
字符串变换
482.密钥格式化(简单)
给定一个许可密钥字符串 s,仅由字母、数字字符和破折号组成。字符串由 n 个破折号分成 n + 1 组。你也会得到一个整数 k 。
我们想要重新格式化字符串 s,使每一组包含 k 个字符,除了第一组,它可以比 k 短,但仍然必须包含至少一个字符。此外,两组之间必须插入破折号,并且应该将所有小写字母转换为大写字母。
输入: S = "2-5g-3-J", k = 2
输出: "2-5G-3J"
class Solution {
//题意是要每个组长度都为k,而第一组的长度为s.length()%k
//故从后向前遍历,同时计数
public String licenseKeyFormatting(String s, int k) {
int len = s.length();
s = s.toUpperCase();
int count = 0;
StringBuffer sb = new StringBuffer();
for(int i = len - 1; i >= 0; i--){
if(s.charAt(i) != '-'){
sb.append(s.charAt(i));
count++;
if(count % k == 0){
sb.append("-");
}
}
}
//解决最后一个组后带-的问题,sb.length()>0,防止数组越界
if(sb.length() > 0 && sb.charAt(sb.length()-1) == '-'){
sb.deleteCharAt(sb.length()-1);
}
return sb.reverse().toString();
}
}
6.Z字形变换(中等)
将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。比如输入字符串为 "PAYPALISHIRING" 行数为 3 时,排列如下:
P A H N
A P L S I I G
Y I R
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"。
class Solution {
//第一行和最后一行相邻元素下标相差2r-2
//其他元素每个周期需要添加两个元素,第一个元素下标是当前元素位置+2(numRows - r)
//第二个元素下标是当前元素位置+2(r-1)
public String convert(String s, int numRows) {
char[] array = s.toCharArray();
int len = array.length;
if(numRows == 1 || numRows >= len) return s; //特殊情况处理
int r = 1; //当前处理的是第r行
StringBuffer res = new StringBuffer();
int index; //当前处理行的第一个元素下标
while(r <= numRows){
index = r - 1;
while(index < len){
if(r == 1 || r == numRows){
res.append(array[index]);
index += 2 * numRows - 2;
}else{
if(index < len){
res.append(array[index]);
index += 2 * (numRows - r);
}
if(index < len){
res.append(array[index]);
index += 2 * (r - 1);
}
}
}
r++;
}
return res.toString();
}
}
//这道题也可以用二维矩阵模拟,还可以将二维矩阵压缩为一维矩阵
68.左右文本对齐(困难)★★
给定一个单词数组 words 和一个长度 maxWidth ,重新排版单词,使其成为每行恰好有 maxWidth 个字符,且左右两端对齐的文本。
你应该使用 “贪心算法” 来放置给定的单词;也就是说,尽可能多地往每行中放置单词。必要时可用空格 ' ' 填充,使得每行恰好有 maxWidth 个字符。
要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数。
文本的最后一行应为左对齐,且单词之间不插入额外的空格。
class Solution {
//首先计算出一行中可以填充的单词数量,由填充的单词长度可以计算出需要填充的空格数
//若是最后一行,除了单词间的空格,其他空格全部在最后
//若只有一个单词,则空格全部在最后
//若有多个单词且不是最后一行,空格先平均分配avgSpaces=spacesNum / wordsNum-1
//再将剩余空格分配给前spacesNum % wordsNum-1个单词,即每个单词后多加1个空格
public List<String> fullJustify(String[] words, int maxWidth) {
int len = words.length;
List<String> res = new ArrayList<>();
int right = 0; //当前行的最后一个单词在words的下标
while(true){
int left = right; //当前行的第一个单词在words的下标
int rowWordsLen = 0;//当前行的单词长度
//循环计算当前行可以放多少个单词
while(right < len && rowWordsLen + words[right].length() + right - left <= maxWidth){
rowWordsLen += words[right++].length(); //注意++,right会指向下一个新的单词
}
//若当前行是最后一行
if(right == len){
StringBuffer sb = join(words, left, right, " "); //添加单词,每个单词由一个空格分割
sb.append(blank(maxWidth - sb.length())); //在末尾添加对应缺少数量的空格
res.add(sb.toString());
return res;
}
int wordsNum = right - left; //当前行的单词书
int spacesNum = maxWidth - rowWordsLen; //当前行的空格数
//若当前行只有一个单词,单词左对齐,空格全部在最后
if(wordsNum == 1){
StringBuffer sb = new StringBuffer(words[left]);
sb.append(blank(spacesNum));
res.add(sb.toString());
continue; //转下一行
}
//若当前行有多个单词且不是最后一行
int avgSpaces = spacesNum / (wordsNum - 1); //平均每个单词带的空格
int etraSpaces = spacesNum % (wordsNum - 1); //前extraSpaces需要多带一个空格
StringBuffer sb = new StringBuffer();
sb.append(join(words, left, left+etraSpaces+1, blank(avgSpaces+1)));//前extraSpaces个单词多带一个空格,注意join左闭右开,且最后一个单词后没有空格
sb.append(blank(avgSpaces)); //补上第extraSpaces+1个单词后的空格
sb.append(join(words, left+etraSpaces+1, right, blank(avgSpaces)));//添加后面单词的空格
res.add(sb.toString());
}
}
//join函数实现把下标为left和right-1的单词用sep拼接在一起并返回一个字符串
public StringBuffer join(String[] words, int left, int right, String sep){
StringBuffer sb = new StringBuffer(words[left]);
for(int i = left+1; i < right; i++){
sb.append(sep + words[i]);
}
return sb;
}
//返回指定长度的空格字符串
public String blank(int spacesNum){
StringBuffer sb = new StringBuffer();
for(int i = 0; i < spacesNum; i++){
sb.append(' ');
}
return sb.toString();
}
}
字符串匹配
28.实现strStr()(简单)★
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
说明: 当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符。
class Solution {
//暴力匹配算法,每次取haystack中长度为m的子串进行匹配
public int strStr(String haystack, String needle) {
int n = haystack.length();
int m = needle.length();
boolean flag;
for(int i = 0; i + m <= n; i++){
flag = true;
for(int j = 0; j < m; j++){
if(haystack.charAt(i+j) != needle.charAt(j)){
flag = false;
break;
}
}
if(flag){
return i;
}
}
return -1;
}
}
KMP算法
暴力解法的问题在于每次匹配失败时原始字符串上的指针需要回溯,当匹配字符串有许多重复的前后缀时,KMP算法借助相同的前后缀这一信息,提前排除不可能匹配的位置,原始字符串上的指针不需要回溯。
KMP的核心在于对匹配字符串构建next数组,next数组记录了当前位置匹配失败时,匹配字符串的指针应该回到哪个位置,而原始字符串的指针不会回溯。
如何构建next数组?
- 初始化next[0] = 0,匹配串为p,指针j从0开始,指针i从1开始,
- 若p[j] == p[i], next[i] = j+1, i和j同时往后移
- 若p[j] != p[i], j = next[j-1], 直到j==0
- 若j == 0 且 p[j] != p[i], next[i] = 0, i后移,j不变
- 注意第二步的next[i] = j+1实际上是指在每次匹配成功时,next都要基于前一个位置的值加一,第三步的j = next[j-1]指的是每次匹配失败则要回溯到前一个位置next数组的值,且字符串和next数组可能从0或1开始,对应表达式会比较乱,主要理解next数组构建和kmp的操作过程
class Solution {
// KMP 算法,ss为原字符串,pp为匹配串
public int strStr(String ss, String pp) {
if (pp.isEmpty()) return 0;
int n = ss.length(), m = pp.length();
ss = " " + ss; // 原串和匹配串前面都加空格,使其下标从 1 开始
pp = " " + pp;
char[] s = ss.toCharArray(); //转换为char数组,提高获取数据的效率
char[] p = pp.toCharArray();
// 构建 next 数组,长度与匹配串相同(从1开始)
int[] next = new int[m + 1];
for (int i = 2, j = 0; i <= m; i++) { //初始i指向第二个数,j从0开始,j+1指向第一个数
while (j > 0 && p[i] != p[j + 1]) j = next[j]; //匹配失败,j = next(j)
if (p[i] == p[j + 1]) j++; // 匹配成功的话,先让 j++,再更新next[i],结束循环后i++
next[i] = j;
}
// 匹配过程,i为指向原字符串的指针,j为指向匹配串的指针
for (int i = 1, j = 0; i <= n; i++) {
while (j > 0 && s[i] != p[j + 1]) j = next[j]; // 匹配不成功 j = next(j)
if (s[i] == p[j + 1]) j++; // 匹配成功,i,j同时后移,先让 j++,结束本次循环后 i++
if (j == m) return i - m; // j到达匹配串末尾,匹配成功,返回下标
}
return -1;
}
}
686.重复叠加字符串匹配(中等)★
给定两个字符串 a 和 b,寻找重复叠加字符串 a 的最小次数,使得字符串 b 成为叠加后的字符串 a 的子串,如果不存在则返回 -1
注意:字符串 "abc" 重复叠加 0 次是 "",重复叠加 1 次是 "abc",重复叠加 2 次是 "abcabc"。
输入: a = "abcd", b = "cdabcdab"
输出: 3
解释: a 重复叠加三遍后为 "abcdabcdabcd", 此时 b 是其子串。
class Solution {
//a的复制次数有上界和下界,下界是a的长度要大于等于b的长度
//因为b的起始位置必然在第一个a中,且在下界的基础上b不会大于a,故上界为下界加一
public int repeatedStringMatch(String a, String b) {
StringBuilder sa = new StringBuilder();
int n = 0;
while(sa.length() < b.length()){ //得到下界
sa.append(a);
n++;
}
sa.append(a); //得到上界时的字符串
int index = sa.indexOf(b);
if(index != -1){
return index + b.length() > a.length() * n ? n+1 : n; //若超出下界字符串的长度,则返回上界
}
return -1;
}
}
//将上述解法的indexOf函数换为KMP算法
class Solution {
//a的复制次数有上界和下界,下界是a的长度要大于等于b的长度
//因为b的起始位置必然在第一个a中,故上界为下界加一
public int repeatedStringMatch(String a, String b) {
StringBuilder sa = new StringBuilder();
int n = 0;
while(sa.length() < b.length()){ //得到下界
sa.append(a);
n++;
}
sa.append(a); //得到上界时的字符串
int index = subStr(sa.toString(), b);
if(index != -1){
return index + b.length() > a.length() * n ? n+1 : n; //若超出下界字符串的长度,则返回上界
}
return -1;
}
public int subStr(String ss, String pp){ //KMP算法
if(pp.isEmpty()) return 0;
int n = ss.length(), m = pp.length();
ss = " " + ss;
pp = " " + pp;
char[] s = ss.toCharArray();
char[] p = pp.toCharArray();
//构建next数组
int[] next = new int[m+1];
for(int i = 2, j = 0; i <= m; i++){
while(j > 0 && p[i] != p[j+1]){
j = next[j];
}
if(p[i] == p[j+1]){
j++;
}
next[i] = j;
}
//字符串匹配
for(int i = 1, j = 0; i <= n; i++){
while(j > 0 && s[i] != p[j+1]){
j = next[j];
}
if(s[i] == p[j+1]){
j++;
}
if(j == m){
return i - m;
}
}
return -1;
}
}
//也可以使用字符串哈希
class Solution {
//a的复制次数有上界和下界,下界是a的长度要大于等于b的长度
//因为b的起始位置必然在第一个a中,故上界为下界加一
public int repeatedStringMatch(String a, String b) {
StringBuilder sa = new StringBuilder();
int n = 0;
while(sa.length() < b.length()){ //得到下界
sa.append(a);
n++;
}
sa.append(a); //得到上界时的字符串
int index = strHash(sa.toString(), b);
if(index != -1){
return index + b.length() > a.length() * n ? n+1 : n; //若超出下界字符串的长度,则返回上界
}
return -1;
}
//字符串hash使用一个数组记录字符串前缀对应的hash值,采样进制的方法将字符串转化为数字
//一般使用进制为131或13331,能够最小化冲突
int strHash(String ss, String b) {
int P = 131; //进制数
int n = ss.length(), m = b.length();
String str = ss + b; //两个字符串合并
int len = str.length();
int[] h = new int[len + 10], p = new int[len + 10]; //h数组存储字符串hash值
p[0] = 1; //P数组存储的是P的n次方
for (int i = 0; i < len; i++) {
p[i + 1] = p[i] * P;
h[i + 1] = h[i] * P + str.charAt(i); //当前值为前一个数字乘进制数加当前字符(对应数字)
}
int r = len, l = r - m + 1; //子串b下标的范围
int target = h[r] - h[l - 1] * p[r - l + 1]; // b 的哈希值
for (int i = 1; i <= n; i++) { //遍历有可能和b相同的子串
int j = i + m - 1;
int cur = h[j] - h[i - 1] * p[j - i + 1]; // 子串哈希值
if (cur == target) return i - 1; //两hash值相同说明两字符串相等
}
return -1;
}
}
459.重复的子字符串(简单)★
给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。
输入: s = "abab"
输出: true
解释: 可由子串 "ab" 重复两次构成。
class Solution {
//枚举子串长度i(不超过n/2),当原串长度无法被子串长度整除时,匹配失败
//可以整除的情况下,原串每个位置的字符要与比它前i个的字符相等
public boolean repeatedSubstringPattern(String s) {
int n = s.length();
char[] ch = s.toCharArray();
for(int i = 1; i * 2 <= n; i++){ //i为子串的长度
boolean flag = true;
if(n % i != 0){ //不能整除直接匹配失败
flag = false;
}
for(int j = i; j < n; j++){
if(ch[j] != ch[j-i]){ //与上一个重复序列对应位置不等,匹配失败
flag = false;
break;
}
}
if(flag) return true;
}
return false;
}
}
class Solution {
//若s由xxxxx组成(x代表一段字符串),则把第一个x移动到末尾,字符串仍等于s
//于是用两个s连接在一起,去掉第一个和最后一个字符(相当于破坏第一个和最后一个x)
//若s仍是这个字符串的子串,则满足要求
public boolean repeatedSubstringPattern(String s) {
//从第二个位置开始找s,等价于去掉首元素,不等于s.length()等价于去掉最后一个字符
return (s+s).indexOf(s, 1) != s.length();
}
}
上个解法的indexOf也可以换成kmp算法
class Solution {
//若s由xxxxx组成(x代表一段字符串),则把第一个x移动到末尾,字符串仍等于s
//于是用两个s连接在一起,去掉第一个和最后一个字符(相当于破坏第一个和最后一个x)
//若s仍是这个字符串的子串,则满足要求
//indexOf函数也可以由kmp算法代替(在kmp中取出头尾字符)
public boolean repeatedSubstringPattern(String s) {
return kmp(s+s, s) == -1 ? false : true;
}
public int kmp(String ss, String pp){ //KMP算法
if(pp.isEmpty()) return 0;
int n = ss.length(), m = pp.length();
ss = " " + ss;
pp = " " + pp;
char[] s = ss.toCharArray();
char[] p = pp.toCharArray();
//构建next数组
int[] next = new int[m+1];
for(int i = 2, j = 0; i <= m; i++){
while(j > 0 && p[i] != p[j+1]){
j = next[j];
}
if(p[i] == p[j+1]){
j++;
}
next[i] = j;
}
//字符串匹配
for(int i = 2, j = 0; i <= n-1; i++){
while(j > 0 && s[i] != p[j+1]){
j = next[j];
}
if(s[i] == p[j+1]){
j++;
}
if(j == m){
return i - m;
}
}
return -1;
}
}
214.最短回文串(困难)★
给定一个字符串s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。注意回文串指正逆序相同的字符串
输入: s = "aacecaaa"
输出: "aaacecaaa"
哈希字符串解法
class Solution {
//将原字符串分为s1+s2,s1为回文串部分,s2为非回文串,此时需要在s1前加上s2的逆序,即可构成回文串
//故原题转化为取得s的最长回文前缀s1
//逐个遍历s1的结束位置,判断s1是否为回文串,同时使用字符串哈希算法,s1为回文串时正逆序相同,哈希值相同
//根据s1结束位置从小到大计算哈希值比较容易编程(保留了上一次的hash值)
//注意对结果取模,避免字符串过长导致数据溢出
public String shortestPalindrome(String s) {
int n = s.length();
if(n==0 || n==1) return s;
int base = 131;
int mod = 1000000007;
int left = 0; //s1的正序
int right = 0; //s1的逆序
int mul = 1; //逆序时高位需要乘的数
int best = -1; //取得最长回文前缀的s1结束位置
for(int i = 0; i < n; i++){ //遍历结束位置
left = (int)(((long)left * base + s.charAt(i)) % mod); //正序时为前一个left乘进制base
right = (int)(((long)s.charAt(i) * mul + right) % mod); //逆序时为高位字符乘mul
if(left == right) best = i;
mul = (int)((long)mul * base % mod); //每次乘进制base,相当于高位需要乘的进制
}
String add = best == n-1 ? "" : s.substring(best+1);
StringBuffer sb = new StringBuffer(add).reverse();
sb.append(s);
return sb.toString();
}
}
class Solution {
//将原字符串分为s1+s2,s1为回文串部分,s2为非回文串,此时需要在s1前加上s2的逆序,即可构成回文串
//故原题转化为取得s的最长回文前缀s1
//也可以不用逐个遍历s1的结束位置,s的逆序为s',s1作为s的前缀,s1的逆序s1'为s'的后缀
//而s1为回文串,故s1' = s1,于是把s'作为匹配串,s作为模式串
//利用kmp算法找出模式串s与匹配串匹配的字符个数,即为s1的长度
public String shortestPalindrome(String s) {
int n = s.length();
if(n==0 || n==1) return s;
char[] str = s.toCharArray();
int[] next = new int[n];
Arrays.fill(next, -1);
//构造next数组
for(int i = 1; i < n; i++){
int j = next[i-1];
while(j != -1 && str[i] != str[j+1]){
j = next[j];
}
if(str[j+1] == str[i]){
next[i] = j+1;
}
}
int best = -1; //两个字符串匹配的最大长度
for(int i = n-1; i >= 0; i--){ //匹配串从末尾开始计算,相当于逆序
while(best != -1 && str[i] != str[best+1]){
best = next[best];
}
if(str[best+1] == str[i]){
best++;
}
}
String add = best == n-1 ? "" : s.substring(best+1);
StringBuffer sb = new StringBuffer(add).reverse();
sb.append(s);
return sb.toString();
}
}
中心拓展法
5.最长回文子串(简单)
给你一个字符串 s,找到 s 中最长的回文子串。
输入: s = "babad"
输出: "bab"
解释: "aba" 同样是符合题意的答案。
中心扩散法
class Solution {
//使用中心扩散的方法,枚举每个中心点(两种情况,一个中心或两个中心)
//从中心点往外扩散直到头尾元素不等,记录长度
public String longestPalindrome(String s) {
if(s.length() <= 1) return s;
int start = 0, end = 0;
for(int i = 0; i < s.length(); i++){ //遍历每个中心点
int len1 = reverseLength(s, i, i); //一个中心的情况
int len2 = reverseLength(s, i, i+1); //两个中心的情况
int len = Math.max(len1, len2); //当前中心的回文串长度
if(len > end - start){
start = i - (len-1)/2;
end = i + len/2;
}
}
return s.substring(start, end+1);
}
public int reverseLength(String s, int left, int right){
while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
left--;
right++;
}
return right - left - 1; //注意此时right和left已经更新,在最后一个匹配位置的下一个位置
}
}
动态规划
class Solution {
//动态规划,若一个子串是回文串,那给它前后加上相同字符后仍是回文串
//设dp[i][j]为以i开头,j结尾的字符串是否为回文串
//若dp[i+1][j-1]为true且s[i]=s[j],则dp[i][j]=true
//边界条件为,dp[i][i]=true即只有一个字符的情况,dp[i][i+1]=(s[i]==s[i+1])即两个字符的情况
public String longestPalindrome(String s) {
int n = s.length();
if(n < 2) return s;
boolean[][] dp = new boolean[n][n];
for(int i = 0; i < n; i++){
dp[i][i] = true;
}
int maxlen = 1; //注意maxlen不是从0开始,最少会有单个字符为回文串
int begin = 0;
char[] str = s.toCharArray();
for(int len = 2; len <= n; len++){ //遍历长度
for(int i = 0; i < n; i++){ //i为起始位置
int j = i + len - 1; //j为结束位置
if(j >= n) break;
if(str[i] != str[j]){
dp[i][j]=false;
}else{
if(j-i < 3){ //处理两个字符的情况
dp[i][j] = true;
}else{
dp[i][j] = dp[i+1][j-1];
}
}
if(dp[i][j] == true && j-i+1 > maxlen){
maxlen = j-i+1;
begin = i;
}
}
}
return s.substring(begin, begin + maxlen);
}
}
647.回文子串(中等)
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
class Solution {
//遍历每个可能的回文串中心,向外扩散的同时计数,注意中心有两种情况,即单个字符和两个字符
public int countSubstrings(String s) {
int n = s.length();
char[] str = s.toCharArray();
int count = 0;
int i = 0, j = 0;
for(int k = 0; k < n; k++){ //枚举中心
i = k; //一个中心的情况
j = k;
while(i >= 0 && j < n && str[i--] == str[j++]){
count++;
}
i = k; //两个中心的情况
j = k+1;
while(i >= 0 && j < n && str[i--] == str[j++]){
count++;
}
}
return count;
}
}