代码随想录算法训练营day08|| 344.反转字符串、541. 反转字符串II、卡码网:54.替换数字、151.翻转字符串里的单词、卡码网:55.右旋转字符串

344.反转字符串

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。

示例 1:
输入:["h","e","l","l","o"]
输出:["o","l","l","e","h"]

思路:在反转链表中,使用了双指针的方法。

那么反转字符串依然是使用双指针的方法,只不过对于字符串的反转,其实要比链表简单一些。

因为字符串也是一种数组,所以元素在内存中是连续分布,这就决定了反转链表和反转字符串方式上还是有所差异的。

对于字符串,我们定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。

以字符串hello为例,过程如下:

代码随想录算法训练营day08|| 344.反转字符串、541. 反转字符串II、卡码网:54.替换数字、151.翻转字符串里的单词、卡码网:55.右旋转字符串_第1张图片

class Solution {
    public void reverseString(char[] s) {
        int left = 0;
        int right = s.length - 1;
        while(left <  right){
            //固定三行代码:交换语句
            char temp = s[left];
            s[left] = s[right];
            s[right] = temp;
            left ++;
            right --;
        }
    }
}
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

注意:

如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。

如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。

在字符串相关的题目中,库函数对大家的诱惑力是非常大的,因为会有各种反转,切割取词之类的操作,这也是为什么字符串的库函数这么丰富的原因。

 541. 反转字符串II

给定一个字符串 s 和一个整数 k,从字符串开头算起, 每计数至 2k 个字符,就反转这 2k 个字符中的前 k 个字符。

如果剩余字符少于 k 个,则将剩余字符全部反转。

如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。

示例:

输入: s = "abcdefg", k = 2
输出: "bacdfeg"

思路:(本题需要注意的是for循环的表达式上,不要思维定式的只会i++)

这道题目其实也是模拟,实现题目中规定的反转规则就可以了。

一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。

其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。

因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。

所以当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。

 那么这里具体反转的逻辑我们要不要使用库函数呢,其实用不用都可以,使用reverse来实现反转也没毛病,毕竟不是解题关键部分。(java语言可能需要自己写一下)

class Solution {
    public String reverseStr(String s, int k) {
        char[] ch = s.toCharArray();//首先将字符串转化为字符串数组格式
        //1、每隔2k 个字符的前k 个字符进行反转
        for(int i = 0; i < ch.length; i += 2*k){
            //2、判断一下 剩余字符小于2k个但大于k个,则反转前k个字符
            if(i + k <= ch.length){
                reverse(ch,i,i + k-1);
                continue; //不要漏掉这个continue!!!
            }
            //3、剩余字符少于k个,则全部反转
            reverse(ch,i,ch.length  - 1);
        }
        return  new String(ch); //new String(数组) 把数组转为字符串
    }

    //定义翻转函数reverse
    public void reverse(char[] ch, int i, int j){
        for( ; i < j ; i++,j--){
            char temp = ch[i];
            ch[i] = ch[j];
            ch[j] = temp;
        }
    }
}
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)或O(n), 取决于使用的语言中字符串是否可以修改.

 卡码网:54.替换数字

给定一个字符串 s,它包含小写字母和数字字符,请编写一个函数,将字符串中的字母字符保持不变,而将每个数字字符替换为number。 例如,对于输入字符串 "a1b2c3",函数应该将其转换为 "anumberbnumbercnumber"。

输入:一个字符串 s,s 仅包含小写字母和数字字符。

输出:打印一个新的字符串,其中每个数字字符都被替换为了number

样例输入:a1b2c3

样例输出:anumberbnumbercnumber

数据范围:1 <= s.length < 10000。

 思路 :如果想把这道题目做到极致,就不要只用额外的辅助空间了! (不过使用Java刷题的录友,一定要使用辅助空间,因为Java里的string不能修改)

首先扩充数组到每个数字字符替换成 "number" 之后的大小。(对于C++而言  )

例如 字符串 "a5b" 的长度为3,那么 将 数字字符变成字符串 "number" 之后的字符串为 "anumberb" 长度为 8。

如图:

代码随想录算法训练营day08|| 344.反转字符串、541. 反转字符串II、卡码网:54.替换数字、151.翻转字符串里的单词、卡码网:55.右旋转字符串_第2张图片

