算法训练--字符串

300 阅读7分钟

算法训练--字符串

反转字符串

344. 反转字符串

  • 题目描述

    image.png

  • 题解

    class Solution {
        public void reverseString(char[] s) {
            int start=0,end=s.length-1;
            while(start<end){
                char temp=s[end];
                s[end]=s[start];
                s[start]=temp;
                start++;
                end--;
            }
        }
    }
    

541. 反转字符串 II

  • 题目描述

    image.png

  • 题解

    class Solution {
        public String reverseStr(String s, int k) {
            char[] ch=s.toCharArray();
            for(int i=0;i<ch.length;i+=2*k){
                //每隔2k个字符,对前k个字符进行反转
                //剩余字符小于2k但大于等于k个,则反转前k个字符
                if(i+k<=ch.length){
                    reverse(ch,i,i+k-1);
                    continue;//继续下轮循环
                }
                //剩余字符小于k个,反转剩余的全部字符
                reverse(ch,i,ch.length-1);
            }
            return new String(ch);
        }
        public void reverse(char[] ch, int start,int end){
            while(start<end){
                char temp=ch[end];
                ch[end]=ch[start];
                ch[start]=temp;
                start++;
                end--;
            }
        }
    }
    

剑指 Offer 05. 替换空格

  • 题目描述

    image.png

  • 题解一

      //使用一个新的对象,复制 str,复制的过程对其判断,是空格则替换,否则直接复制,类似于数组复制
    public static String replaceSpace(StringBuffer str) {
    	if (str == null) {
    		return null;
      }
    	StringBuilder sb = new StringBuilder();
      //使用 sb 逐个复制 str ,碰到空格则替换,否则直接复制
      for (int i = 0; i < str.length(); i++) {
      	if (s.charAt(i) == ' ') {
        	sb.append("%20");
         } else {
         	sb.append(str.charAt(i));
         }
      }
      	return sb.toString();
     }
    
  • 题解二

    替换空格

    /**
    	为什么选择从后往前填充?
    	答:从前向后填充就是$O(n^2)$的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动
    	
    	其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作
    	这么做有两个好处:
    	1.不用申请新数组
    	2.从后向前填充元素,避免了从前先后填充元素要来的 每次添加元素都要将添加元素之后的所有元素向后移动
    */
    
    class Solution {
        public String replaceSpace(String s) {
            //双指针法
            if(s==null || s.length()==0){
                return s;
            }
            int len=s.length();
            StringBuilder sb=new StringBuilder();
            for(int i=0;i<len;i++){
                //存在空格,扩充空间,数量为空格的2倍
                if(s.charAt(i)==' '){
                    sb.append("  ");
                }
            }
            //不存在空格
            if(sb.length()==0){
                return s;
            }
            int left=len-1;
            s+=sb.toString();
            int right=s.length()-1;
            char[] ch=s.toCharArray();
            while(left>=0){
                if(ch[left]==' '){
                    ch[right--]='0';
                    ch[right--]='2';
                    ch[right]='%';
                }else{
                    ch[right]=ch[left];
                }
                left--;
                right--;
            }
            return new String(ch);
        }
    }
    

208. 实现 Trie (前缀树)

  • 题目描述

    image.png

  • 题解

    • Trie,又称前缀树或字典树,是一棵有根树,其每个节点包含以下字段:

      指向子节点的指针数组 children。对于本题而言,数组长度为 26,即小写英文字母的数量。此时 children[0] 对应小写字母 a,children[1] 对应小写字母 b,children[25] 对应小写字母 z 布尔字段 isEnd,表示该节点是否为字符串的结尾

    • 插入字符串

      我们从字典树的根开始,插入字符串。对于当前字符对应的子节点,有两种情况:

      子节点存在。沿着指针移动到子节点,继续处理下一个字符 子节点不存在。创建一个新的子节点,记录在 children 数组的对应位置上,然后沿着指针移动到子节点,继续搜索下一个字符 重复以上步骤,直到处理字符串的最后一个字符,然后将当前节点标记为字符串的结尾

    • 查找前缀

      我们从字典树的根开始,查找前缀。对于当前字符对应的子节点,有两种情况:

      子节点存在。沿着指针移动到子节点,继续搜索下一个字符 子节点不存在。说明字典树中不包含该前缀,返回空指针 重复以上步骤,直到返回空指针或搜索完前缀的最后一个字符

      若搜索到了前缀的末尾,就说明字典树中存在该前缀。此外,若前缀末尾对应节点的isEnd 为真,则说明字典树中存在该字符串

    /**
    	基于数组化哈希的前缀树
    */
    class Trie {
        Trie[] children;
        boolean isEnd;
        /** Initialize your data structure here. */
        public Trie() {
            children=new Trie[26];
            isEnd=false;
        }
        
        /** Inserts a word into the trie. */
        public void insert(String word) {
            //得到根节点
            Trie node=this;
            for(char ch:word.toCharArray()){
                int index=ch-'a';
                if(node.children[index]==null){
                    node.children[index]=new Trie();
                }
                node=node.children[index];
            }
            node.isEnd=true;
        }
        
        /** Returns if the word is in the trie. */
        public boolean search(String word) {
            Trie node = searchPrefix(word);
            return node != null && node.isEnd;
        }
        
        /** Returns if there is any word 
        in the trie that starts with the given prefix. */
        public boolean startsWith(String prefix) {
            return searchPrefix(prefix) != null;
        }
        private Trie searchPrefix(String prefix) {
            Trie node = this;
            for (char ch:prefix.toCharArray()) {
                int index = ch - 'a';
                if (node.children[index] == null) {
                    return null;
                }
                node = node.children[index];
            }
            return node;
        }
    }
    

