【算法刷题系列】第8天 344. 反转字符串, 541. 反转字符串 II, 54. 替换数字(第八期模拟笔试)

Overview

学习内容

收获总结

  1. 字符串和字节数组的转换:在 Go 语言中,字符串是不可变的。对于需要修改字符串的操作,如反转、替换等,我们通常会先将字符串转换为 []byte 类型。这是因为 []byte 是可变的,可以通过索引操作直接修改其内容。在许多字符串操作中,将字符串转换为字节数组是一个有效的优化策略,尤其是在需要大量的字符替换和处理时,这种转换能够提高性能和代码的可读性。

  2. unicode.IsDigitrune 的用法:在处理字符串中数字字符时,使用 unicode.IsDigit 函数来判断字符是否为数字非常便捷。unicode.IsDigit 适用于所有 Unicode 字符,能够处理多字节字符,如中文或特殊字符。而 rune 是 Go 语言中用于表示 Unicode 码点的类型,它可以存储多字节字符。因此,在处理包含不同字符集的字符串时,理解 runeunicode.IsDigit 的作用,可以让我们编写的代码更具通用性和健壮性。

  3. 从后向前替换字符的技巧:当处理需要对字符进行替换且替换后长度不同的情况时(例如将单个数字字符替换为多个字符),先扩展数组的长度,然后从后向前进行替换操作。这种方式能够有效避免字符替换过程中覆盖未处理的部分,保证替换操作的正确性。在替换操作时,提前计算所需的最终数组长度,并从末尾倒序进行填充,能够让操作更为高效,也避免了不必要的内存分配。

题目解析

题目1:344. 反转字符串

  • 题目描述
    给定一个字符数组 s,请你将该字符数组原地反转。要求算法的空间复杂度为 O(1),也就是说必须在原数组上进行修改,而不借助额外的空间。

  • 示例

    • 输入:["h", "e", "l", "l", "o"]
    • 输出:["o", "l", "l", "e", "h"]
  • 解法说明
    本题的最佳解决方法是使用双指针法。我们从数组的头尾两端分别设立两个指针,一个指向数组的第一个元素(left),另一个指向最后一个元素(right)。然后通过不断交换 leftright 所指向的元素,left 逐渐向右移动,right 逐渐向左移动,直到两个指针相遇或交错。这样便完成了原地反转操作,且没有额外的空间开销。

    这种方法非常简洁,且利用了数组的特性,使得反转操作只需遍历一遍数组即可完成,非常高效。

  • 代码实现:

     1func reverseString(s []byte) {
     2	// 对撞双指针反转法
     3	left := 0
     4	right := len(s) - 1
     5
     6	for left < right {
     7		s[left], s[right] = s[right], s[left]
     8		left++
     9		right--
    10	}
    11}
    
  • 复杂度说明
    时间复杂度为 O(n),其中 n 是数组的长度。每个元素被访问和交换一次,因此遍历的时间复杂度是线性的。
    空间复杂度为 O(1),因为我们是在原数组上进行操作,未使用任何额外的空间。

题目2:541. 反转字符串 II

  • 题目描述
    给定一个字符串 s 和一个整数 k,请将字符串按照以下规则进行反转:

    1. 每隔 2k 个字符,反转前 k 个字符。
    2. 如果剩余字符少于 k 个,则将所有剩余字符反转。
    3. 如果剩余字符多于 k 个但少于 2k 个,则只反转前 k 个字符,其余字符保持不变。
  • 示例

    • 输入:s = "abcdefg", k = 2
    • 输出:"bacdfeg"
  • 解法说明
    该题的核心是按照每 2k 个字符为一组,反转其中的前 k 个字符。为了实现这一点,首先我们需要将字符串转换为可修改的 []byte,然后按照步长为 2k 进行遍历。在每个 2k 的区间内,判断当前剩余的字符数:

    1. 如果该区间的字符数大于或等于 k,则对前 k 个字符进行反转。
    2. 如果剩余字符少于 k 个,则将所有剩余字符反转。 最终处理完所有区间后,将字节数组重新转换为字符串并返回。

    这种解法灵活地处理了各种剩余字符的情况,同时通过局部反转,达到了题目的要求。反转操作依旧使用双指针法,因此时间复杂度不会过高。

  • 代码实现:

     1func reverseStr(s string, k int) string {
     2	ss := []byte(s)
     3
     4	// 同样使用对撞双指针,只不过逻辑上往前移动2k个,前k个反转;还要考虑怎么收尾处理问题
     5	for i := 0; i < len(s); i += 2 * k {
     6		// 1. 如果还可以往前走k个,则反转前k个
     7		if i+k <= len(s)-1 {
     8			reverseFunc(ss[i : i+k])
     9		} else {
    10			// 2. 其他情况反转全部
    11			reverseFunc(ss[i:])
    12		}
    13	}
    14	return string(ss)
    15}
    16
    17func reverseFunc(s []byte) {
    18	start := 0
    19	end := len(s) - 1
    20
    21	for start < end {
    22		s[start], s[end] = s[end], s[start]
    23		start++
    24		end--
    25	}
    26}
    
  • 复杂度说明
    时间复杂度为 O(n),其中 n 是字符串的长度。由于我们每次以 2k 为单位进行遍历并在部分区间内进行反转,整体上所有字符的操作次数是线性的。
    空间复杂度为 O(n),因为我们将字符串转换为了字节数组,这个转换过程需要额外的空间来存储新的字节数组。