然后从后向前替换数字字符,也就是双指针法,过程如下:i指向新长度的末尾,j指向旧长度的末尾。

代码随想录算法训练营day08|| 344.反转字符串、541. 反转字符串II、卡码网:54.替换数字、151.翻转字符串里的单词、卡码网:55.右旋转字符串_第3张图片

为什么要从后向前填充,从前向后填充不行么?

从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素整体向后移动。

其实很多数组填充类的问题,其做法都是先预先给数组扩容带填充后的大小,然后在从后向前进行操作。

这么做有两个好处:

  1. 不用申请新数组。
  2. 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。

Java语言的代码:

补充函数:
①CharacterisDigit()方法用于判断指定字符是否为数字
语法: boolean  isDigit(char ch)
参数: ch -- 要测试的字符。
返回值: 如果字符为数字,则返回 true;否则返回 false

②charAt(i)函数 是获取字符串中i位置的字符

功能:获取字符串中 i 位置的字符。【通过索引位置找字符。】

索引范围是从0到length() - 1。

是一个具有查询功能的函数。

str.charAt(i)的意思是第i+1个字符在字符串str中所占的位置,输出的是字符;

import java.util.Scanner;

class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        String s = in.nextLine();
        StringBuilder sb = new StringBuilder();//定义可变字符串StringBuilder
        for (int i = 0; i < s.length(); i++) {
            if (Character.isDigit(s.charAt(i))) {//判断是否有数字,有的话就在字符串末尾加number
                sb.append("number");
            }else sb.append(s.charAt(i)); //没有的话就直接把这个字母加到末尾
        }
        System.out.println(sb);
    }
}

关于java中字符串是否可变问题:

对于String的学习:

final 修饰了String类所以String类是无法被修改的,并且不是基本数据类型。

当我们用双引号创建一个字符串时,jvm首先在字符串常量池中找寻具有相同值的字符串如果找到了,他将返回字符串常量池中的字符串对象引用。否则就在常量池中创建字符串对象并返回引用。如果使用new创建字符串就需要在堆中创建它。

当对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类。

和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。

1、StringBuffer

在使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,所以如果需要对字符串进行修改推荐使用 StringBuffer。

代码随想录算法训练营day08|| 344.反转字符串、541. 反转字符串II、卡码网:54.替换数字、151.翻转字符串里的单词、卡码网:55.右旋转字符串_第4张图片

代码随想录算法训练营day08|| 344.反转字符串、541. 反转字符串II、卡码网:54.替换数字、151.翻转字符串里的单词、卡码网:55.右旋转字符串_第5张图片

代码随想录算法训练营day08|| 344.反转字符串、541. 反转字符串II、卡码网:54.替换数字、151.翻转字符串里的单词、卡码网:55.右旋转字符串_第6张图片

2、StringBuilder

StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。

总结
String 是不可变的,而 StringBuffer 和 StringBuilder 是可变类。
StringBuffer 是线程安全和同步的,而 StringBuilder 不是。这就是 StringBuilder 比 StringBuffer 快的原因。
字符串连接运算符 (+) 在内部使用 StringBuilder 类。
对于非多线程环境中的字符串操作,我们应该使用 StringBuilder 否则使用 StringBuffer 类。

import java.util.Scanner;

class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        String s = in.nextLine();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            if (Character.isDigit(s.charAt(i))) {
                sb.append("number");
            }else sb.append(s.charAt(i));
        }
        System.out.println(sb);
    }
}

 151.翻转字符串里的单词(有难度,过程有点复杂,好好理解)

双指针法(快慢指针法)

通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

定义快慢指针

  • 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
  • 慢指针:指向更新 新数组下标的位置
class Solution {
     /**方法一:
     * 不使用Java内置方法实现
     * 1.去除首尾以及中间多余空格
     * 2.反转整个字符串
     * 3.反转各个单词
     */
    public String reverseWords(String s) {
        //需要自己构造三个方法去实现这三步
        //1、去除首尾以及中间多余空格
        StringBuilder sb = removeSpace(s);
        //2、反转整个字符串 
        reverseString(sb,0,sb.length() - 1);
        //3、反转各个单词
        reverseEachWord(sb);
        return sb.toString(); //返回 字符串表示形式
    }