剑指 Offer II 062. 实现前缀树

  • 题目描述

    image.png

  • 题解

    class Trie {
        Trie[] children;
        boolean isEnd;
        public Trie() {
            children=new Trie[26];
            isEnd=false;
        }
        
        public void insert(String word) {
            //获取根节点
            Trie node=this;
            for(char ch:word.toCharArray()){
                int index=ch-'a';
                if(node.children[index]==null){
                    node.children[index]=new Trie();
                }
                node=node.children[index];
            }
            node.isEnd=true;
        }
        
        public boolean search(String word) {
            Trie node=searchPrefix(word);
            return node!=null && node.isEnd;
        }
        
        public boolean startsWith(String prefix) {
            return searchPrefix(prefix)!=null;
        }
        public Trie searchPrefix(String str){
            Trie node=this;
            for(char ch:str.toCharArray()){
                int index=ch-'a';
                if(node.children[index]==null){
                    return null;
                }
                node=node.children[index];
            }
            return node;
        }
    }
    

剑指 Offer II 063. 替换单词

  • 题目描述

    image.png

  • 题解一

    /**
    	哈希集合
    */
    class Solution {
        public String replaceWords(List<String> dictionary, String sentence) {
            Set<String> set=new HashSet<>();
            for(String str:dictionary){
                set.add(str);
            }
            String[] words=sentence.split(" ");
            for(int i=0;i<words.length;i++){
                String word=words[i];
                for(int j=0;j<word.length();j++){
                    if(set.contains(word.substring(0,j+1))){
                        words[i]=word.substring(0,j+1);
                        break;
                    }
                }
            }
            return String.join(" ",words);
        }
    }
    
  • 题解二

    与哈希集合不同,我们用 dictionary 中所有词根构建一棵字典树,并用特殊符号标记结尾。在搜索前缀时,只需在字典树上搜索出一条最短的前缀路径即可

    /**
    	Trie树,前缀树
    */
    class Solution {
        class Trie{
            Trie[] children;
            boolean isEnd;
            public Trie(){
                children=new Trie[26];
                isEnd=false;
            }
            public void insert(String word){
                Trie node=this;
                for(char ch:word.toCharArray()){
                    int index=ch-'a';
                    if(node.children[index]==null){
                        node.children[index]=new Trie();
                    }
                    node=node.children[index];
                }
                node.isEnd=true;
            }
            //查找单词的最短前缀
            //判断传入单词的某个前缀是否存在于字典树中,如果存在,返回该前缀的长度
            public int searchPrefix(String word){
                Trie node=this;
                for(int i=0;i<word.length();i++){
                    char ch=word.charAt(i);
                    int index=ch-'a';
                   	if(node.children[index]==null) return -1;
                    if(node.children[index].isEnd) return i;
                    node=node.children[index];
                }
                return -1;
            }
        }
    
        public String replaceWords(List<String> dictionary, String sentence) {
            Trie trie=new Trie();
            for(String str:dictionary){
                trie.insert(str);
            }
            String[] words=sentence.split(" ");
            StringBuilder sb=new StringBuilder();
            for(int i=0;i<words.length;i++){
                int end=trie.searchPrefix(words[i]);
                if(end==-1){
                    sb.append(words[i]);
                }else{
                    sb.append(words[i],0,end+1);
                }
                sb.append(" ");
            }
            // 删除最后一个多余的空格
            sb.deleteCharAt(sb.length()-1);
            return sb.toString();
        }
    }
    