题目3:54. 替换数字(第八期模拟笔试)

  • 题目描述
    给定一个包含数字和字母的字符串,将其中的每个数字字符替换为字符串 "rebmun"。例如,字符串 "a1b2c3" 中的数字 1、2、3 分别替换为 "rebmun",结果是 "arebmubrebmuncrebmun"

  • 示例

    • 输入:"a1b2c3"
    • 输出:"arebmubrebmuncrebmun"
  • 解法说明
    这道题目要求我们对字符串中的每一个数字字符进行替换。我们可以先遍历整个字符串,统计其中数字字符的个数。然后,根据数字字符的个数,扩展数组的长度,为每个数字字符替换为 "rebmun" 预留足够的空间。
    接着,我们使用倒序遍历字符串的方式进行替换操作:从字符串的末尾开始,遇到数字字符时,将其替换为 "rebmun"。如果遇到非数字字符,则将其原样放回。这种从后往前替换的方式能够避免覆盖未处理的字符,保证替换过程的正确性。

    该解法的关键在于对数组的扩展和倒序替换。扩展数组是为了避免频繁的内存分配,而倒序替换则能够确保每个字符都能得到正确的处理,特别是在原地替换的场景中。

  • 代码实现:

     1package main
     2
     3import (
     4    "fmt"
     5    "unicode"
     6)
     7
     8func replaceNumber(ss []byte) []byte {
     9    // 1. 统计数字字符的个数
    10    count := 0
    11    for _, i := range ss {
    12        if unicode.IsDigit(rune(i)) {
    13            count++
    14        }
    15    }
    16
    17    // 2. 扩充原切片大小
    18    expandedSize := len(ss) + count * 5 // 每个数字替换成 "rebmun",长度为 5
    19    ss = append(ss, make([]byte, count*5)...) // 扩展原始切片大小
    20
    21    // 3. 从后往前替换
    22    newindex := expandedSize - 1
    23    oldindex := len(ss) - count*5 - 1
    24
    25    for oldindex < newindex {
    26        if unicode.IsDigit(rune(ss[oldindex])) {
    27            // 替换数字为 "rebuman"
    28            ss[newindex-5] = 'n'
    29            ss[newindex-4] = 'u'
    30            ss[newindex-3] = 'm'
    31            ss[newindex-2] = 'b'
    32            ss[newindex-1] = 'e'
    33            ss[newindex] = 'r'
    34            newindex -= 6
    35        } else {
    36            ss[newindex] = ss[oldindex]
    37            newindex--
    38        }
    39        oldindex--
    40    }
    41
    42    return ss
    43}
    44
    45func main() {
    46    var str string
    47    fmt.Scanln(&str)
    48
    49    strByte := []byte(str)
    50    newString := replaceNumber(strByte)
    51
    52    fmt.Println(string(newString))
    53}
    
  • 复杂度说明
    时间复杂度为 O(n),其中 n 是字符串的长度。我们需要遍历整个字符串两次:第一次用于统计数字字符的个数,第二次用于从后向前进行替换操作。因此时间复杂度是线性的。
    空间复杂度为 O(n),因为我们需要扩展数组的大小来存储替换后的字符串,并且扩展后的数组长度与原字符串成正比。