    public StringBuilder removeSpace(String s){
        int start = 0;
        int end = s.length() - 1;
        while(s.charAt(start) == ' ') start++; //去除首部 空格
        while(s.charAt(end) == ' ') end--;   //去除尾部  空格
        StringBuilder sb = new StringBuilder(); //定义一个可变字符串StringBuilder类
        while(start <= end){  // 遍历字符串内部,注意这里 有=号,一开始没写错了
            char c = s.charAt(start); //charAt(i)是获取字符串中i位置的字符
            if(c != ' ' || sb.charAt(sb.length() - 1) != ' '){
              //c != ' ' ||sb.charAt(sb.length() - 1) != ' ' 
              //这一句可以保证每个单词之间仅单个加空格
                sb.append(c); //c不为空就给添加到sb字符串中去
            }
            start++;
        }
        return sb;
    }

    //反转字符串指定区间[start, end]的字符
    public void reverseString(StringBuilder sb, int start, int end){
        while(start < end){
            char temp = sb.charAt(start);
            //setCharAt(int index,char ch)方法:给定索引处的字符设置为 ch
            sb.setCharAt(start,sb.charAt(end));
            sb.setCharAt(end,temp);
            
           // 注意:这里不可以直接简单三行交换,因为sb.charAt(start)是获取start索引的值,不能把值赋给值操作
          /* char temp = sb.charAt(start);
             sb.charAt(start) = sb.charAt(end);
             sb.charAt(end) = temp;*/
           
            start++;
            end--;
        }
    }

    public void reverseEachWord(StringBuilder sb){
        int slow = 0; //慢指针位于字符串起始点
        int fast = 1; //快指针比慢指针 快一步
        int n = sb.length();
        while(fast < n){ //双指针法(快慢指针,end是快)
            while(fast < n && sb.charAt(fast) != ' '){ //快指针小于n 且不是空 
                fast ++; //快指针继续向右走
            }
            reverseString(sb,slow,fast - 1); //反转(slow,fast-1) 这段字符串
            //快慢指针向后移动,继续反转下一个单词
            slow = fast + 1;
            fast = slow + 1;
        }
    }
}
class Solution {
/*
* 解法四:时间复杂度 O(n)
* 参考卡哥 c++ 代码的三步骤:先移除多余空格,再将整个字符串反转,最后把单词逐个反转
 * 有别于解法一 :没有用 StringBuilder  实现,而是对 String 的 char[] 数组操作来实现以上三个步骤
 */

    //用 char[] 来实现 String 的 removeExtraSpaces,reverse 操作
    public String reverseWords(String s) {
        char[] chars = s.toCharArray();
        //1.去除首尾以及中间多余空格
        chars = removeExtraSpaces(chars);
        //2.整个字符串反转
        reverse(chars, 0, chars.length - 1);
        //3.单词反转
        reverseEachWord(chars);
        return new String(chars); // new String(数组chars)  把数组转为字符串
    }

    //1.用 快慢指针 去除首尾以及中间多余空格,可参考数组元素移除的题解
    public char[] removeExtraSpaces(char[] chars) {
        int slow = 0;
        for (int fast = 0; fast < chars.length; fast++) {
            //先用 fast 移除所有空格
            if (chars[fast] != ' ') {
                //在用 slow 加空格。 除第一个单词外,单词末尾要加空格
                if (slow != 0)
                    chars[slow++] = ' ';
                //fast 遇到空格或遍历到字符串末尾,就证明遍历完一个单词了
                while (fast < chars.length && chars[fast] != ' ')
                    chars[slow++] = chars[fast++];
            }
        }
        //相当于 c++ 里的 resize()
        char[] newChars = new char[slow];
        System.arraycopy(chars, 0, newChars, 0, slow); 
        return newChars;
    }

    //双指针(快慢指针)实现指定范围内字符串反转,可参考字符串反转题解
    public void reverse(char[] chars, int left, int right) {
        if (right >= chars.length) {
            System.out.println("set a wrong right");
            return;
        }
        while (left < right) {
            // chars[left] ^= chars[right];
            // chars[right] ^= chars[left];
            // chars[left] ^= chars[right];
            char temp = chars[left];
            chars[left] = chars[right];
            chars[right] = temp;
            left ++;
            right --;
        }
    }