151. 颠倒字符串中的单词

  • 题目描述

    image.png

  • 题解

    image.png

    class Solution {
        public String reverseWords(String s) {
            //去除多余空格
            StringBuilder sb=removeSpace(s);
            //反转整个字符串
            reverseAllWords(sb,0,sb.length()-1);
            //反转每个单词
            reverseEachWord(sb);
            return sb.toString();
        }
        public StringBuilder removeSpace(String s){
            int start=0;
            int end=s.length()-1;
            char[] ch=s.toCharArray();
            StringBuilder sb=new StringBuilder();
            //去除收尾空格
            while(ch[start]==' ') start++;
            while(ch[end]==' ') end--;
            while(start<=end){
                //多个连续空格
                if(ch[start]!=' ' || sb.charAt(sb.length()-1)!=' '){
                    sb.append(ch[start]);
                }
                start++;
            }
            return sb;
        }
    
        public void reverseAllWords(StringBuilder sb,int start,int end){
            while(start<end){
                char temp=sb.charAt(start);
                sb.setCharAt(start,sb.charAt(end));
                sb.setCharAt(end,temp);
                start++;
                end--;
            }
        }
    
        public void reverseEachWord(StringBuilder sb){
            int start=0;
            int end=1;
            int n=sb.length();
            while(start<n){
                while(end<n && sb.charAt(end)!=' '){
                    end++;
                }
                reverseAllWords(sb,start,end-1);
                start=end+1;
                end=start+1;
            }
        }
    }
    

剑指 Offer 58 - II. 左旋转字符串

  • 题目描述

    image.png

  • 题解

    class Solution {
        public String reverseLeftWords(String s, int n) {
            StringBuilder sb=new StringBuilder(s);
            int len=sb.length();
            //反转全部字符串 --->gfedcba
            reverseString(sb,0,len-1);
            //反转前s.length()-n个--->cdefgba
            reverseString(sb,0,len-n-1);
            //反转倒数n个字符 --->cdefgab  
            reverseString(sb,len-n,len-1);
            return sb.toString();
        }
        public void reverseString(StringBuilder sb,int start,int end){
            while(start<end){
                char temp=sb.charAt(start);
                sb.setCharAt(start,sb.charAt(end));
                sb.setCharAt(end,temp);
                start++;
                end--;
            }
        }
    }
    

28. 实现 strStr()

  • 题目描述

    image.png

  • 题解1:普通法

    class Solution {
        public int strStr(String haystack, String needle) {
            if("".equals(needle)){
                return 0;
            }
            int n=needle.length();
            int h=haystack.length();
            int i=0;
            for(int j=i+n;j<=h;j++){
                if(haystack.substring(i,j).equals(needle)){
                    return i;
                }
                i++;
            }
            return -1;
        }
    }
    

KMP

  • 什么是KMP,因为是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP

KMP有什么用

KMP主要应用在字符串匹配上

KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了

所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任

next数组是什么?next数组就是一个前缀表(prefix table)

前缀表

前缀表有什么用:前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配

**前缀表是如何记录的呢:**首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀

next数组匹配过程

KMP精讲2

构造next数组

  • 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串(首字母开头不含尾字母的组合)

  • 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串(不含首字母以尾字母结尾的子串组合)

    前缀表中记录的是最长相等的前后缀长度

    举例:aabaaf

    • 第一个子串a,首字母也是尾字母 最长相等前后缀为0
    • 第二个子串 aa,前缀:a, 后缀:a,最长相等前缀为1
    • 第三个子串 aab,前缀:a、aa 后缀:ab、b ,最长相等前后缀为0
    • 第四个子串 aaba,前缀:a、aa、aab后缀:aba、ba 、a,最长相等前后缀为1
    • 第五个子串aabaa,前缀:a、aa、aab、aaba 后缀:abaa、baa、aa、a,最长相等前后缀为2
    • 第六个子串aabaaf,前缀:a、aa、aab、aaba、aabaa 后缀:abaaf、baaf、aaf、af、f,最长相等的前后缀为0
  • 以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要!

    下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新新匹配就可以了

    所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力

    失败位置(不匹配的位置)时,就需要知道失败位置前的子串的最长相等前后缀的长度是多少(记录再前缀表中),也就对应着需要回退到的下标值,进行重新匹配

  • 构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步:

    1. 初始化
    2. 处理前后缀不相同的情况
    3. 处理前后缀相同的情况
  • 题解2:KMP

    /**
    	前缀表统一减一的实现
    */
    class Solution {
        public int strStr(String haystack, String needle) {
            if(needle=="") return 0;
            int[] next=new int[needle.length()];
            getNext(next,needle);
            int j=-1;
            for(int i=0;i<haystack.length();i++){
                while(j>=0 && haystack.charAt(i)!=needle.charAt(j+1)){
                    //不匹配,寻找j前一个前缀匹配的位置,比较的是j+1,所以前一位再next中的值就是next[j]
                    j=next[j];
                }
                if(haystack.charAt(i)==needle.charAt(j+1)){
                    j++;
                }
                if(j==needle.length()-1){
                    //完全匹配
                    return i-needle.length()+1;
                }
            }
            return -1;
        }
    
        public void getNext(int[] next,String s){
            int j=-1;
            next[0]=j;
            for(int i=1;i<s.length();i++){
                //前后缀不同
                while(j>=0 && s.charAt(i)!=s.charAt(j+1)){
                    //回退
                    j=next[j];
                }
                //找到相同的前后缀
                if(s.charAt(i)==s.charAt(j+1)){
                    j++;
                }
                //将j(前后缀的长度)赋给next[i]
                next[i]=j;
            }
        }
    }
    

