【刷题】字符串

系列汇总:《刷题系列汇总》



——————《剑指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,cabcba
  • 优秀思路:先找到所有的全排列组合,再按字典序排序
    • 第一步:将输入的字符串的第一个字符与后面每一个字符依次进行比较,若不同,则二者交换,由此第一个位置的字符就已经确定了
      (第一步的结果是得到若干’第一个字符不同’的字符串)
    • 第二步:对第一步得到的字符串,保持第一个字符不动,从第二个位置开始依次跟后面的字符进行比较,不同则交换。由此第二个位置的字符也确定了。
      (第二步的结果是得到若干’前两个字符不同’的字符串。)
    • 依次类推…直到对最后一个位置的字符进行如上操作时,停止交换。按字典排序输出所有字符串即可。
    • 每一步的操作相同,故可用递归解决。 若将递归层层画出来,相当于一棵树结构,最深层的递归,也就是递归出口处理得到的结果,也即树的所有叶节点的处理就是我们需要的结果。
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. 扑克牌顺子

  • 题目描述:现在有五张扑克牌,我们需要来判断一下是不是顺子。
    有如下规则:
  1. A为1,J为11,Q为12,K为13

  2. 数据中的0可以看作任意牌

  3. 如果给出的五张牌能组成顺子(即这五张牌是连续的)就输出true,否则就输出false。

    例如:给出数据[6,0,2,0,4]
    中间的两个0一个看作3,一个看作5 。即:[6,3,2,5,4]
    这样这五张牌在[2,6]区间连续,输出true
    数据保证每组5个数字,每组最多含有4个零

  • 我的思路(还不错):计算 0 的个数n及数组中的最大值max和最小值min
  1. 若数组中除0外含相同数,则一定非顺子
  2. 若n≥4,则一定为顺子。
  3. 若 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"均不匹配
  • 优秀思路:使用动态规划求解
  1. 开辟一个二维数组 dp[i][j]来存放模式串的前j个元素与字符串的前i个元素是否匹配,初始化dp[0][0]为true(表示两个空串是匹配的)
  2. 如果第j个元素不是'*',那么采用正常的匹配方式即模式串的第j个元素与字符串的第i个元素一样,或者模式串的第j个元素是‘.’,dp[i][j]=dp[i-1][j-1]
  3. 如果第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"都不是。
  • 我的思路:列出数字的所有规则
    1. 不能出现除了 e/E 之外的其他字母
    2. 不能一个数字都没有
    3. 最多 1 个小数点
    4. 最多 1 个 e/E
    5. e/E 前必须为数字,e/E 后不能为空且必须跟整数
    6. 小数点不能出现在 e 后面
    7. 正负号只能出现在首位或者 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()
}

  1. 输出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;
}
  • 优秀思路
  1. 最长公共子串:将原字符串s倒置成s‘,求出s和s’的最长公共字串,判断该子串是否回文,若回文则为所求答案。
  2. 中心扩散法:回文串一定是对称的,所以我们可以每次循环选择一个中心,进行左右扩展,判断左右字符是否相等即可。(分奇数和偶数讨论:由于存在奇数的字符串和偶数的字符串,所以我们需要从一个字符开始扩展,或者从两个字符之间开始扩展,所以总共有 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 ,判断字符串是否有效。有效字符串需满足:

    1. 左括号必须用相同类型的右括号闭合。
    2. 左括号必须以正确的顺序闭合。
  • 优秀思路:考虑到后出现的左括号先匹配(后进先出),所以采用栈结构。利用哈希表存储配对括号方便快速查找,键为“右括号”,值为“左括号”。更简化做法是不用存储,遇到左括号则将其对应的右括号入栈,遇到右括号则判断栈顶元素是否为该右括号。

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。根据空格分割字符串,得到字符串数组splitAsplitB。根据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, 那么此时更新 leftmap.get(a)+1=1,当前有效子段更新为 bca
    • 2)当前字符不包含在当前最长有效子段中,如:abba,我们先添加a,bmap,此时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).
  • 另外,更新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. 字符串相加(双指针)

  • 题目描述:给定两个字符串形式的非负整数 num1num2 ,计算它们的和。提示:
    • 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);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值