    //3.单词反转
    public void reverseEachWord(char[] chars) {
        int start = 0;
//end <= s.length()这里的 = ,是为了让end永远指向单词末尾后一个位置
//这样reverse的实参更好设置
        for (int end = 0; end <= chars.length; end++) {
            // end 每次到单词末尾后的空格或串尾,开始反转单词
            if (end == chars.length || chars[end] == ' ') {
                reverse(chars, start, end - 1);
                start = end + 1;
            }
        }
    }
}

55.右旋转字符串

字符串的右旋转操作是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,请编写一个函数,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。

例如,对于输入字符串 "abcdefg" 和整数 2,函数应该将其转换为 "fgabcde"。

数据范围:1 <= k < 10000, 1 <= s.length < 10000;

思路:

为了让本题更有意义,提升一下本题难度:不能申请额外空间,只能在本串上操作(Java不能在字符串上修改,所以使用java一定要开辟新空间)

不能使用额外空间的话,模拟在本串操作要实现右旋转字符串的功能还是有点困难的。

那么我们可以想一下上一题目151.翻转字符串里的单词中讲过,使用整体反转+局部反转就可以实现反转单词顺序的目的。

本题中,我们需要将字符串右移n位,字符串相当于分成了两个部分,如果n为2,符串相当于分成了两个部分,如图: (length为字符串长度)

代码随想录算法训练营day08|| 344.反转字符串、541. 反转字符串II、卡码网:54.替换数字、151.翻转字符串里的单词、卡码网:55.右旋转字符串_第7张图片

 右移n位, 就是将第二段放在前面,第一段放在后面,先不考虑里面字符的顺序,是不是整体倒叙不就行了。如图:

代码随想录算法训练营day08|| 344.反转字符串、541. 反转字符串II、卡码网:54.替换数字、151.翻转字符串里的单词、卡码网:55.右旋转字符串_第8张图片

此时第一段和第二段的顺序是我们想要的,但里面的字符位置被我们倒叙,那么此时我们在把 第一段和第二段里面的字符再倒叙一把,这样字符顺序不就正确了。 如果:

代码随想录算法训练营day08|| 344.反转字符串、541. 反转字符串II、卡码网:54.替换数字、151.翻转字符串里的单词、卡码网:55.右旋转字符串_第9张图片

其实,思路就是 通过 整体倒叙,把两段子串顺序颠倒,两个段子串里的的字符在倒叙一把,负负得正,这样就不影响子串里面字符的顺序了。

// 版本一
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = Integer.parseInt(in.nextLine()); //输入正整数n
        String s = in.nextLine();  //输入一个给定的字符串

        int len = s.length();  //获取字符串长度
        char[] chars = s.toCharArray(); //将字符串转化为字符串数组  
        reverseString(chars, 0, len - 1);  //反转整个字符串
        reverseString(chars, 0, n - 1);  //反转前一段字符串,此时的字符串首尾尾是0,n - 1
        reverseString(chars, n, len - 1);  //反转后一段字符串,此时的字符串首尾尾是n,len - 1
        
        System.out.println(chars);

    }

    public static void reverseString(char[] ch, int start, int end) {
        //异或法反转字符串,参照题目 344.反转字符串的解释
        while (start < end) {
            ch[start] ^= ch[end];
            ch[end] ^= ch[start];
            ch[start] ^= ch[end];
            //三行交换法
           /*char temp = ch[start];
            ch[start] = ch[end];
            ch[end] = temp;*/ 
            start++;
            end--;
        }
    }
}
  1. Scanner in = new Scanner(System.in);: 这一行创建了一个名为in的Scanner对象,它用于从标准输入流(System.in)中读取数据。Scanner类提供了一种方便的方式来解析基本类型和字符串。

  2. int n = Integer.parseInt(in.nextLine());: 这一行首先调用Scanner对象innextLine()方法,它等待用户输入一行文本并读取它然后,Integer.parseInt()方法将这行文本解析为一个整数,并将其赋值给变量n这行代码的目的是读取一个整数,通常是用来表示输入数据的数量或长度。

  3. String s = in.nextLine();: 这一行再次调用Scanner对象innextLine()方法,读取用户输入的下一行文本,并将其赋值给字符串变量s。通常,这行代码用于读取包含输入数据的字符串。