459. 重复的子字符串

  • 题目描述

    image.png

  • 题解

    image.png

    class Solution {
        public boolean repeatedSubstringPattern(String s) {
            int len=s.length();
            int[] next=new int[s.length()];
            getNext(next,s);
            //next数组统一减一 计算时要加上1
            int max=next[len-1]+1;
            //
            if(max!=0 && len%(len-max)==0){
                return true;
            }else{
                return false;
            }
        }
    
        public void getNext(int[] next,String s){
            //统一减-构建next数组
            int j=-1;
            next[0]=j;
            for(int i=1;i<s.length();i++){
                while(j>=0 && s.charAt(i)!=s.charAt(j+1)){
                    j=next[j];
                }
                if(s.charAt(i)==s.charAt(j+1)){
                    j++;
                }
                next[i]=j;
            }
        }
    }
    

回文

125. 验证回文串

  • 题目描述

    image-20220403131236491.png

  • 题解

    class Solution {
        public boolean isPalindrome(String s) {
            StringBuffer sgood=new StringBuffer();
          	//去除标点符号和空格
            for(Character ch:s.toCharArray()){
                if(Character.isLetterOrDigit(ch)){
                    sgood.append(Character.toLowerCase(ch));
                }
            }
            int len=sgood.length();
            int l=0,r=len-1;
          	//双指针向中间收敛
            while(l<r){
                if(sgood.charAt(l)!=sgood.charAt(r)){
                    return false;
                }
                l++;
                r--;
            }
            return true;
        }
    }
    

680. 验证回文字符串 Ⅱ

  • 题目描述

    image-20220403132830010.png

  • 题解

    /**
    在允许最多删除一个字符的情况下,同样可以使用双指针,通过贪心实现。初始化两个指针low 和 high 分别指向字符串的第一个字符和最后一个字符。每次判断两个指针指向的字符是否相同,如果相同,则更新指针,将 low 加 1,high 减 1,然后判断更新后的指针范围内的子串是否是回文字符串。如果两个指针指向的字符不同,则两个字符中必须有一个被删除,此时我们就分成两种情况:即删除左指针对应的字符,留下子串s[low+1:high],或者删除右指针对应的字符,留下子串 s[low:high−1]。当这两个子串中至少有一个是回文串时,就说明原始字符串删除一个字符之后就以成为回文串
    */
    class Solution {
        public boolean validPalindrome(String s) {
            int len=s.length();
            int i=0,j=len-1;
            while(i<j){
                if(s.charAt(i)==s.charAt(j)){
                    i++;
                    j--;
                }else{
                    return (validPalindrome(s,i+1,j) || validPalindrome(s,i,j-1));
                }
            }
            return true;
        }
      
        public boolean  validPalindrome(String s,Integer left,Integer right){
            while(left<right){
                if(s.charAt(left)!=s.charAt(right)){
                    return false;
                }
                left++;
                right--;
            }
            return true;
        }
    }
    

647. 回文子串

  • 题目描述

    !image.png

  • 题解

    image-20220403142602454.png

    class Solution {
        public int countSubstrings(String s) {
            int res=0,len=s.length();
            char[] charArr=s.toCharArray();
            for(int i=0;i<2*len-1;i++){
                int l=i/2,r=i/2+i%2;
                while(l>=0 && r<len && charArr[l]==charArr[r]){
                 l--;
                 r++;
                 res++;
                } 
            }
            return res;
        }
    }
    

5. 最长回文子串

  • 题目描述

    image-20220403144109509.png

  • 题解

    image-20220403151902157.png

    class Solution {
        public String longestPalindrome(String s) {
            String res="";
            int len=s.length();
            if(len<2){
                return s;
            }
            int begin=0,maxLen=1;
          	//dp[i][j] 表示字符串s[i..j]是否为回文
            boolean[][] dp=new boolean[len][len];
          	//初始化,所有长度为1的字符串都是回文
            for(int i=0;i<len;i++){
                dp[i][i]=true;
            }
          	//先枚举子串的长度,从2开始到len
            for(int L=2;L<=len;L++){
              	//左边界,可以适当放宽松
                for(int l=0;l<len;l++){
                  	//有了左边界和子串长度,可以计算出右边界
                    int r=L+l-1;
                  	//右边界越界 退出循环
                    if(r>=len) break;
                    if(s.charAt(l)!=s.charAt(r)){
                        dp[l][r]=false;
                    }else{
                        if(r-l<3){
                            dp[l][r]=true;
                        }else{
                            dp[l][r]=dp[l+1][r-1];
                        }
                    }
                  	//只要 dp[i][L] == true 成立
                  	//就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
                    if(dp[l][r] && r-l+1>maxLen){
                        maxLen=r-l+1;
                        begin=l;
                    }
                }
                res=s.substring(begin,begin+maxLen);
            }
            return res;
        }
    }
    

