Bootstrap

每日算法一练:剑指offer——字符串篇(1)

1.路径加密

        假定一段路径记作字符串 path,其中以 "." 作为分隔符。现需将路径加密,加密方法为将 path 中的分隔符替换为空格 " ",请返回加密后的字符串。

示例 1:

输入:path = "a.aef.qerf.bb"

输出:"a aef qerf bb"

限制:

0 <= path.length <= 10000

1.1遍历添加

解题思路

题目要求将路径字符串 path 中的每个 . 替换为空格 。因为字符串是不可变的,我们需要新建一个字符串或容器,在遍历 path 的每个字符时逐一进行判断和添加。

算法流程

具体的算法流程如下:

  1. 初始化一个 StringBuilder (Java)或 list(Python),用于存放加密后的字符,命名为 res
  2. 遍历字符串 path 的每一个字符 c
    • 如果 c.,则将空格 添加到 res
    • 如果 c 不是 .,则直接将 c 添加到 res
  3. 遍历结束后,将 res 转化为字符串并返回。

这种逐字符处理的方式适用于不修改原字符串的情况,有助于我们根据条件自由地构建新的字符串。

复杂度分析

  • 时间复杂度:O(N),其中 N 为 path 的长度。遍历 path 中的每个字符的时间复杂度是 O(N),每个字符的处理(判断与添加)为 O(1)。
  • 空间复杂度:O(N)。创建了一个 StringBuilderlist 来存储新字符串,相当于与 path 等长的额外空间开销。

代码示例

class Solution {
    // 定义一个方法,用于加密路径字符串 path,将所有的 "." 替换为空格 " "
    public String pathEncryption(String path) {
        // 创建一个 StringBuilder 对象,用于构建新的字符串
        StringBuilder res = new StringBuilder();
        
        // 将字符串 path 转换为字符数组,逐个字符进行遍历
        for (Character c : path.toCharArray()) {
            // 如果当前字符是 '.',将空格 ' ' 添加到 StringBuilder 中
            if (c == '.') {
                res.append(' ');
            } else {
                // 如果当前字符不是 '.',直接将该字符添加到 StringBuilder 中
                res.append(c);
            }
        }
        
        // 将 StringBuilder 转换为字符串并返回结果
        return res.toString();
    }
}

2.有效数字

有效数字(按顺序)可以分成以下几个部分:

  1. 若干空格
  2. 一个 小数 或者 整数
  3. (可选)一个 'e' 或 'E' ,后面跟着一个 整数
  4. 若干空格

