字符串--JZ27、面试题5、58 - I、- II、67

面试题 5. 替换空格

在 Python 和 Java 等语言中,字符串都被设计成「不可变」的类型,即无法直接修改字符串的某一位字符,需要新建一个字符串实现。


  • 字符用单引号 ‘ ’
    字符串用双引号“ ”
    代码:
class Solution {
    
    
    public String replaceSpace(String s) {
    
    
        StringBuilder res = new StringBuilder();
        for(Character c : s.toCharArray()){
    
    
            if(c == ' ') res.append("%20");
            else res.append(c);
        }
        return res.toString();
        
    }
}




时间复杂度 O(N)
空间复杂度 O(N)

JZ27 字符串的排列

题目描述
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则按字典序打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

输入描述:
输入一个字符串,长度不超过9`(可能有字符重复)`,字符只包括大小写字母。
示例1
输入
"ab"
返回值
["ab","ba"]

:字典序 含义:
找本英汉字典,和那个排序方法一样。
对于字符串,先按首字符排序,如果首字符相同,再按第二个字符排序,以此类推。
如aa,ab,ba,bb,bc就是一个字典序。

题解:
在这里插入图片描述
在这里插入图片描述
上述③显然是①②的子问题,典型的递归思路;

但,需要注意的是,对于"ABB"来说,就会有重复,如图
在这里插入图片描述
我们用set【在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序】,它可以进行去重,并且可以达到按字母顺序排序。

递归三部曲为:

1、递归函数功能(原问题):
dfs(int pos, string s), 表示固定字符串s的pos下标的字符s[pos]

2、递归终止条件
当pos+1 == s.length()的时候,终止,表示对最后一个字符进行固定,也就说明,完成了一次全排列

3、下一步递归(子问题):
dfs(pos+1, s), 很显然,下一次递归就是对字符串的下一个下标进行固定

代码:
Java:Java版参考

class Solution {
    
    
    List<String> res = new LinkedList<>();
    char[] c;
    public String[] permutation(String s) {
    
    
        c = s.toCharArray();
        dfs(0);
        return res.toArray(new String[res.size()]);
    }


    void dfs(int pos){
    
    
        if(pos == c.length){
    
    
            res.add(String.valueOf(c));
            return;
        }
        HashSet<Character> set = new HashSet<>();
        for(int i = pos; i < c.length; i++){
    
    
            if(set.contains(c[i])) continue;
            set.add(c[i]);
            swap(pos , i);
            dfs(pos + 1);
            swap(pos , i);
        }
    }

    void swap(int pos , int i){
    
    
        char temp = c[i];
        c[i] = c[pos];
        c[pos] = temp;
    }
}

C++

class Solution {
    
    
public:
    vector<string> Permutation(string str) {
    
    
        if(str.empty()) return {
    
    };//vector的初始化形式其中一种是vector<string> a{1,2,3},这里是初始化为空
        set<string> ret;
        dfs(0, str, ret);
        return vector<string> ({
    
    ret.begin(),ret.end()});
        //return vector<string> {ret.begin(),ret.end()};
        //return vector<string> (ret.begin(),ret.end());都可
    }
    void dfs(int pos, string& s,set<string>& ret ){
    
    
        if(pos+1==s.size()){
    
    
            ret.insert(s);
            return;
        }
        //整个for循环是求所有可能出现在pos处的字符,操作就是交换s[pos]和它后面的字符,共有s.size()-pos种情况
        //每一次for循环在pos处固定一个字符
        //========================================================
        // 以str=“ABC”,pos=0为例,for循环和swap的含义:
        // 第一次for循环 'A' 与 'A'交换,字符串为"ABC", 相当于固定'A'
        // 第二次for循环 'A' 与 'B'交换,字符串为"BAC", 相当于固定'B'
        // 第三次for循环 'A' 与 'C'交换,字符串为"CBA", 相当于固定'C'
        //=============================================
        for(int i=pos; i<s.size(); i++){
    
    
            swap(s[pos], s[i]);
            dfs(pos+1, s, ret);//接着固定下一个字符;
            //下面 回溯的原因: 上面在固定第pos个字符时,进行过一次交换;为了下次for循环能用swap(s[pos], s[i])固定pos处的下一种情况,需还原后,在进行交换。
            //继续以上面的例子为例:比如第二次交换后是"BAC",需要回溯到"ABC"。 然后进行第三次交换,才能得到"CBA"
            swap(s[pos], s[i]);
        }
    }
};