131. 分割回文串

  • 题目描述

    image-20220403161033123.png

  • 题解:回溯 + 动态规划预处理

    class Solution {
        List<List<String>> res=new ArrayList();
        List<String> temp=new ArrayList();
        int n;
        boolean[][] dp;
        public List<List<String>> partition(String s) {
            n=s.length();
            dp=new boolean[n][n];
            for(int i=0;i<n;i++){
                dp[i][i]=true;
            }
            char[] charArr=s.toCharArray();
            for(int r=0;r<n;r++){
                for(int l=0;l<r;l++){
                    if(charArr[r]!=charArr[l]){
                        dp[l][r]=false;
                    }else{
                        if(r-l<3){
                            dp[l][r]=true;
                        }else{
                            dp[l][r]=dp[l+1][r-1];
                        }
                    }
                }
            }
            dfs(s,0);
            return res;
        }
    
        public void dfs(String s, Integer i){
            if(i==n){
                res.add(new ArrayList<String>(temp));
                return;
            }
            for(int j=i;j<n;j++){
                if(dp[i][j]){
                    temp.add(s.substring(i,j+1));
                    dfs(s,j+1);
                    temp.remove(temp.size()-1);
                }
            }
        }
    }
    

加法

415. 字符串相加

  • 题目描述

    image-20220405165853424.png

  • 题解

    /**
    	字符串加法、链表加法以及二进制加法之类的都可以这么写
    */
    class Solution {
        public String addStrings(String num1, String num2) {
            StringBuilder sb=new StringBuilder();
            int carry=0,i=num1.length()-1,j=num2.length()-1;
            while(i>=0 || j>=0 || carry!=0){
                if(i>=0) carry+=num1.charAt(i--)-'0';
                if(j>=0) carry+=num2.charAt(j--)-'0';
                sb.append(carry%10);
                carry/=10;
            }
            return sb.reverse().toString();
        }
    }
    

844. 比较含退格的字符串

  • 题目描述

    image.png

  • 题解

    class Solution {
        public boolean backspaceCompare(String s, String t) {
            int sSkipNum=0;  //记录s的#数量
            int tSkipNum=0;  //记录t的#数量
            int i=s.length()-1;
            int j=t.length()-1;
            while(true){
                while(i>=0){
                    //从后往前,消除s的#
                    if(s.charAt(i)=='#') sSkipNum++;
                    else{
                        if(sSkipNum>0){
                            sSkipNum--;
                        }else{
                            break;
                        }
                    }
                    i--;
                }
                while(j>=0){
                    //从后往前,消除t的#
                    if(t.charAt(j)=='#'){
                        tSkipNum++;
                    }else{
                        if(tSkipNum>0){
                            tSkipNum--;
                        }else{
                            break;
                        }
                    }
                    j--;
                }
                //后半部分#消除完了,接下来比较
                if(i<0 || j<0) break;//s或t遍历到头了
                if(s.charAt(i)!=t.charAt(j)){
                    return false;
                }
                i--;j--;
            }
            if(i==-1 && j==-1){
                //同时遍历完成
                return true;
            }else{
                return false;
            }
        }
    }
    

字符串总结

双指针法

  • 双指针法在数组,链表和字符串中很常用
    • 334.反转字符串、替换空格
  • 其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作
    • 那么针对数组删除操作的问题,其实在27. 移除元素 中就已经提到了使用双指针法进行移除操作
    • 同样的道理在151.翻转字符串里的单词中我们使用O(n)的时间复杂度,完成了删除冗余空格

反转系列

KMP

  • KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了

  • KMP的精髓所在就是前缀表

  • 前缀表:起始位置到下标i之前(包括i)的子串中,有多大长度的相同前缀后缀

    那么使用KMP可以解决两类经典问题:

    1. 匹配问题:28. 实现 strStr()
    2. 重复子串问题:459.重复的子字符串

    前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串

    后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串

CodeTop系列

14. 最长公共前缀

  • 题目描述

    image.png

  • 题解

    显然,最长公共前缀的长度不会超过字符串数组中的最短字符串的长度。用 minLength 表示字符串数组中的最短字符串的长度,则可以在 [0,minLength] 的范围内通过二分查找得到最长公共前缀的长度。每次取查找范围的中间值 mid,判断每个字符串的长度为mid 的前缀是否相同,如果相同则最长公共前缀的长度一定大于或等于 mid,如果不相同则最长公共前缀的长度一定小于 mid,通过上述方式将查找范围缩小一半,直到得到最长公共前缀的长度

    fig4

    class Solution {
        public String longestCommonPrefix(String[] strs) {
            if (strs == null || strs.length == 0) {
                return "";
            }
            int minLength = Integer.MAX_VALUE;
            for (String str : strs) {
                minLength = Math.min(minLength, str.length());
            }
            int low = 0, high = minLength;
            while (low < high) {
                int mid = (high - low + 1) / 2 + low;
                if (isCommonPrefix(strs, mid)) {
                    low = mid;
                } else {
                    high = mid - 1;
                }
            }
            return strs[0].substring(0, low);
        }
    
        public boolean isCommonPrefix(String[] strs, int length) {
            String str0 = strs[0].substring(0, length);
            int count = strs.length;
            for (int i = 1; i < count; i++) {
                String str = strs[i];
                for (int j = 0; j < length; j++) {
                    if (str0.charAt(j) != str.charAt(j)) {
                        return false;
                    }
                }
            }
            return true;
        }
    }
    