小数(按顺序)可以分成以下几个部分:

  1. (可选)一个符号字符('+' 或 '-'
  2. 下述格式之一:
    1. 至少一位数字,后面跟着一个点 '.'
    2. 至少一位数字,后面跟着一个点 '.' ,后面再跟着至少一位数字
    3. 一个点 '.' ,后面跟着至少一位数字

整数(按顺序)可以分成以下几个部分:

  1. (可选)一个符号字符('+' 或 '-'
  2. 至少一位数字

部分有效数字列举如下:["2", "0089", "-0.1", "+3.14", "4.", "-.9", "2e10", "-90E3", "3e+7", "+6e-1", "53.5e93", "-123.456e789"]

部分无效数字列举如下:["abc", "1a", "1e", "e3", "99e2.5", "--6", "-+3", "95a54e53"]

给你一个字符串 s ,如果 s 是一个 有效数字 ,请返回 true 。

示例 1:

输入:s = "0"
输出:true

示例 2:

输入:s = "e"
输出:false

示例 3:

输入:s = "."
输出:false

提示:

  • 1 <= s.length <= 20
  • s 仅含英文字母(大写和小写),数字(0-9),加号 '+' ,减号 '-' ,空格 ' ' 或者点 '.' 。

解题思路

这道题的目标是验证一个字符串是否为有效数字。有效数字的定义比较复杂,因此我们使用**有限状态自动机(Finite State Machine, FSM)**来设计算法,将不同字符的输入分成多个状态来判断有效性。

算法流程

1. 字符类型划分

根据题目中的描述,将字符分为以下几类:

  • 空格' ',允许出现在开头或结尾。
  • 数字'0''9'
  • 符号'+''-',可以出现在数字前面。
  • 幂符号'e''E',表示科学计数法的指数部分。
  • 小数点.,表示小数的整数部分与小数部分之间的分隔。
  • 非法字符:任何不属于以上类别的字符会导致判定为无效。
2. 状态定义

我们定义九种状态:

  • 状态 0:起始的空格。
  • 状态 1:数字或小数点前的符号。
  • 状态 2:小数点前的数字。
  • 状态 3:小数部分的数字。
  • 状态 4:只有小数点后的数字。
  • 状态 5:幂符号。
  • 状态 6:幂符号后的符号。
  • 状态 7:幂符号后的数字。
  • 状态 8:结尾的空格。
3. 状态转移表

基于以上状态,构建状态转移表 states。该表用于记录每个状态下遇到特定类型字符后的转移情况。以 HashMap 来存储,每个 key-value 对表示一个字符类型(key)和目标状态(value)。

4. 算法步骤
  1. 初始化 states 数组,定义从每个状态到下一个状态的可能转移。
  2. 遍历字符串 s 的每个字符,根据字符类型确定转移类型 t
  3. 检查当前状态 p 下是否存在该字符类型的转移;若不存在,则返回 false
  4. 若存在转移,则根据 states 中的映射进行状态更新。
  5. 遍历结束后,如果状态 p 是有效的结束状态(2, 3, 7, 8),则返回 true,否则返回 false

复杂度分析

  • 时间复杂度:O(N),其中 N 为字符串 s 的长度。遍历字符串,每个字符的状态转移操作为 O(1)。
  • 空间复杂度:O(1),states 和状态变量 p 使用常数大小的空间。

代码示例

import java.util.HashMap;
import java.util.Map;

class Solution {
    public boolean validNumber(String s) {
        // 状态转移表,states[i] 表示在状态 i 时,根据输入字符的类型选择下一状态
        Map<Character, Integer>[] states = new Map[]{
            // 状态 0:起始的空格,可以转移到状态 0(继续空格)、状态 1(符号)、状态 2(数字)、状态 4(小数点)
            new HashMap<>() {{ put(' ', 0); put('s', 1); put('d', 2); put('.', 4); }},
            // 状态 1:符号后,可以转移到状态 2(数字)、状态 4(小数点)
            new HashMap<>() {{ put('d', 2); put('.', 4); }},
            // 状态 2:小数点前的数字,可以转移到状态 2(继续数字)、状态 3(小数部分的数字)、状态 5(幂符号)、状态 8(结尾空格)
            new HashMap<>() {{ put('d', 2); put('.', 3); put('e', 5); put(' ', 8); }},
            // 状态 3:小数部分的数字,可以转移到状态 3(继续小数数字)、状态 5(幂符号)、状态 8(结尾空格)
            new HashMap<>() {{ put('d', 3); put('e', 5); put(' ', 8); }},
            // 状态 4:只有小数点后的数字,可以转移到状态 3(小数数字)
            new HashMap<>() {{ put('d', 3); }},
            // 状态 5:幂符号,可以转移到状态 6(幂后的符号)、状态 7(幂后的数字)
            new HashMap<>() {{ put('s', 6); put('d', 7); }},
            // 状态 6:幂符号后的符号,可以转移到状态 7(幂后的数字)
            new HashMap<>() {{ put('d', 7); }},
            // 状态 7:幂符号后的数字,可以转移到状态 7(继续幂数字)、状态 8(结尾空格)
            new HashMap<>() {{ put('d', 7); put(' ', 8); }},
            // 状态 8:结尾的空格,可以继续保持在状态 8
            new HashMap<>() {{ put(' ', 8); }}
        };

        int p = 0; // 初始状态
        char t;    // 当前字符类型

        // 遍历字符串中的每个字符
        for (char c : s.toCharArray()) {
            // 判断字符类型:数字('d')、符号('s')、幂符号('e')、小数点或空格
            if (c >= '0' && c <= '9') t = 'd';
            else if (c == '+' || c == '-') t = 's';
            else if (c == 'e' || c == 'E') t = 'e';
            else if (c == '.' || c == ' ') t = c;
            else t = '?'; // 非法字符

            // 如果当前状态下没有该字符类型的转移,则直接返回 false
            if (!states[p].containsKey(t)) return false;

            // 状态转移
            p = states[p].get(t);
        }

        // 检查是否为合法的结束状态,只有状态 2(整数部分)、状态 3(小数部分)、状态 7(幂数字)、状态 8(结尾空格)为合法
        return p == 2 || p == 3 || p == 7 || p == 8;
    }
}

3.套餐内的商品的排列顺序

        某店铺将用于组成套餐的商品记作字符串 goods,其中 goods[i] 表示对应商品。请返回该套餐内所含商品的 全部排列方式 。

返回结果 无顺序要求,但不能含有重复的元素。

示例 1:

输入:goods = "agew"
输出:["aegw","aewg","agew","agwe","aweg","awge","eagw","eawg","egaw","egwa","ewag","ewga","gaew","gawe","geaw","gewa","gwae","gwea","waeg","wage","weag","wega","wgae","wgea"]

提示:

  • 1 <= goods.length <= 8

解题思路

对于一个包含不同商品的字符串 goods,可以生成所有可能的排列组合。这道题的主要思路是使用**深度优先搜索(DFS)**来遍历字符串的所有排列组合,并通过剪枝来避免重复的组合。

对于给定长度为 n 的字符串(假设字符互不重复),共有 n! 种排列方式。具体地,我们通过递归固定字符串中的每一位字符,并在后续步骤中固定剩余字符的位置。这样可以得到所有可能的排列组合。

算法流程

  1. 初始化:将 goods 转换为字符数组 arr 便于交换字符,创建 res 列表用于存储所有无重复排列组合。

  2. 递归与回溯

    • 递归函数 dfs(x) 用于确定字符串 arr 的第 x 位字符。
    • x 达到 arr.length - 1 时,意味着所有位都已经固定,因此将 arr 转换为字符串并加入结果集中 res
    • 初始化一个 HashSet 集合 set,用于记录已经在当前位置固定过的字符。如果字符已经在 set 中,则跳过此字符,避免生成重复排列。
    • 遍历 i ∈ [x, arr.length) 范围内的字符,将字符 arr[i]arr[x] 交换位置以固定 arr[i] 在第 x 位,随后递归调用 dfs(x + 1) 固定下一个位置。
    • 完成递归后,将 arr[i]arr[x] 位置还原,以恢复之前的状态(回溯)。
  3. 剪枝:在每一层递归中使用 HashSet 记录已固定的字符。如果字符已经出现,则跳过(剪枝),这样可以避免生成重复排列。

复杂度分析

  • 时间复杂度O(N! * N),其中 N 为字符串 goods 的长度。总共有 N! 种排列方案,每种排列的拼接操作 join() 复杂度为 O(N)
  • 空间复杂度O(N^2)。递归深度为 N,占用栈空间为 O(N)。在每一层递归中,HashSet 用于存储当前字符的不同排列组合,最多占用 N + (N - 1) + ... + 2 + 1 = (N + 1) * N / 2 个字符,即 O(N^2)

代码实现

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

class Solution {
    List<String> res = new LinkedList<>(); // 存储所有的排列结果
    char[] arr; // 字符数组,用于存储输入的商品字符串

    // 主函数,将输入字符串转换为字符数组并开始 DFS
    public String[] goodsOrder(String goods) {
        arr = goods.toCharArray(); // 将输入字符串转换为字符数组
        dfs(0); // 从第一个字符开始进行深度优先搜索
        return res.toArray(new String[res.size()]); // 返回所有排列结果的数组
    }

    // 深度优先搜索,固定第 x 位字符
    void dfs(int x) {
        // 当 x 达到 arr 的最后一位时,说明所有字符都已固定
        if (x == arr.length - 1) {
            res.add(String.valueOf(arr)); // 将当前排列转换为字符串并添加到结果中
            return; // 返回上层递归
        }
        
        HashSet<Character> set = new HashSet<>(); // 创建一个集合用于记录已固定的字符
        // 遍历当前位置 x 到 arr.length 的所有字符
        for (int i = x; i < arr.length; i++) {
            // 如果集合中已存在当前字符,则跳过此循环(剪枝)
            if (set.contains(arr[i])) continue;
            set.add(arr[i]); // 将当前字符加入集合,标记为已使用
            
            swap(i, x); // 交换字符,将 arr[i] 固定在第 x 位
            dfs(x + 1); // 递归调用,固定下一个字符的位置
            swap(i, x); // 恢复交换(回溯),以便进行下一个排列
        }
    }

    // 辅助函数,用于交换字符数组中的两个字符
    void swap(int a, int b) {
        char tmp = arr[a]; // 暂存 a 位置的字符
        arr[a] = arr[b];   // 将 b 位置的字符放到 a 位置
        arr[b] = tmp;      // 将暂存的字符放到 b 位置
    }
}

;