综合起来,这段代码的作用是从标准输入读取两行数据:第一行是一个整数n,通常表示数据的数量或长度;第二行是一个字符串s,通常包含实际的输入数据

            ch[start] ^= ch[end];  // ch[start] =  ch[start] ^ch[end];
            ch[end] ^= ch[start];  //  ch[end] = ch[end] ^ ch[start];
            ch[start] ^= ch[end];  //  ch[start] = ch[start] ^ ch[end];

这段代码是用来交换字符数组(或字符串)中两个位置的字符的常见技巧,通常称为异或交换。(挺常用,最好掌握)

异或运算中  两个相同值计算结果为0 

int a = 6;

int b = 7;
              
a = a ^ b;
b = a ^ b;
a = a ^ b;

解析:

第一步:a = a ^ b;  a = 6 ^ 7

第二步:b = a ^ b; b = 6 ^ 7 ^ 7   

异或运算中  两个相同值计算结果为0  所以 b = 6 ^ 0 = 6

第三步:a = a ^ b; a = 6 ^ 7 ^ 6   (异或运算具有交换律,因此可以交换操作数的顺序。)

与上同理 a = 7 ^ 6 ^ 6 = 7 ^ 0 = 7

故最终a = 7,b = 6 这样就完成了交换。

综合起来,这段代码实现了将字符数组中指定位置的两个字符进行互换的功能,通过利用异或运算的特性,可以不借助额外的变量实现这一操作。(有时候是必要的,节省内存 )

不适用场景:用这种交换方式,两个变量指向的内存必须是不同的。不然两个相同内存的变量异或交换 结果会变成0。

// 版本二
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = Integer.parseInt(in.nextLine());
        String s = in.nextLine();

        int len = s.length();  //获取字符串长度
        char[] chars = s.toCharArray();
        reverseString(chars, 0, len - n - 1);  //反转前一段字符串,此时的字符串首尾是0,len - n - 1
        reverseString(chars, len - n, len - 1);  //反转后一段字符串,此时的字符串首尾是len - n,len - 1
        reverseString(chars, 0, len - 1);  //反转整个字符串

        System.out.println(chars);

    }

    public static void reverseString(char[] ch, int start, int end) {
        //异或法反转字符串,参照题目 344.反转字符串的解释
        while (start < end) {
            ch[start] ^= ch[end];
            ch[end] ^= ch[start];
            ch[start] ^= ch[end];
            start++;
            end--;
        }
    }
}

 拓展:

大家在做剑指offer的时候,会发现 剑指offer的题目是左反转,那么左反转和右反转 有什么区别呢? 其实思路是一样一样的,就是反转的区间不同而已。如果本题是左旋转n,那么实现代码如下:

#include
#include
using namespace std;
int main() {
    int n;
    string s;
    cin >> n; //从标准输入流中读取一个整数,并将其存储在变量 n 中。
    cin >> s; //从标准输入流中读取一个字符串,并将其存储在变量 s 中。
    int len = s.size(); //获取长度
    reverse(s.begin(), s.begin() + n); //  反转第一段长度为n 
    reverse(s.begin() + n, s.end()); // 反转第二段长度为len-n 
    reverse(s.begin(), s.end());  // 反转整个字符串
    cout << s << endl; //将反转后的字符串 s 输出到标准输出流,并换行。

Java版本:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();  // 读取整数 n
        String s = scanner.next();  // 读取字符串 s

        // 将字符串 s 转换为字符数组,方便后续操作
        char[] chars = s.toCharArray();

        // 获取字符串 s 的长度
        int len = s.length();

        // 反转第一段长度为 n 的部分
        reverse(chars, 0, n - 1);

        // 反转第二段长度为 len - n 的部分
        reverse(chars, n, len - 1);

        // 整体反转字符串 s
        reverse(chars, 0, len - 1);

        // 将字符数组转换回字符串并输出
        System.out.println(new String(chars));
    }

    // 反转字符数组 chars 中从索引 start 到索引 end 的部分
    private static void reverse(char[] chars, int start, int end) {
        while (start < end) {
            char temp = chars[start];
            chars[start] = chars[end];
            chars[end] = temp;
            start++;
            end--;
        }
    }
}

你可能感兴趣的:(代码随想录,算法,数据结构)