3. 无重复字符的最长子串

  • 题目描述

    image.png

  • 题解

    class Solution {
        public int lengthOfLongestSubstring(String s) {
            if(s.length()==0) return 0;
            Map<Character,Integer> map=new HashMap<>();
            int left=0,max=Integer.MIN_VALUE;
            for(int right=0;right<s.length();right++){
                char cRight=s.charAt(right);
                map.put(cRight,map.getOrDefault(cRight,0)+1);
                while(map.get(cRight)>1){
                    //出现重复
                    char cLeft=s.charAt(left);
                    map.put(cLeft,map.get(cLeft)-1);
                    left++;
                }
                max=Math.max(max,right-left+1);
            }
            return max;
        }
    }
    

5. 最长回文子串

  • 题目描述

    image.png

  • 题解

    class Solution {
        public String longestPalindrome(String s) {
            String  res="";
            int len=s.length();
            if(len<2) return s;
            boolean[][] dp=new boolean[len][len];
            for(int i=0;i<len;i++){
                dp[i][i]=true;
            }
            int maxLen=1,start=0;
            for(int L=2;L<=len;L++){
                for(int left=0;left<len;left++){
                    int right=L+left-1;
                    if(right>=len) break;
                    if(s.charAt(left)!=s.charAt(right)){
                        dp[left][right]=false;
                    }else{
                        if(right-left<3){
                            dp[left][right]=true;
                        }else{
                            dp[left][right]=dp[left+1][right-1];
                        }
                    }
                    if(dp[left][right] && right-left+1>maxLen){
                        maxLen=right-left+1;
                        start=left;
                    }
                }
                res=s.substring(start,start+maxLen);
            }
            return res;
        }
    }
    

415. 字符串相加

  • 题目描述

    image.png

  • 题解

    class Solution {
        public String addStrings(String num1, String num2) {
            int l1=num1.length()-1,l2=num2.length()-1;
            int carry=0;
            StringBuilder sb=new StringBuilder();
            while(l1>=0 || l2>=0 || carry!=0){
                if(l1>=0){
                    carry+=num1.charAt(l1)-'0';
                    l1--;
                }
                if(l2>=0){
                    carry+=num2.charAt(l2)-'0';
                    l2--;
                }
                sb.append(carry%10);
                carry/=10;
            }
            return sb.reverse().toString();
        }
    }
    

43. 字符串相乘

  • 题目描述

    image.png

  • 题解

    如果使用数组代替字符串存储结果,则可以减少对字符串的操作

    image.png

    img

    class Solution {
        public String multiply(String num1, String num2) {
            if (num1.equals("0") || num2.equals("0")) {
                return "0";
            }
            int m = num1.length(), n = num2.length();
            int[] ansArr = new int[m + n];
            for (int i = m - 1; i >= 0; i--) {
                int x = num1.charAt(i) - '0';
                for (int j = n - 1; j >= 0; j--) {
                    int y = num2.charAt(j) - '0';
                    ansArr[i + j + 1] += x * y;
                }
            }
            for (int i = m + n - 1; i > 0; i--) {
                ansArr[i - 1] += ansArr[i] / 10;
                ansArr[i] %= 10;
            }
            int index = ansArr[0] == 0 ? 1 : 0;
            StringBuffer ans = new StringBuffer();
            while (index < m + n) {
                ans.append(ansArr[index]);
                index++;
            }
            return ans.toString();
        }
    }
    

8. 字符串转换整数 (atoi)

  • 题目描述

    image.png

    image.png

  • 题解

    class Solution {
        public int myAtoi(String s) {
            char[] chars = s.toCharArray();
            int n = chars.length;
            int idx = 0;
            while (idx < n && chars[idx] == ' ') {
                // 去掉前导空格
                idx++;
            }
            if (idx == n) {
                //去掉前导空格以后到了末尾了
                return 0;
            }
            boolean negative = false;
            if (chars[idx] == '-') {
                //遇到负号
                negative = true;
                idx++;
            } else if (chars[idx] == '+') {
                // 遇到正号
                idx++;
            } else if (!Character.isDigit(chars[idx])) {
                // 其他符号
                return 0;
            }
            int ans = 0;
            while (idx < n && Character.isDigit(chars[idx])) {
                int digit = chars[idx] - '0';
                if (ans > (Integer.MAX_VALUE - digit) / 10) {
                    // 本来应该是 ans * 10 + digit > Integer.MAX_VALUE
                    // 但是 *10 和 + digit 都有可能越界,所有都移动到右边去就可以了。
                    return negative? Integer.MIN_VALUE : Integer.MAX_VALUE;
                }
                ans = ans * 10 + digit;
                idx++;
            }
            return negative? -ans : ans;
        }
    }
    

