在字符串中遨游

前言: \textcolor{Green}{前言:} 前言:

一天一个知识点

在字符串中遨游

  • 一、算法
  • 二、算法实例
    • 1. 题目一:反转字符串Ⅱ
    • 2. 题目二:替换空格
    • 3. 题目三:反转字符串中的单词
    • 4. 题目四:左旋转字符串
    • 5. 进阶
  • 总结归纳

一、算法

字符串更多的在做题中使用的方法。

尤其是Stringchar 这两个应该怎么转换,以及字符串自带的方法。

二、算法实例

1. 题目一:反转字符串Ⅱ

题目来源: \textcolor{blue}{题目来源:} 题目来源:反转字符串Ⅱ
等级:简单 \textcolor{OrangeRed}{等级:简单} 等级:简单

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

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

示例 1:

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

示例 2:

输入:s = "abcd", k = 2
输出:"bacd"

提示:

1 <= s.length <= 104
s 仅由小写英文组成
1 <= k <= 104

代码编写

方法1

class Solution {
    public String reverseStr(String s, int k) {
        // string 转换成char数组
        char[] c = s.toCharArray();
        for (int i = 0; i < c.length; i += 2 * k) {
            int left = i;
            int right = Math.min(c.length - 1, left + k - 1);
            while (left < right) {
                c[left] ^= c[right];
                c[right] ^= c[left];
                c[left] ^= c[right];
                right--;
                left++;
            }
        }
        // char数组转换成String,可以直接转
        return new String(c);
    }
}

注意点

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

因为要找的也就是每2 * k 区间的起点。

可以提炼的知识

  1. 对于 char[]String 之间进行转换
String s = "hxl";
char[] chars = s.toCharArray();

string sb = new String(chars);

  1. 关于交换两个变量的方法
    • 一种常见的交换方法:通过临时值进行
     int tmp = a;
     a = b;
     b = a;
    
    • 通过位运算进行交换
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    

2. 题目二:替换空格

题目来源: \textcolor{blue}{题目来源:} 题目来源:替换空格
等级:简单 \textcolor{OrangeRed}{等级:简单} 等级:简单

题目描述

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

示例 1:

输入:s = "We are happy."
输出:"We%20are%20happy."

限制:

0 <= s 的长度 <= 10000

代码编写

方法1:直接进行替换

class Solution {
    public String replaceSpace(String s) {
        if (s == null) {
            return s;
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == ' ') {
                sb.append("%20");
            } else {
                sb.append(s.charAt(i));
            }
        }
        return sb.toString();
    }
}

方法2:双指针
String 是不可变的,我们不能直接对它的长度进行改变,需要转换成StringBuilder

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++) {
            if (s.charAt(i) == ' ') {
                sb.append("  ");
            }
        }
        // 没有空格直接返回
        if (sb.length() == 0) {
            return s;
        }
        // 从后往前进行转换
        int left = len - 1;
        // 一个空格变成三个空间
        s += sb.toString();
        // s变了,长度也就变了,这个扩展的最后一个字符
        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];
            }
            right--;
            left--;
        }
        return new String(ch);
    }
}

可以提炼的知识

  1. 为什么要从后向前填充,从前向后填充不行么?
    从前向后填充的算法复杂度是O(n^2),因为每次添加元素都要将添加元素之后的所有元素向后移动。
    很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
    好处
    • 不用申请新数组。
    • 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。

3. 题目三:反转字符串中的单词

题目来源: \textcolor{blue}{题目来源:} 题目来源:反转字符串中的单词
等级:中等 \textcolor{OrangeRed}{等级:中等} 等级:中等

题目描述

给你一个字符串 s ,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。

示例 1:

输入:s = "the sky is blue"
输出:"blue is sky the"

示例 2:

输入:s = "  hello world  "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。

示例 3:

输入:s = "a good   example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。

提示:

1 <= s.length <= 104
s 包含英文大小写字母、数字和空格 ' '
s 中 至少存在一个 单词

代码编写

方法1

class Solution {
    public String reverseWords(String s) {
        int left = 0, right = s.length() - 1;
        Deque<String> deq = new ArrayDeque<>();
        StringBuilder word = new StringBuilder();
        while (left <= right && s.charAt(right) == ' ') {
            --right;
        }

        while (left <= right) {
            char c = s.charAt(left);
            if (word.length() != 0 && c == ' ') {
                deq.offerFirst(word.toString());
                word.setLength(0);
            } else if (c != ' ') {
                word.append(c);
            }
            ++left;
        }
        deq.offerFirst(word.toString());
        return String.join(" ", deq);
    }
}

