系列汇总:《刷题系列汇总》
——————《剑指offeer》———————
1. 替换空格(char[])
- 题目描述:请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为
We Are Happy
.则经过替换之后的字符串为We%20Are%20Happy
。 - 优秀思路1:利用字符数组
char[]
public String replaceSpace (String s) {
int length = s.length();
char[] result = new char[length*3];//最坏的情况是全为空格
int resLength = 0;//替换后的字符串长度
for(int i = 0;i<length;i++){
if(s.charAt(i) == ' '){
result[resLength++] = '%';
result[resLength++] = '2';
result[resLength++] = '0';
}else{
result[resLength++] = s.charAt(i);
}
}
// 字符数组转字符串
return new String(result,0,resLength);
}
- 优秀思路2:直接利用
String
已有的replace
方法
string replaceSpace(string s) {
// write code here
if(s.size()==0)return s;
for(auto i=0; i<s.size();i++)
{
if(s.at(i) == ' ')
s.replace(i,1,"%20");
}
return s;
}
2. 字符串的排列(困难)
- 题目描述:输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则按字典序打印出由字符
a,b,c
所能排列出来的所有字符串abc,acb,bac,bca,cab
和cba
。 - 优秀思路:先找到所有的全排列组合,再按字典序排序
- 第一步:将输入的字符串的第一个字符与后面每一个字符依次进行比较,若不同,则二者交换,由此第一个位置的字符就已经确定了
(第一步的结果是得到若干’第一个字符不同’的字符串) - 第二步:对第一步得到的字符串,保持第一个字符不动,从第二个位置开始依次跟后面的字符进行比较,不同则交换。由此第二个位置的字符也确定了。
(第二步的结果是得到若干’前两个字符不同’的字符串。) - 依次类推…直到对最后一个位置的字符进行如上操作时,停止交换。按字典排序输出所有字符串即可。
- 每一步的操作相同,故可用递归解决。 若将递归层层画出来,相当于一棵树结构,最深层的递归,也就是递归出口处理得到的结果,也即树的所有叶节点的处理就是我们需要的结果。
- 第一步:将输入的字符串的第一个字符与后面每一个字符依次进行比较,若不同,则二者交换,由此第一个位置的字符就已经确定了
import java.util.*;
public class Main{
public static ArrayList arraylist = new ArrayList();
private static HashSet hashset = new HashSet();
// 交换字符子函数
private static void swap(char[] str,int i,int j ) {
char temp = str[i];
str[i] = str[j];
str[j] = temp;
}
public static void permutation(char[] str,int start,int length) {
//递归出口,最后只有一个字符,不需要交换
if(start == length-1) {
hashset.add("\""+String.valueOf(str)+"\"");
}else {
for(int j=start;j<length;j++) {
if(str[start] == str[j] && start!=j) {
continue;
}
swap(str,j,start);
//确定一个在start位置的字符,再递归去判断后面start+1位置,该放哪个字符
permutation(str, start+1, length);
//换回来,以便往后判断是否需要跟start位置交换
swap(str,j,start);
}
}
}
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
String input;
//牛客网测试的平台估计是linux系统,换行是回车\n,在本地windows调试的话要用回车回格\r\n来换行结束第一轮输入。
// scan.useDelimiter("\r\n");
scan.useDelimiter("\n");
while(scan.hasNext()) {
input = scan.next();
permutation(input.toCharArray(),0,input.length());
if ("[]".equals(hashset.toString())){//空字符串
System.out.println("[]");
}else {
Iterator iterator = hashset.iterator();
while(iterator.hasNext()) {
arraylist.add(iterator.next());
}
Collections.sort(arraylist);
//arraylist的toString()方法会带有空格,要替换掉
System.out.println(arraylist.toString().replace(", ",","));
}
hashset.clear();
arraylist.clear();
}
}
}
3. 第一个只出现一次的字符
- 题目描述:在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).(从0开始计数)
- 我的思路(还行):循环遍历,新建一个ArrayList存储遍历过的重复字符索引
import java.util.*;
public class Solution {
public int FirstNotRepeatingChar(String str) {
if(str.length()==0 || str==null) return -1;
ArrayList<Integer> visited = new ArrayList<Integer>();
for(int i = 0;i<str.length();i++){ //注意最好不要把注意如果不满足循环条件则会直接跳出循环
if(visited.contains(i)) continue;
int count = 1;
for(int j = i+1;j<str.length();j++){
if(str.charAt(i)==str.charAt(j)){
count++;
visited.add(j);
}
}
if(count==1) return i;
}
return -1;
}
}
- 优秀思路:利用字符的ASCII码,依次将str里所有元素
— ‘A’
,将ASCII码差值作为索引,遇到重复字母则该索引处的值+1,最后输出为第一个值为1的字符索引
public class Solution {
public int FirstNotRepeatingChar(String str) {
if(str.length()==0 || str==null) return -1;
int[] result = new int[60];//ASCII码:字母最大为z-122,最小为A-65,最大差值57+1 = 58
char[] arr = str.toCharArray();
for(char c : arr) result[c -'A']++;
for(int i = 0;i<arr.length;i++){
if(result[arr[i] -'A']==1){
return i;
}
}
return -1;
}
}
4. 左旋转字符串
- 题目描述:汇编语言中有一种移位指令叫做循环左移
(ROL)
,现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”
,要求输出循环左移3位后的结果,即“XYZdefabc”
。是不是很简单?OK,搞定它! - 我的思路(优秀,代码还需精简):左移 n 位,则从原字符串第n位开始读入,再将0~ n-1位贴在后面即可
public class Solution {
public String LeftRotateString(String str,int n) {
// 边界条件
if(str.length()==0 || str == null) return str;
// 1.我的写法:效率较低
// int count = 0;
// int length = str.length();
// int validN = n % length;// 左移 length 位后 = 原字符串,故有效移位长度为 n % length
// char[] res = new char[length];
// char[] arr = str.toCharArray();
// for(int i = validN;i<arr.length;i++) res[count++] = arr[i];
// for(int i = 0;i < validN;i++) res[count++] = arr[i];
// return new String(res);
// 2.简要写法:利用String 的 substring方法
int validN = n % str.length();
String result = str.substring(validN,str.length()) + str.substring(0,validN);
return result;
}
}
5. 翻转单词顺序列
- 题目描述:将类似“student. a am I”的句子翻转成“I am a student.”
- 我的思路(还不错,蛮优秀):根据空格找到每段字符串,倒序组装起来
public class Solution {
public String ReverseSentence(String str) {
if(str.length()<2) return str;
// 1. 给字符串前后添加空格
str = ' ' + str + ' ';
// 2. 找出空格索引
int length = str.length();
int count = 0;//空格数
String res = ""; //注意是双引号
int[] spaceIndex = new int[length];
for(int i = 0;i < length;i++){
if(str.charAt(i)==' ') spaceIndex[count++] = i;
}
// 3. 根据空格索引找出每段字符串,倒序组装起来
for(int i = count-1;i > 0;i--){
if(i != 1) res += str.substring(spaceIndex[i-1]+1,spaceIndex[i]) + ' '; //最后一个字符串不加空格
else res += str.substring(spaceIndex[i-1]+1,spaceIndex[i]);
}
return res;
}
}
- 优秀思路:2次翻转,先翻转整个句子,再分别反转句子中的每个单词,优点是直接在原字符串上操作,不需要额外储存空间
public class Solution {
public String ReverseSentence(String str) {
if(str.length()<2) return str;
char[] arr = str.toCharArray();
// 1、翻转整个句子
reverse(arr,0,str.length()-1);
// 2、反转句子中的每个单词
int start = 0, end = 0; //每个单词对应的开始和结尾
while(start < arr.length){
if(arr[start]==' '){ //遇到空格后移1位
start++;
end++;
}else if(arr[end]==' '){ // 寻找到一个单词
reverse(arr,start,end-1);
end++;
start = end;
}else if(end == arr.length - 1){ //遍历到末尾(末尾是没有空格的哈)
reverse(arr,start,end);
return String.valueOf(arr);
}else{
end++;
}
}
return String.valueOf(arr);
}
private void reverse(char[] c,int start,int end){ // 注意:无返回值
while(start<=end){
char temp = c[start];
c[start++] = c[end];
c[end--] = temp;
}
}
}
6. 扑克牌顺子
- 题目描述:现在有五张扑克牌,我们需要来判断一下是不是顺子。
有如下规则:
-
A为1,J为11,Q为12,K为13
-
数据中的0可以看作任意牌
-
如果给出的五张牌能组成顺子(即这五张牌是连续的)就输出true,否则就输出false。
例如:给出数据[6,0,2,0,4]
中间的两个0一个看作3,一个看作5 。即:[6,3,2,5,4]
这样这五张牌在[2,6]区间连续,输出true
数据保证每组5个数字,每组最多含有4个零
- 我的思路(还不错):计算 0 的个数
n
及数组中的最大值max
和最小值min
- 若数组中除0外含相同数,则一定非顺子
- 若n≥4,则一定为顺子。
- 若 n≤3,max-min ≤ 4则为顺子,否则为非顺子
public class Solution {
public boolean IsContinuous(int [] numbers) {
if (numbers == null || numbers.length == 0) return false;
int zeroCount = 0; // 0 的数量
int min = Integer.MAX_VALUE; // 最小值:注意是除0外的最小值
int[] temp = new int[14]; // 牌面最大为 13
for(int i = 0;i < 5;i++){
if(numbers[i] == 0){
zeroCount++;
if(zeroCount == 4) return true; // 4个0以上必为顺子
}
else{
temp[numbers[i]]++;
if(temp[numbers[i]] > 1) return false; //存在0以外的相同数
if(numbers[i] < min) min = numbers[i];
if(numbers[i] - min > 4) return false; // 存在差值4以上的两个数必为非顺子
}
}
return true;
}
}
- 优秀思路:思路一致,寻找最大值和最代码更精简
public class Solution {
public boolean IsContinuous(int [] numbers) {
if (numbers == null || numbers.length == 0) return false;
int[] temp = new int[14]; // 牌面最大为 13
for(int i:numbers){
temp[i]++;
if(i != 0 && temp[i] > 1) return false; // 存在非0相同数
}
int min = 1; // 原数组最小值
while(min < temp.length && temp[min] == 0) min++;
int max = temp.length - 1; // 原数组最大值
while(max > 1 && temp[max] == 0) max--;
return max-min <= 4;
}
}
7. 把字符串转换成整数
- 题目描述:将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0
- 我的思路:利用ASCII码比较
public class Solution {
public int StrToInt(String str) {
// 边界条件
if(str.length() == 0 || str == null || (str.length() == 1 && (str.charAt(0) == '+' || str.charAt(0) == '-'))) return 0;
char[] arr = str.toCharArray();
for(char c : arr) if(c != '+' && c != '-' && !(c >= '0' && c <= '9')) return 0; //不为正负号或数字
return Integer.valueOf(new String(arr));
}
}
8. 正则表达式匹配(困难)
- 题目描述:请实现一个函数用来匹配包括
'.'
和'*'
的正则表达式。模式中的字符'.'
表示任意一个字符,而'*'
表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"
与模式"a.a"
和"ab*ac*a"
匹配,但是与"aa.a"
和"ab*a"
均不匹配 - 优秀思路:使用动态规划求解
- 开辟一个二维数组
dp[i][j]
来存放模式串的前j
个元素与字符串的前i
个元素是否匹配,初始化dp[0][0]
为true(表示两个空串是匹配的) - 如果第j个元素不是
'*'
,那么采用正常的匹配方式即模式串的第j个元素与字符串的第i个元素一样,或者模式串的第j个元素是‘.’
,dp[i][j]=dp[i-1][j-1]
- 如果第j个元素是
'*'
,那么分两种情况有一种情况为true即可- 第一种:将
'*'
和前面那个元素视为空串(这时'*'
号代表前面字符出现0次)那么dp[i][j-2]==true
即表示匹配成功,即dp[i][j] = dp[i][j-2]
- 第二种:
dp[i][j-2]!=true
那么就得是得第i
个元素与第j-1
个元素相等(此时代表的是出现1次)或者第j-1
个元素是‘.’
且dp[i-1][j]=true
即可
- 第一种:将
import java.util.*;
public class Solution {
public boolean match (String str, String pattern){
int s = str.length();
int p = pattern.length();
boolean[][] dp = new boolean[s+1][p+1];//00 用于存放两个空字符串的结果 dp[i][j] 表示【字符串第i个】与【模式串第j个】是否匹配
for(int i = 0;i<=s;i++){ // 实际上模式串和字符串的起点为1(所以后面的下标都是i-1 j-1)
for(int j =0;j<=p;j++){
if(j==0){
dp[i][j] = (i==0);//只有字符串和模式串都为空的时候才匹配,当模式串为空,字符串不为空则返回false
}else{
if(pattern.charAt(j-1)!='*'){ //如果第j-1个字符不是*
if(i>0 && (str.charAt(i-1) == pattern.charAt(j-1) || pattern.charAt(j-1) == '.')){
//正常匹配
dp[i][j] = dp[i-1][j-1];
}
}else{//如果第j个是* 那么分两种情况,有一种成立即可
//case 1 可以直接忽略*前模式的那个元素(*代表出现0次 比如a* 这两个元素做空字符串)
// 那么dp[i][j]==true 只需满足 dp[i][j-2]==true即可
if(j>=2){
dp[i][j] = dp[i][j-2];
}
//case 2 如果dp[i][j-2]不等于true那么要满足第j-1个字符(这个字符也可以为‘.’)与第i个字符匹配即可
//下标多减1是因为dp是从1开始记录的
if(i>0 && j>=2 &&(str.charAt(i-1)==pattern.charAt(j-2)||pattern.charAt(j-2)=='.')){
dp[i][j] |= dp[i-1][j];//使用或等于 两种情况有一种符合就行
}
}
}
}
}
return dp[str.length()][pattern.length()];
}
}
9. 表示数值的字符串(缺优秀思路)
- 题目描述:请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串
"+100","5e2","-123","3.1416"和"-1E-16"
都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"
都不是。 - 我的思路:列出数字的所有规则
- 不能出现除了 e/E 之外的其他字母
- 不能一个数字都没有
- 最多 1 个小数点
- 最多 1 个 e/E
- e/E 前必须为数字,e/E 后不能为空且必须跟整数
- 小数点不能出现在 e 后面
- 正负号只能出现在首位或者 e/E 后首位
import java.util.*;
public class Solution {
public boolean isNumeric (String str) {
if(str == null || str.length() == 0) return false;
char[] arr = str.toCharArray();
int pointNum = 0;
int signNum = 0; // 正负号
int eNum = 0; // e/E的数量
int numNum = 0; // 数字的数量
int pointPosition = 0;//小数点位置
int ePosition = 0; // e/E 位置
int signPosition = 0; // 正负号位置
for(int i = 0;i < arr.length;i++){
// 出现除了e/E之外的其他字母
if(arr[i] != '+' && arr[i] != '-' && arr[i] != 'e' && arr[i] != 'E' && arr[i] != '.' && (arr[i] < '0' || arr[i] > '9')){
return false;
}
// 不能一个数字都没有
if(arr[i] >= '0' && arr[i] <= '9'){
numNum++;
}
// 小数点数量最多1个
if(arr[i] == '.'){
pointPosition = i;//小数点位置
pointNum++;
if(pointNum > 1) return false;
}
// e/E 数量最多一个
if(arr[i] == 'e' || arr[i] == 'E'){
ePosition = i; // e/E 位置
// e/E后面不能为空 且 必须跟整数
if(arr.length == ePosition+1 || (arr.length == ePosition+2 && (arr[ePosition+1] == '+' || arr[ePosition+1] == '-'))) return false;
// e/E前面必须为数字
if(arr[ePosition-1] < '0' || arr[ePosition-1] > '9') return false;
eNum++;
if(eNum > 1) return false;
}
// e/E 前面可以出现小数点,后面不能出现(保证了e/E 后面跟的是整数)
if(pointNum >= 1 && eNum >= 1){
if(pointPosition >= ePosition) return false;
}
// 正负号只能出现在只能出现在第一位或者e后面第一位
if(arr[i] == '+' || arr[i] == '-'){
signPosition = i;
if(eNum >= 1){ //此时已经出现了e/E
if(signPosition != 0 && signPosition != ePosition+1) return false;
}else{ //此时未出现e/E
if(signPosition != 0) return false;
}
}
}
if(numNum == 0) return false; // 1个数字都没有
return true;
}
}
10. 字符流中第一个不重复的字符(和第3题思路一致)
- 题目描述:请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
后台会用以下方式调用Insert 和 FirstAppearingOnce 函数
string caseout = "";
1. 读入测试用例字符串casein
2. 如果对应语言有Init()函数的话,执行Init()
函数
3. 循环遍历字符串里的每一个字符ch {
Insert(ch);
caseout += FirstAppearingOnce()
}
- 输出
caseout
,进行比较。
- 优秀思路:
public class Solution {
StringBuffer sb = new StringBuffer(); //无需初始化,也可以用StringBuilder
int[] count = new int[128]; // 存储每个字符出现的次数
public void Insert(char ch){ // 往字节流中插入一个字符
sb.append(ch);
count[ch]++; // 空格是ASCII码表中最靠前的字符
}
// 返回现在的字节流中的第一个出现了1次的字符
public char FirstAppearingOnce(){
for(int i = 0;i < sb.length();i++){
if(count[sb.charAt(i)] == 1) return sb.charAt(i);
}
return '#';
}
}
——————《LeetCode》———————
1. 最长回文子串(困难)
- 题目描述:给你一个字符串 s,找到 s 中最长的回文子串。
- 基本方法:双层遍历,找出所有的回文子串,核心是识别回文子串的子方法的书写
// 判断 s 是否为回文子串
public boolean isPalindromic(String s) {
int len = s.length();
for (int i = 0; i < len / 2; i++) { // 回文子串特性:中心对称
if (s.charAt(i) != s.charAt(len - i - 1)) {
return false;
}
}
return true;
}
- 优秀思路:
- 最长公共子串:将原字符串s倒置成s‘,求出s和s’的最长公共字串,判断该子串是否回文,若回文则为所求答案。
- 中心扩散法:回文串一定是对称的,所以我们可以每次循环选择一个中心,进行左右扩展,判断左右字符是否相等即可。(分奇数和偶数讨论:由于存在奇数的字符串和偶数的字符串,所以我们需要从一个字符开始扩展,或者从两个字符之间开始扩展,所以总共有 n+n-1 个中心)
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) {
return "";
}
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i); //奇数串,中心为某个元素
int len2 = expandAroundCenter(s, i, i + 1); //偶数串,中心为某两个元素
int len = Math.max(len1, len2); // 更新子串最大长度
// 若产生最长回文子串:根据当前位置及回文子串长度,更新最长回文子串起始点
if (len > end - start + 1) {
// **核心**:start 和 end 的写法
start = i - (len - 1) / 2;
end = i + len / 2; // int中出现小数,会直接丢弃小数部分,例如 int(1/2) = 0,int(3/2) = int(1.5) = 1
}
}
return s.substring(start, end + 1); // substring-左开右闭
}
// 寻找当前左右点确定的中心点进行扩展得到的回文子串
public int expandAroundCenter(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是不满足条件的左右点,所以要-1
}
}
2. 有效的括号(栈)
-
题目描述:给定一个只包括
'(',')','{','}','[',']'
的字符串s
,判断字符串是否有效。有效字符串需满足:- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
-
优秀思路:考虑到后出现的左括号先匹配(后进先出),所以采用栈结构。利用哈希表存储配对括号方便快速查找,键为“右括号”,值为“左括号”。更简化做法是不用存储,遇到左括号则将其对应的右括号入栈,遇到右括号则判断栈顶元素是否为该右括号。
class Solution {
public boolean isValid(String s) {
//边界条件
if(s.length() == 0 || s == null || s.length() % 2 == 1) return false;
Stack<Character> stack = new Stack<Character>();
for(char c : s.toCharArray()){ // 记得加char
if(c == '(') stack.push(')');
else if(c == '[') stack.push(']');
else if(c == '{') stack.push('}');
else if(stack.isEmpty() || stack.pop() != c) return false;
}
// 全部匹配成功
if(stack.isEmpty()) return true;
return false;
}
}
3.自定义排序(困难,没看懂)
- 题目描述:
给你一个日志数组logs
。每条日志都是以空格分隔的字串,其第一个字为字母与数字混合的 标识符 。有两种不同类型的日志:- 字母日志:除标识符之外,所有字均由小写字母组成
- 数字日志:除标识符之外,所有字均由数字组成
- 请按下述规则将日志重新排序,返回日志的最终顺序:
- 所有 字母日志 都排在 数字日志 之前。
- 字母日志 在内容不同时,忽略标识符后,按内容字母顺序排序;在内容相同时,按标识符排序。
- 数字日志 应该保留原来的相对顺序。
- 优秀思路:使用
Java Arrays.sort()
重写比较器对于日志a,b
。根据空格分割字符串,得到字符串数组splitA
和splitB
。根据splitA[1]和splitB[1]
判断日志类型。根据规则进行排序。
class Solution {
public String[] reorderLogFiles(String[] logs) {
Arrays.sort(logs, (a, b) -> { // ->应该表示遍历比较的意思
//分割String[]全部字符串,split(String regex, int limit),limit表示分割的份数。
String[] splitA = a.split(" ", 2);
String[] splitB = b.split(" ", 2);
//判断日志类型
boolean aIsDigit = Character.isDigit(splitA[1].charAt(0));
boolean bIsDigit = Character.isDigit(splitB[1].charAt(0));
//a和b都是字母日志
if (!aIsDigit && !bIsDigit) {
//在内容不同时,忽略标识符,按内容字母顺序排序;
if (!splitA[1].equals(splitB[1])) {
return splitA[1].compareTo(splitB[1]);
}else { //在内容相同时,按标识符排序
return splitA[0].compareTo(splitB[0]);
}
}else if (aIsDigit && bIsDigit) {//a和b都是数字日志
//保留原来的相对顺序。
return 0;
}else if (!aIsDigit) { //a是字母日志
//所有字母日志都排在数字日志之前。
return -1;
}else {//b是字母日志
//所有字母日志都排在数字日志之前。
return 1;
}
});
return logs;
}
}
4.无重复字符的最长子串(滑动窗口)
-
题目描述:给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
-
优秀思路:利用滑动窗口框选当前的不重复子串,利用哈希表记录字母及其出现位置,方便查询重复元素及更新窗口边界,记录窗口的最大长度
-
1、首先,判断当前字符是否包含在
map
中,如果不包含,将该字符添加到map
(字符,字符在数组下标),此时没有出现重复的字符,左指针不需要变化。此时不重复子串的长度为:i-left+1
,与原来的maxLen
比较,取最大值; -
2、如果当前字符
ch
包含在map
中,此时有2类情况:- 1)当前字符包含在当前有效的子段中,如:
abca
,当我们遍历到第二个a
,当前有效最长子段是abc
,我们又遍历到a
, 那么此时更新left
为map.get(a)+1=1
,当前有效子段更新为bca
; - 2)当前字符不包含在当前最长有效子段中,如:
abba
,我们先添加a,b
进map
,此时left=0
,我们再添加b
,发现map
中包含b
, 而且b
包含在最长有效子段中,就是1)的情况,我们更新left=map.get(b)+1=2
,此时子段更新为b
,而且map
中仍然包含a,map.get(a)=0
; - 随后,我们遍历到a,发现a包含在
map
中,且map.get(a)=0
,如果我们像1)一样处理,就会发现left=map.get(a)+1=1
,实际上,left
此时应该不变,left
始终为2,子段变成ba
才对。为了处理以上2类情况,我们每次更新left,left=Math.max(left , map.get(ch)+1).
- 1)当前字符包含在当前有效的子段中,如:
-
另外,更新
left
后,不管原来的s.charAt(i)
是否在最长子段中,我们都要将s.charAt(i)
的位置更新为当前的i,因此此时新的s.charAt(i)
已经进入到 当前最长的子段中!
class Solution {
public int lengthOfLongestSubstring(String s) {
HashMap<Character,Integer> map = new HashMap<>(); // 因为要存储的形式是“字母-出现位置”,所以泛型定位为<Character, Integer>
int maxLen = 0;//用于记录最大不重复子串的长度
int left = 0;//滑动窗口左边界
for (int i = 0; i < s.length() ; i++){
if(map.containsKey(s.charAt(i))){ // 出现重复元素:左边界+1(右移一位,抛出重复的元素),为了应对abba这种情况,取max
left = Math.max(left,map.get(s.charAt(i))+1);
}
//不管是否更新left,都要更新 s.charAt(i) 的位置!
map.put(s.charAt(i) , i); // 覆盖放入字母的最新位置
maxLen = Math.max(maxLen , i-left+1);
}
return maxLen;
}
}
5.整数转换英文表示(分治)
- 题目描述:将非负整数
num
转换为其对应的英文表示。 - 优秀思路:每三位划分一个单位级,求出每个单位前的数字,对数字进行转换。(转换利用分治策略)
class Solution {
public String one(int num) {
switch(num) {
case 1: return "One";
case 2: return "Two";
case 3: return "Three";
case 4: return "Four";
case 5: return "Five";
case 6: return "Six";
case 7: return "Seven";
case 8: return "Eight";
case 9: return "Nine";
}
return "";
}
public String twoLessThan20(int num) {
switch(num) {
case 10: return "Ten";
case 11: return "Eleven";
case 12: return "Twelve";
case 13: return "Thirteen";
case 14: return "Fourteen";
case 15: return "Fifteen";
case 16: return "Sixteen";
case 17: return "Seventeen";
case 18: return "Eighteen";
case 19: return "Nineteen";
}
return "";
}
public String ten(int num) {
switch(num) {
case 2: return "Twenty";
case 3: return "Thirty";
case 4: return "Forty";
case 5: return "Fifty";
case 6: return "Sixty";
case 7: return "Seventy";
case 8: return "Eighty";
case 9: return "Ninety";
}
return "";
}
public String two(int num) {
if (num == 0)
return "";
else if (num < 10)
return one(num);
else if (num < 20)
return twoLessThan20(num);
else {
int tenner = num / 10;
int rest = num - tenner * 10;
if (rest != 0)
return ten(tenner) + " " + one(rest);
else
return ten(tenner);
}
}
public String three(int num) {
int hundred = num / 100;
int rest = num - hundred * 100;
String res = "";
if (hundred * rest != 0)
res = one(hundred) + " Hundred " + two(rest);
else if ((hundred == 0) && (rest != 0))
res = two(rest);
else if ((hundred != 0) && (rest == 0))
res = one(hundred) + " Hundred";
return res;
}
public String numberToWords(int num) {
if (num == 0)
return "Zero";
int billion = num / 1000000000;
int million = (num - billion * 1000000000) / 1000000;
int thousand = (num - billion * 1000000000 - million * 1000000) / 1000;
int rest = num - billion * 1000000000 - million * 1000000 - thousand * 1000;
String result = "";
if (billion != 0)
result = three(billion) + " Billion";
if (million != 0) {
if (! result.isEmpty())
result += " ";
result += three(million) + " Million";
}
if (thousand != 0) {
if (! result.isEmpty())
result += " ";
result += three(thousand) + " Thousand";
}
if (rest != 0) {
if (! result.isEmpty())
result += " ";
result += three(rest);
}
return result;
}
}
6.括号生成(无返回值递归函数)
- 题目描述:数字
n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的括号组合。 - 优秀思路:极其巧妙的利用无返回值递归函数解决了该问题
class Solution {
List<String> res = new ArrayList<>(); // res需定义在函数外
public List<String> generateParenthesis(int n) {
if(n<1) return res;
generateBrackets("",n,n);
return res;
}
private void generateBrackets(String str,int left,int right){
// 左右都用完了,产生一组结果
if(left==0 && right==0){
res.add(str);
return;
}
// left==right,下一个只能是左括号
if(left==right) generateBrackets(str+"(",left-1,right);
// left < right,左右均可
else{
if(left>0) generateBrackets(str+"(",left-1,right);
if(right>0) generateBrackets(str+")",left,right-1);
}
}
}
7. 移除无效的括号(栈/技巧变量)
- 题目描述:
-给你一个由'('、')'
和小写字母组成的字符串s
。你需要从字符串中删除最少数目的'('
或者')'
(可以删除任意位置的括号),使得剩下的「括号字符串」有效。
请返回任意一个合法字符串。有效「括号字符串」应当符合以下 任意一条 要求:- 空字符串或只包含小写字母的字符串
- 可以被写作 AB(A 连接 B)的字符串,其中 A 和 B 都是有效「括号字符串」
- 可以被写作 (A) 的字符串,其中 A 是一个有效的「括号字符串」
- 我的思路(效率较低):建立双栈,一个栈存储应被删除的元素索引,一个栈存储‘(’
class Solution{
public String minRemoveToMakeValid(String s){
Stack<Integer> indexRemove = new Stack<Integer>();
Stack<Character> bracket = new Stack<Character>();
for(int i = 0;i<s.length();i++){
if(s.charAt(i) == '('){
bracket.push('(');
indexRemove.push(i);
}else if(s.charAt(i) == ')'){
// 栈顶元素为'('
if(!indexRemove.isEmpty() && !bracket.isEmpty()){
if(bracket.peek() == '('){ //为空时无peek会报错
bracket.pop();
indexRemove.pop();
}
}else indexRemove.push(i);
}
}
// 删除无效括号
StringBuilder sb = new StringBuilder(s);
while(!indexRemove.isEmpty()){
sb.deleteCharAt(indexRemove.pop());
}
return sb.toString();
}
}
- 优秀思路:不用栈结构,巧妙地利用
balance
变量存储n左-n右
。① 先删除无效‘)’
:当balance ≥ 0
时该右括号有效,反之则无效;② 再根据balance
的值 删除多余的'('
class Solution{
public String minRemoveToMakeValid(String s){
// 为方便添加元素,采用 StringBuilder
StringBuilder sb = new StringBuilder();
int balance = 0; // 左括号多于右括号的数量
// 删除多余的')':当右括号多于'('时则该右括号无效
for (char c : s.toCharArray()){
if (c == '(') {
balance++;
sb.append(c);
}else if (c == ')'){
balance--;
if (balance < 0) balance = 0; // 右括号数 > 左括号
else sb.append(c);// 右括号数 ≤ 左括号(则该右括号有效)
}else sb.append(c);
}
// 删除多余的'('
int i = sb.length() - 1; // 从尾部开始删
while (balance != 0){ // 左括号多于右括号
if (sb.charAt(i) == '('){
balance--;
sb.deleteCharAt(i);
}
i--;
}
return sb.toString(); // StringBuilder 转 String
}
}
8. 文本左右对齐
- 题目描述:
给定一个单词数组和一个长度maxWidth
,重新排版单词,使其成为每行恰好有maxWidth
个字符,且左右两端对齐的文本。- 你应该使用“贪心算法”来放置给定的单词;也就是说,尽可能多地往每行中放置单词。
- 必要时可用空格
' '
填充,使得每行恰好有maxWidth
个字符。 - 要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数。
- 文本的最后一行应为左对齐,且单词之间不插入额外的空格。
- 我的思路(效率不高):每次根据
maxWidth
找出符合条件的一行,递归查找
class Solution {
List<String> res = new ArrayList(); // List是一个接口,不能被实现(即不能实例化),所以new List不行
public List<String> fullJustify(String[] words, int maxWidth) {
int len = 0;
int end = 0;
int flagLastLine = 1; //表明是否为最后一行
int wordsLen = words.length;
// 找出第一行能放置的最多单词索引
for(int i = 0;i < wordsLen;i++){
len += words[i].length()+1;
if(len - 1 > maxWidth){
end = i-1;
if(end==0) flagLastLine = 0; // 此时end=0表明不是最后一行
break;
}
}
// 注意 end = 0 有两种情况:① 只有一个单词;② 最后一行
if(end == 0 && flagLastLine == 1){ // 表明是最后一行
end = wordsLen - 1;
}else flagLastLine = 0;
// 取出第一行所能放置的所有单词
String[] Line = new String[end+1];
for(int i = 0;i < end+1;i++){
Line[i] = words[i];
}
order(Line,maxWidth,end,flagLastLine);
// 更新words:去掉第一行所有单词
if(end == wordsLen-1) return res;
else{
String[] newWords = new String[wordsLen-end-1];
int count = 0;
for(int i = end + 1;i<wordsLen;i++){
newWords[count++] = words[i];
}
fullJustify(newWords,maxWidth);
}
return res;
}
public void order(String[] Line,int maxWidth,int end,int flagLastLine){
String s = Line[0];
if(end == 0 || flagLastLine == 1){ // 最后一行或只有一个单词,左对齐
for(int i = 1;i<Line.length;i++){
s += " "+Line[i];
}
// 后面补空格
int numZero = maxWidth - s.length();
s += spaceSection(numZero);
}else{ // 两端对齐,且左边空格多于右边
int numSpaceSection = end; // 空格段数
int numSpace = maxWidth; // 总空格数
for(int i = 0;i < Line.length;i++){
numSpace -= Line[i].length();
}
String[] SpaceSection = findSpaceSection(numSpace,numSpaceSection);
int count = 0;
for(int i = 1;i < end+1;i++){
s += SpaceSection[count++] +Line[i];
}
}
res.add(s);
}
// 根据空格段数及总空格数确定每段的空格数,保证尽量均匀分配且左比右多
public String[] findSpaceSection(int num,int nsection){
String[] res = new String[nsection];
int baseNum = num/nsection;
int rest = num % nsection;
for(int i = 0;i < nsection;i++){
if(rest != 0){
res[i] = spaceSection(baseNum + 1);
rest--;
}else res[i] = spaceSection(baseNum);
}
return res;
}
// 产生指定长度的空格段
public String spaceSection(int num){
String s = "";
for(int i = 0;i<num;i++){
s += " ";
}
return s;
}
}
- 优秀思路:按单个单词处理,会出现以下三种情况
- 第一种是添加了当前单词后也不溢出行长度要求,这时候就直接放进来;
- 第二种就是加进来当前单词后就正好是行长度,这时候也可以直接放进来,不过需要再把缓冲区内容放到返回值中去;
- 第三种情况就比较复杂了,需要调整空格位置和数量。
class Solution {
public List<String> fullJustify(String[] words, int maxWidth) {
List<Integer> index = new ArrayList<>();
List<String> ans = new ArrayList<>();
StringBuilder str = new StringBuilder();
// 把单词挨个放到结果中去
for (int i = 0; i < words.length; ++i) {
if (str.length() + words[i].length() < maxWidth) {
// 如果当前单词加入到当前的行中时,没有超过要求,直接放进来好了
str.append(words[i]); // 拼接当前行内容
index.add(str.length());// 然后记下来当前空格位置
str.append(" ");
} else if (str.length() + words[i].length() == maxWidth) {
// 如果正好碰到了边界,那么就加进来当前单词后,放到最终的返回值中
str.append(words[i]);
ans.add(str.toString());
// 然后清空当前的缓冲内容
str = new StringBuilder();
index.clear();
} else {
// 如果添加了当前单词后,超出了容量限制,就进行空格调整
// 首先记录剩余多少空格
int space = maxWidth - str.length();
// 然后把最后一个空格去掉,把所有的空格放到中间去
if (index.size() > 1) {
str.deleteCharAt(str.length() - 1);
index.remove(index.size() - 1);
space += 1;
}
// 计算每一个单词中间的空格基本个数(every),以及额外的空格 (remain)
int every = 0, remain = 0;
if (!index.isEmpty()) {
every = space / index.size();
remain = space % index.size();
}
// 从后往前进行空格插入,这样方便计算下标在哪里
for (int j = index.size() - 1; j >= 0; --j) {
char[] cs = new char[every + (j < remain ? 1 : 0)];
Arrays.fill(cs, ' ');
str.insert(index.get(j), new String(cs));
}
// 然后放到返回值中
ans.add(str.toString());
str = new StringBuilder();
index.clear();
--i;
}
}
// 对剩余的单词进行空格拼接
if (str.length() > 0) {
if (str.length() < maxWidth) {
char[] cs = new char[maxWidth - str.length()];
Arrays.fill(cs, ' ');
str.append(new String(cs));
}
ans.add(str.toString());
}
return ans;
}
}
9. 字母异位词分组(哈希表)
- 题目描述:给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。
- 我的思路(效率低):
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
int len = strs.length;
List<List<String>> res = new ArrayList<>(); // List<List<String>>定义方式:前面尽管有两层,后面只能一层
List<String> tempList;
boolean[] cheaked = new boolean[len];
for(int i = 0;i<len;i++){
if(!cheaked[i]){
tempList = new ArrayList<>();
tempList.add(strs[i]);
cheaked[i] = true;
for(int j = i+1;j < len;j++){
if(!cheaked[j]){
if(judge(strs[i],strs[j])){
tempList.add(strs[j]);
cheaked[j] = true;
}
}
}
res.add(tempList);
}
}
return res;
}
private boolean judge(String s1,String s2){
if(s1.length() != s2.length()) return false;
int[] sum1 = new int[26];
int[] sum2 = new int[26];
for(char c:s1.toCharArray()){
sum1[c-'a']++;
}
for(char c:s2.toCharArray()){
sum2[c-'a']++;
}
if(Arrays.equals(sum1,sum2)) return true; // 比较两数组是否相同 Arrays.equals(sum1,sum2),不能用“==”
return false;
}
}
- 优秀思路:建立一个哈希表,键存储排序后的字符串,值存储字符串对应的所有字母异位词。最妙的是使用
getOrDefault()
方法
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
// 键存储排序后的字符串
// 值存储字符串对应的所有字母异位词
Map<String, List<String>> map = new HashMap<String, List<String>>();
for (String str : strs) {
char[] array = str.toCharArray();
Arrays.sort(array); // 字符数组排序
String key = new String(array); // 键存储排序后的字符串
// getOrDefault() 方法获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值,此处设置为 new ArrayList<String>()。
// 如果排序后相同(key相同),则取出之前的键值,将 新的 str add进去,再将list
List<String> list = map.getOrDefault(key, new ArrayList<String>());
list.add(str);
map.put(key, list); // 覆盖放入:list可能会更新
}
return new ArrayList<List<String>>(map.values());
}
}
10. 字符串相加(双指针)
- 题目描述:给定两个字符串形式的非负整数
num1
和num2
,计算它们的和。提示:- num1 和num2 的长度都小于 5100
- num1 和num2 都只包含数字 0-9
- num1 和num2 都不包含任何前导零
- 你不能使用任何內建 BigInteger 库, 也不能直接将输入的字符串转换为整数形式
- 我的思路:给短字符串补零,然后从末尾(个位)逐个相加、进位
class Solution {
public String addStrings(String s1, String s2) {
// 因为s1和s2的长度甚至会超过 long 的范围,所以不能转整型后计算
// 采用逐位相加,满十进一的做法
int dec = 0; //进位
int rest = 0;
int tempSum = 0; // 两位数之和
StringBuilder res = new StringBuilder(); // StringBuilder比建立空字符串相加更快
// 对较短字符串前面补0
int diffLen = 0;
if(s1.length() < s2.length()){
diffLen = s2.length() - s1.length();
for(int i = 0;i<diffLen;i++){
s1 = "0"+s1;
}
}else if(s1.length() > s2.length()){
diffLen = s1.length() - s2.length();
for(int i = 0;i<diffLen;i++){
s2 = "0"+s2;
}
}
for(int i = s1.length()-1;i>=0;i--){
tempSum = dec + s1.charAt(i) + s2.charAt(i) - 2*'0';
dec = tempSum/10; //进位
rest = tempSum - 10*dec; //有效位
res.append(rest);
}
// 最高位相加后可能存在进位
if(dec != 0) res.append(1);
res.reverse();
return res.toString();
}
}
- 优秀思路:巧妙地利用双指针解决了两个字符串长度不一致的情况
class Solution {
public String addStrings(String num1, String num2) {
int i1 = num1.length() - 1 , i2 = num2.length() - 1;
int dec = 0; // 进位
int add1 = 0; // 加数1
int add2 = 0; // 加数2
int sumTemp = 0; // 两位之和
StringBuilder res = new StringBuilder();
while(i1 >= 0 || i2 >= 0 || dec != 0){ // 进位条件不为0很重要
// 利用三元运算符解决长度不一致的问题
add1 = i1 >= 0 ? num1.charAt(i1--)-'0' : 0;
add2 = i2 >= 0 ? num2.charAt(i2--)-'0' : 0;
sumTemp = add1 + add2 + dec;
dec = sumTemp / 10;
res.append(sumTemp % 10);
}
res.reverse();
return new String(res);
}
}