72. 编辑距离

  • 题目描述

    image.png

  • 题解

    class Solution {
        public int minDistance(String word1, String word2) {
            int l1 = word1.length();
            int l2 = word2.length();
            int[][] dp = new int[l1+1][l2+1];
            // 初始化
            for (int i=1; i<=l1; i++) {
                dp[i][0] = i;
            }
            for (int j=1; j<=l2; j++) {
                dp[0][j] = j;
            }
            for (int i=1; i<=l1; i++) {
                for (int j=1; j<= l2; j++) {
                    // 因为dp数组有效位从1开始
                    // 所以当前遍历到的字符串的位置为i-1 | j-1
                    if (word1.charAt(i-1) == word2.charAt(j-1)) {
                        dp[i][j] = dp[i-1][j-1];
                    } else {
                        dp[i][j] = Math.min(Math.min(dp[i-1][j-1], dp[i][j-1]), dp[i-1][j])+1;
                    }
                }
            }
            return dp[l1][l2];
        }
    }
    

165. 比较版本号

  • 题目描述

    image.png

  • 题解

    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;
        }
    }
    

151. 颠倒字符串中的单词

  • 题目描述

    image.png

  • 题解

    class Solution {
        public String reverseWords(String s) {
            StringBuilder sb=removeSpace(s);
            //反转整体字符串
            reverseAll(sb,0,sb.length()-1);
            //反转每隔单词
            reverseEachWord(sb);
            return sb.toString();
        }
    
        public StringBuilder removeSpace(String s){
            StringBuilder sb=new StringBuilder();
            char[] sArr=s.toCharArray();
            int start=0,end=sArr.length-1;
            while(sArr[start]==' ') start++;
            while(sArr[end]==' ') end--;
            while(start<=end){
                //多个连续空格
                if(sArr[start]!=' ' || sb.charAt(sb.length()-1)!=' '){
                    sb.append(sArr[start]);
                }
                start++;
            }
            return sb;
        }
    
        public void reverseAll(StringBuilder sb,int start,int end){
            while(start<end){
                char temp=sb.charAt(start);
                sb.setCharAt(start,sb.charAt(end));
                sb.setCharAt(end,temp);
                start++;
                end--;
            }
        }
    
        public void reverseEachWord(StringBuilder sb){
            int start=0,end=1;
            while(start<sb.length()){
                while(end<sb.length() && sb.charAt(end)!=' '){
                    end++;
                }
                reverseAll(sb,start,end-1);
                start=end+1;
                end=start+1;
            }
        }
    }
    

20. 有效的括号

  • 题目描述

    image.png

  • 题解

    class Solution {
        public boolean isValid(String s) {
            Stack<Character> stack=new Stack<>();
            char[] sArr=s.toCharArray();
            for(char c:sArr){
                if(c=='('){
                    stack.push(')');
                }else if(c=='{'){
                    stack.push('}');
                }else if(c=='['){
                    stack.push(']');
                }else{
                    if(stack.isEmpty() || stack.pop()!=c) return false;
                }
            }
            return stack.isEmpty();
        }
    }
    

22. 括号生成

  • 题目描述

    image.png

  • 题解

    class Solution {
        List<String> res=new ArrayList<>();
        StringBuilder sb=new StringBuilder();
        public List<String> generateParenthesis(int n) {
            backtracking(0,0,n);
            return res;
        }
        public void backtracking(int left,int right,int n){
            if(left==n && right==n){
                res.add(sb.toString());
                return;
            }
            if(left<n){
                sb.append("(");
                backtracking(left+1,right,n);
                sb.deleteCharAt(sb.length()-1);
            }
            if(right<left){
                sb.append(")");
                backtracking(left,right+1,n);
                sb.deleteCharAt(sb.length()-1);
            }
        }
    }
    

32. 最长有效括号

  • 题目描述

    image.png

  • 题解

    class Solution {
        public int longestValidParentheses(String s) {
            if(s.length()<=1) return 0;
            char[] sArr=s.toCharArray();
            int[] dp=new int[sArr.length];
            dp[1]= sArr[0]=='('&& sArr[1]==')'?2:0;
            int max=dp[1]; 
            for(int i=2;i<sArr.length;i++){
                if(sArr[i]=='(') dp[i]=0;
                else{
                    if(sArr[i-1]=='('){
                        dp[i]=dp[i-2]+2;
                    }else{
                        if(i-dp[i-1]-1<0 || sArr[i-dp[i-1]-1]==')'){
                            dp[i]=0;
                        }else{
                            dp[i]=i-dp[i-1]-1>=1?dp[i-1]+2+dp[i-dp[i-1]-2]:dp[i-1]+2;
                        }
                    }
                }
                max=Math.max(max,dp[i]);
            }
            return max;
        }
    }
    