注意点

  1. 我这里只去除了尾部的空格,没有去除首部。经测试,首部的空格可以通过双端队列将单词push到队列的头部,这个时候进行判断去除首部空格。

  2. StringBuilder可以直接设置长。

可以提炼的知识

  1. ArrayDeque类offerFirst()方法
    • offerFirst()方法在java.lang包中可用。
    • offerFirst()方法用于将给定元素添加到此双端队列的前面。
    • offerFirst()方法是一种非静态方法,只能通过类对象访问,如果尝试使用类名称访问该方法,则会收到错误消息。
    • offerFirst()方法在双端队列中添加元素时可能会引发异常。
    • NullPointerException:当给定元素为null时,可能引发此异常。
  2. Java String类中的String.Join()方法的使用
    S t r i n g . J o i n 方法 ( A ( S t r i n g ) , B ( S t r i n g [ ] ) ) ; String.Join 方法 (A (String), B (String[])); String.Join方法(A(String),B(String[]));
    • 在指定 String 数组B的每个元素之间串联指定的分隔符 A,从而产生单个串联的字符串
    • 参数列表:
        1、表示连接的符号
        2、表示被连接的数组(也可以是集合),或者是要连接的多个字符串

例如题目中的 S t r i n g . j o i n ( " " , d e q ) String.join(" ", deq) String.join("",deq) ,是将 deq 中的每个元素通过空格连接。

4. 题目四:左旋转字符串

题目来源: \textcolor{blue}{题目来源:} 题目来源:剑指 Offer 58 - II. 左旋转字符串
等级:简单 \textcolor{OrangeRed}{等级:简单} 等级:简单

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

示例 1:

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

示例 2:

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

限制:

1 <= k < s.length <= 10000

代码编写

方法1
先对前面n个反转,再反转后面的,最后整体进行反转。

class Solution {
    public String reverseLeftWords(String s, int n) {
        if (n > s.length()) {
            return s;
        }
        int len = s.length();
        StringBuilder sb = new StringBuilder(s);
        reverseString(sb, 0, n - 1);
        reverseString(sb, n, len - 1);
        return sb.reverse().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--;
        }
    }
}

方法2
使用原始数组进行反转。
用上面的思路:先反转前n个,再反转剩下的,最后反转整体。
新的思路:先整体反转,再反转前面的,最后反转后面的n个

class Solution {
    public String reverseLeftWords(String s, int n) {
        if (n > s.length()) {
            return s;
        }
        char[] ch = s.toCharArray();
        int len = s.length();
        reverse(ch, 0, len - 1);
        reverse(ch, 0, len - n - 1);
        reverse(ch, len - n, len - 1);
        return new String(ch);
    }

    public void reverse(char[] ch, int left, int right) {
        while (left < right) {
            ch[left] ^= ch[right];
            ch[right] ^= ch[left];
            ch[left] ^= ch[right];
            left++;
            right--;
        }
    }
}

可以提炼的知识

  1. sb.reverse().toString();
    • reverse()方法在java.lang包中可用。
    • reverse()方法用于通过反转字符序列来反转该字符序列。
    • reverse()方法是一个非静态方法,只能通过类对象访问,如果尝试使用类名称访问该方法,则会收到错误消息。
    • reverse()方法在反转此字符序列时不会引发异常。

5. 进阶

下面两个涉及到 K M P 算法,后续会讲解 \textcolor{blue}{下面两个涉及到KMP算法,后续会讲解} 下面两个涉及到KMP算法,后续会讲解

28. 找出字符串中第一个匹配项的下标
459. 重复的子字符串

总结归纳

  1. 双指针
    使用双指针法实现反转字符串的操作,双指针法在数组,链表和字符串中很常用
    很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。可以参考题目二。

  2. 反转
    三和四都是反转,那么具体是先整体反转还是先局部反转,可以根据自己思路来。
    用相同的规律进行字符串处理时,可以通过for循环的条件来降低复杂度。

  3. 复习一下StringBuilderStringStringBuffer

    • String不可修改。StringBufferStringBuilder的内容可以修改,继承自AbstractStringBuilder类,实现原理都基于可修改的char数组
    • StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。String是常量
    • 操作少量数据使用string
      单线程操作字符串缓冲区下操作大量数据使用StringBuilder
      多线程操作字符串缓冲区下操作大量数据使用StringBuffer
    • 性能
      每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
  4. 拼接字符串
    字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

你可能感兴趣的:(算法,java,开发语言)