另外,dfs(int pos, string& s,set<string>& ret )函数中的引用代替了全局变量。
有全局变量的代码如下(只有一点改变)

class Solution {
    
    
public:
    set<string> ret;//全局变量
    string s;//全局变量
    vector<string> Permutation(string str) {
    
    
        s=str;
        if(str.empty()) return {
    
    };
        dfs(0);//使用全局变量后,dfs()的参数减少。
        return vector<string> ({
    
    ret.begin(),ret.end()});
    }
    void dfs(int pos){
    
    
        if(pos+1==s.size()){
    
    
            ret.insert(s);
            return;
        }
        for(int i=pos; i<s.size(); i++){
    
    
            swap(s[pos], s[i]);
            dfs(pos+1);
            swap(s[pos], s[i]);
        }
    }
};

时间复杂度 O(N!) : N 为字符串 s 的长度;时间复杂度和字符串排列的方案数成线性关系,方案数为 N×(N−1)×(N−2)…×2×1 ,因此复杂度为 O(N!) 。
空间复杂度 O(N^2) : 全排列的递归深度为 N ,系统累计使用栈空间大小为 O(N) ;递归中辅助 Set 累计存储的字符数量最多为 N + (N-1) + … + 2 + 1 = (N+1)N/2 ,即占用 O(N^2) 的额外空间。

面试题58 翻转字符串

面试题58 - I 翻转单词顺序

输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. “,则输出"student. a am I”。

示例 1:

输入: "the sky is blue"
输出: "blue is sky the"
示例 2:

输入: "  hello world!  "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
示例 3:

输入: "a good   example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
 

说明:

无空格字符构成一个单词。
输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

题解:
方法一:双指针

  • 倒序遍历字符串 s ,记录单词左右索引边界 i , j ;
  • 每确定一个单词的边界,则将其添加至单词列表 res ;
  • 最终,将单词列表拼接为字符串,并返回即可。
    代码(Java):
class Solution {
    
    
    public String reverseWords(String s) {
    
    
        s = s.trim(); // 删除首尾空格
        int j = s.length() - 1, i = j;
        StringBuilder res = new StringBuilder();
        while(i >= 0) {
    
    
            while(i >= 0 && s.charAt(i) != ' ') i--; // 搜索首个空格
            res.append(s.substring(i + 1, j + 1) + " "); // 添加单词
            while(i >= 0 && s.charAt(i) == ' ') i--; // 跳过单词间空格
            j = i; // j 指向下个单词的尾字符
        }
        return res.toString().trim(); // 转化为字符串并返回
    }
}

时间复杂度 O(N) : 其中 N 为字符串 s 的长度,线性遍历字符串。
空间复杂度 O(N) : 新建的 StringBuilder(Java) 中的字符串总长度<= N ,占用 O(N) 大小的额外空间。

方法二、两次翻转

  • 第一次翻转:翻转整个字符串
  • 第二次翻转:翻转每个单词
  • 另外需对多余的空格进行处理

代码:(这是参考别人的代码,自己写的代码不知为啥不通过)