227. 基本计算器 II

  • 题目描述

    image.png

  • 题解

    class Solution {
        public int calculate(String s) {
            int presign='+';
            int num=0;
            Deque<Integer> stack=new LinkedList<>();
            for(int i=0;i<s.length();i++){
                if(Character.isDigit(s.charAt(i))){
                    //保存当前数字,如:12是两个字符,需要进位累加
                    //记录当前数字。先减,防溢出
                    num=num*10-'0'+s.charAt(i);
                }
                if(s.charAt(i) != ' ' && !Character.isDigit(s.charAt(i)) || i==s.length()-1){
                    switch(presign){
                        case '+':
                            stack.push(num);
                            break;
                        case '-':
                            stack.push(-num);
                            break;
                        case '*':
                            stack.push(stack.pop()*num);
                            break;
                        default:
                            stack.push(stack.pop()/num);
                    }
                    presign=s.charAt(i);
                    num=0;
                }
            }
            int res=0;
            while(!stack.isEmpty()){
                res+=stack.pop();
            }
            return res;
        }
    }
    

93. 复原 IP 地址

  • 题目描述

    image.png

  • 题解

    class Solution {
        List<String> res=new ArrayList<>();
        public List<String> restoreIpAddresses(String s) {
            backtracking(s,0,0);
            return res;
        }
      	//startIndex: 搜索的起始位置,pointNum:添加逗点的数量
        public void backtracking(String s, int startIndex,int pointNum){
            if(pointNum==3){// 逗点数量为3时,分隔结束
                // 判断第四段子字符串是否合法,如果合法就放进result中
                if(isVaild(s,startIndex,s.length()-1)){
                    res.add(s);
                }
                return;
            }
            for(int i=startIndex;i<s.length();i++){
              	//判断 [startIndex,i] 这个区间的子串是否合法
                if(isVaild(s,startIndex,i)){
                    s=s.substring(0,i+1)+"."+s.substring(i+1);
                    pointNum++;
                  	//插入逗点之后下一个子串的起始位置为i+2
                    backtracking(s,i+2,pointNum);
                    pointNum--;
                    s=s.substring(0,i+1)+s.substring(i+2);
                }else{
                  	//不合法 直接退出循环
                    break;
                }
            }
        }
    		//判断字符串s在左闭又闭区间[start, end]所组成的数字是否合法
        public boolean isVaild(String s,int start,int end){
            if(start>end){
                return false;
            }
          	//0开头的数字不合法
            if(s.charAt(start)=='0' && start!=end){
                return false;
            }
            int num=0;
            for(int i=start;i<=end;i++){
                num=num*10+(s.charAt(i)-'0');
              	//如果大于255了不合法
                if(num>255) return false;
            }
            return true;
        }
    }
    

468. 验证IP地址

  • 题目描述(--)

    image.png

  • 题解

    class Solution {
        public String validIPAddress(String queryIP) {
            if (queryIP.indexOf('.') >= 0) {
                // IPv4
                int last = -1;
                for (int i = 0; i < 4; ++i) {
                    int cur = (i == 3 ? queryIP.length() : queryIP.indexOf('.', last + 1));
                    if (cur < 0) {
                        return "Neither";
                    }
                    if (cur - last - 1 < 1 || cur - last - 1 > 3) {
                        return "Neither";
                    }
                    int addr = 0;
                    for (int j = last + 1; j < cur; ++j) {
                        if (!Character.isDigit(queryIP.charAt(j))) {
                            return "Neither";
                        }
                        addr = addr * 10 + (queryIP.charAt(j) - '0');
                    }
                    if (addr > 255) {
                        return "Neither";
                    }
                    if (addr > 0 && queryIP.charAt(last + 1) == '0') {
                        return "Neither";
                    }
                    if (addr == 0 && cur - last - 1 > 1) {
                        return "Neither";
                    }
                    last = cur;
                }
                return "IPv4";
            } else {
                // IPv6
                int last = -1;
                for (int i = 0; i < 8; ++i) {
                    int cur = (i == 7 ? queryIP.length() : queryIP.indexOf(':', last + 1));
                    if (cur < 0) {
                        return "Neither";
                    }
                    if (cur - last - 1 < 1 || cur - last - 1 > 4) {
                        return "Neither";
                    }
                    for (int j = last + 1; j < cur; ++j) {
                        if (!Character.isDigit(queryIP.charAt(j)) && !('a' <= Character.toLowerCase(queryIP.charAt(j)) && Character.toLowerCase(queryIP.charAt(j)) <= 'f')) {
                            return "Neither";
                        }
                    }
                    last = cur;
                }
                return "IPv6";
            }
        }
    }