class Solution {
    
    
public:
    string reverseWords(string s) {
    
    
        if (s.length() == 1 && s[0] == ' ') {
    
    
            // 特殊情况:直输入一个空格(s = " "),返回空字符串
            return "";
        }

        trim(s); // 先去除 s 首尾的空格
        reverse(s, 0, s.length() - 1); // 将整个 s 翻转

        int i = 0, j = 0; // i 和 j 用于定位一个单词的首和尾(左闭右闭)

        while (j < s.length()) {
    
    
            if (s[j] != ' ') {
    
    
                j++;

                if (j == s.length()) {
    
    
                    // 如果此时是最后一个单词,那么 j 此刻等于 s.length()
                    // 为避免直接退出循环而导致最后一个单词没有被处理,于是在此手动处理
                    reverse(s, i, j - 1);
                    break;
                }
            } 
            else {
    
     // j 当前指向空格
                reverse(s, i, j - 1); // 翻转 [i, j - 1] 区间内的单词
                j++; // 看当前空格后面还有没有多余的空格

                while (j < s.length() && s[j] == ' ') {
    
    
                    s.erase(j, 1);
                }

                i = j; // i 定位到下一个单词的起始处
            }
        }

        return s;
    }

    void trim(string& str) {
    
    
        // 去除一个字符串首尾的空格
        if (str.empty()) {
    
    
            return;
        }

        str.erase(0, str.find_first_not_of(' '));
        str.erase(str.find_last_not_of(' ') + 1);
    }

    void reverse(string& str, int start, int end) {
    
    
        // 翻转一个字符串
        if (end - start < 1 || end >= str.length()) {
    
    
            return;
        }

        while (start < end) {
    
    
            char temp = str[start];
            str[start] = str[end];
            str[end] = temp;
            start++; end--;
        }
    }
};

时间复杂度 O(N)
空间复杂度 O(1)

面试题58- II. 左旋转字符串

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

示例 1:

输入: s = "abcdefg", k = 2
输出: "cdefgab"
示例 2:

输入: s = "lrloseumgh", k = 6
输出: "umghlrlose"

题解:
由58 - I的翻转方法启发,翻转三次即可,假设左旋转前n位:

  • 先整体翻转一次;
  • 然后分别单独翻转后n位,和前面剩余的字符串;
  • 完成

代码:

class Solution {
    
    
public:
    string reverseLeftWords(string s, int n) {
    
    
        if(n==0 || n > s.length() || s.length()<=1){
    
    
            return s;
        }
        reverse(s, 0, s.length()-1);
        reverse(s, 0, s.length()-n-1);
        reverse(s, s.length()-n, s.length()-1);
        return s;

    }

    void reverse(string& s, int left, int right){
    
    
        while(left<right){
    
    
            char temp = s[left];
            s[left]= s[right];
            s[right]=temp;
            left++;
            right--;
        }
        return;
    }
};

时间复杂度 O(N)
空间复杂度 O(1)

面试题Offer 67. 把字符串转换成整数

写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。

首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。

当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。

该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。

注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。

在任何情况下,若函数不能进行有效的转换时,请返回 0。

说明:

假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,请返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。

示例 1:

输入: "42"
输出: 42
示例 2:

输入: "   -42"
输出: -42
解释: 第一个非空白字符为 '-', 它是一个负号。
     我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。
示例 3:

输入: "4193 with words"
输出: 4193
解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。
示例 4:

输入: "words and 987"
输出: 0
解释: 第一个非空字符是 'w', 但它不是数字或正、负号。
     因此无法执行有效的转换。
示例 5:

输入: "-91283472332"
输出:  -2147483648
解释: 数字 "-91283472332" 超过 32 位有符号整数范围。 
     因此返回 INT_MIN (231)

题解:
根据题意,有以下四种字符需要考虑:

  • 首部空格: 删除之即可;
  • 符号位: 三种情况,即 ‘’+’’ , ‘‘−’’ , ''无符号" ;新建一个变量保存符号位,返回前判断正负即可。
  • 非数字字符: 遇到首个非数字的字符时,应立即返回。
    数字字符:
  • 字符转数字: “此数字的 ASCII 码” 与 “ 0 的 ASCII 码” 相减即可;
  • 数字拼接: 若从左向右遍历数字,设当前位字符为 c ,当前位数字为 x ,数字结果为 res ,则数字拼接公式为:

x = ASCII(c) - ASCII(0) 即, x = c - ‘0’;
res = 10 * res + x //res初始值为0;

  • 数字越界判断处理
    1、拼接前判断
    若将res声明成int类型,即int = res,则必须在拼接前判断是否越界,因为若某一次拼接后超过边界,如示例5,某一次拼接后res=91283472332,超过边界,res保存不了91283472332,res和边界(INT_MAX 、 INT_MIN)的大小也就无法判断出正确的结果;

拼接前判断,边界的设置:

设数字拼接边界 bndry = 2147483647 // 10 = 214748364 ,则以下两种情况越界:
在这里插入图片描述

关于res == bndry , x> '7',对于INT_MIN明明是-2147483648,但是为什么却只需要判断当前字符是否大于7?

简单来说就是如果直接向int变量赋值-2147483648系统会报错,虽然理论上确实可以取到,但是直接赋值不行,下面是详细讨论。

即INT_MIN明明是-2147483648,但是为什么却只需要判断当前字符是否大于7,因为按常理来说当符号为负时str.charAt(j)为’8’也是不越界的,但是为什么这里只判断‘7’呢? 这是因为我们如果直接向一个int变量赋值-2147483648,系统是会报错的(至少在C++中是这样),我们如果想要返回这个数那就需要使用INT_MIN,因为INT_MIN=-2147483648。 那么我们回过头来看这题,考虑字符串为“-2147483648”这种情况,即res==214748364的前提下,当前字符是‘8’,那么按照最常规的思路,这个数是在int的取值范围中,那么我要存放它,但是int变量并不能直接存放这个数,因此我要找一个与-2147483648相等的,可以表示它的数,那就是INT_MIN。因此即使符号是负号,我们仍然可以将判断条件写为 str.charAt(j) > ‘7’ ,因为当取‘8’时我们返回的也是INT_MIN。

关于为什么不能直接向int变量赋值-2147483648,我搜索了一些资料,大致意思是说-2147483648是一个常量表达式而非常量,系统会把它分成两部分,即负号 - 和 数字 2147483648,因此会出现越界的情况。

当然啦,即使int变量能存放下-2147483648,我们依然可以这么写 str.charAt(j) > ‘7’ ,只是这么些会有一点违反我们的直觉,需要思考一下过程才能理解,因此在这里分享一点我的看法,希望能帮助到有同样疑惑的朋友。

2、拼接后判断
若将res声明成long类型,即long= res,则拼接前后都可以判断越界,但由于最终需返回int类型,所以最后要强制转换成int,即,(int)(res);

代码:

class Solution {
    
    
public:
    int strToInt(string str) {
    
    
        int len = str.size();
        if(len == 0) return 0;
        int i = 0;
        while(str[i] == ' '){
    
    
            if(i++ == len) return 0;
        }
        int sign = 1;
        if(str[i] == '-') sign = -1;
        if(str[i] == '-' || str[i] == '+') ++i;
        int res = 0;
        int binary = INT_MAX/10;
        for(int j = i; j<len; j++){
    
    
            //字符串中的第一个非空格字符不是一个有效整数字符返回 0;
            if(str[j]<'0' || str[j]>'9') break;
            if(res > binary || res == binary && str[j] > '7')
             return sign == 1?  INT_MAX :INT_MIN;
            res = 10*res + (str[j] - '0');
        }
        return sign*res;
    }
};

时间复杂度 O(N) : 其中 N 为字符串长度,线性遍历字符串占用 O(N)时间。
空间复杂度: O(1)

知识点

1、
字典序 含义:
找本英汉字典,和那个排序方法一样。
对于字符串,先按首字符排序,如果首字符相同,再按第二个字符排序,以此类推。
如aa,ab,ba,bb,bc就是一个字典序。
2、
set【在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序】
3、
函数参数为引用形式代替全局变量。

猜你喜欢

转载自blog.csdn.net/qq_42647047/article/details/112181978