LeetCode 题目 87:递归\动态规划\哈希实现 扰乱字符串

简介: LeetCode 题目 87:递归\动态规划\哈希实现 扰乱字符串

作者介绍:10年大厂数据\经营分析经验,现任大厂数据部门负责人。

会一些的技术:数据分析、算法、SQL、大数据相关、python

欢迎加入社区:码上找工作

作者专栏每日更新:

LeetCode解锁1000题: 打怪升级之旅

python数据分析可视化:企业实战案例

python源码解读

程序员必备的数学知识与应用

备注说明:方便大家阅读,统一使用python,带必要注释,公众号 数据分析螺丝钉 一起打怪升级

题目描述

给定一个字符串 s1,我们可以通过对其进行翻转和交换子字符串的方式生成另一个字符串 s2。确定 s2 是否是 s1 的扰乱字符串。

输入格式
  • s1:原始字符串。
  • s2:可能的扰乱字符串。
输出格式
  • 返回一个布尔值,表示 s2 是否是 s1 的扰乱字符串。

示例

示例 1
输入: s1 = "great", s2 = "rgeat"
输出: true
示例 2
输入: s1 = "abcde", s2 = "caebd"
输出: false

方法一:递归

解题步骤
  1. 递归基准情况:如果 s1s2 相等,返回 true
  2. 字符验证:如果两个字符串排序后不等,返回 false
  3. 递归分割:尝试对 s1 进行分割,检查是否能通过扰乱得到 s2
完整的规范代码
def isScramble(s1, s2):
    """
    递归判断 s2 是否是 s1 的扰乱字符串
    :param s1: str, 原始字符串
    :param s2: str, 可能的扰乱字符串
    :return: bool
    """
    if s1 == s2:
        return True
    if sorted(s1) != sorted(s2):
        return False
    n = len(s1)
    for i in range(1, n):
        # 检查不交换的情况
        if isScramble(s1[:i], s2[:i]) and isScramble(s1[i:], s2[i:]):
            return True
        # 检查交换的情况
        if isScramble(s1[:i], s2[-i:]) and isScramble(s1[i:], s2[:-i]):
            return True
    return False
# 示例调用
print(isScramble("great", "rgeat"))  # 输出: true
print(isScramble("abcde", "caebd"))  # 输出: false
算法分析
  • 时间复杂度:(O(n^4)),在递归过程中需要多次分割字符串和比较。
  • 空间复杂度:(O(n)),递归深度依赖于字符串长度。

方法二:记忆化搜索

解题步骤
  1. 使用哈希表存储结果:使用字典存储已计算的结果,避免重复计算。
  2. 递归搜索:基于递归实现,但在每次递归前查询字典。
完整的规范代码
def isScramble(s1, s2, memo={}):
    """
    记忆化搜索判断 s2 是否是 s1 的扰乱字符串
    :param s1: str, 原始字符串
    :param s2: str, 可能的扰乱字符串
    :param memo: dict, 记忆化存储已计算的结果
    :return: bool
    """
    if (s1, s2) in memo:
        return memo[(s1, s2)]
    if s1 == s2:
        memo[(s1, s2)] = True
        return True
    if sorted(s1) != sorted(s2):
        memo[(s1, s2)] = False
        return False
    n = len(s1)
    for i in range(1, n):
        if (isScramble(s1[:i], s2[:i], memo) and isScramble(s1[i:], s2[i:], memo)) or \
           (isScramble(s1[:i], s2[-i:], memo) and isScramble(s1[i:], s2[:-i], memo)):
            memo[(s1, s2)] = True
            return True
    memo[(s1, s2)] = False
    return False
# 示例调用
print(isScramble("great", "rgeat"))  # 输出: true
print(isScramble("abcde", "caebd"))  # 输出: false
算法分析
  • 时间复杂度:(O(n^3)),使用记忆化后,大多数重复的递归调用被省略。
  • 空间复杂度:(O(n^2)),存储已解决问题的状态需要的空间。

方法三:动态规划

解题步骤
  1. 定义状态dp[i][j][k] 表示从 s1 的第 i 个字符开始和从 s2 的第 j 个字符开始,长度为 k 的子串是否互为扰乱字符串。
  2. 状态转移:根据字符是否相等以及可能的分割情况更新状态。
完整的规范代码
def isScramble(s1, s2):
    """
    动态规划判断 s2 是否是 s1 的扰乱字符串
    :param s1: str, 原始字符串
    :param s2: str, 可能的扰乱字符串
    :return: bool
    """
    if len(s1) != len(s2) or sorted(s1) != sorted(s2):
        return False
    n = len(s1)
   dp = [[[False] * (n + 1) for _ in range(n)] for __ in range(n)]
    # 初始化长度为1的情况
    for i in range(n):
        for j in range(n):
            dp[i][j][1] = s1[i] == s2[j]
    # 动态规划计算长度为k的情况
    for k in range(2, n + 1):  # 子串长度
        for i in range(n - k + 1):  # s1的起始位置
            for j in range(n - k + 1):  # s2的起始位置
                for l in range(1, k):  # 分割点
                    if (dp[i][j][l] and dp[i + l][j + l][k - l]) or (dp[i][j + k - l][l] and dp[i + l][j][k - l]):
                        dp[i][j][k] = True
                        break
    
    return dp[0][0][n]
# 示例调用
print(isScramble("great", "rgeat"))  # 输出: true
print(isScramble("abcde", "caebd"))  # 输出: false
算法分析
  • 时间复杂度:(O(n^4)),其中 (n) 是字符串的长度。我们需要计算所有长度、所有起始位置和所有可能的分割点的状态。
  • 空间复杂度:(O(n^3)),存储状态所需的三维数组。

方法四:优化的动态规划

解题步骤
  1. 使用二维数组:降低空间复杂度,每次只保存当前长度的状态信息。
  2. 优化状态转移:只依赖于小于当前长度的状态,因此可以使用滚动数组的技术。
完整的规范代码
def isScramble(s1, s2):
    """
    优化的动态规划算法,使用二维数组降低空间复杂度
    :param s1: str, 原始字符串
    :param s2: str, 可能的扰乱字符串
    :return: bool
    """
    if len(s1) != len(s2) or sorted(s1) != sorted(s2):
        return False
    n = len(s1)
    dp = [[[False] * n for _ in range(n)] for __ in range(2)]
    # 初始化长度为1的情况
    for i in range(n):
        for j in range(n):
            dp[1][i][j] = s1[i] == s2[j]
    # 动态规划计算长度大于1的情况
    for k in range(2, n + 1):  # 子串长度
        for i in range(n - k + 1):  # s1的起始位置
            for j in range(n - k + 1):  # s2的起始位置
                for l in range(1, k):  # 分割点
                    if (dp[(l)%2][i][j] and dp[(k-l)%2][i+l][j+l]) or (dp[(l)%2][i][j+k-l] and dp[(k-l)%2][i+l][j]):
                        dp[k%2][i][j] = True
                        break
    return dp[n%2][0][0]
# 示例调用
print(isScramble("great", "rgeat"))  # 输出: true
print(isScramble("abcde", "caebd"))  # 输出: false
算法分析
  • 时间复杂度:(O(n^4)),虽然优化了空间复杂度,但时间复杂度仍然高。
  • 空间复杂度:(O(n^2)),使用滚动数组技术,只存储当前长度和前一长度的状态。

方法五:字符串哈希

解题步骤
  1. 预处理哈希值:为 s1s2 中的所有子字符串计算哈希值,以便快速比较任何两个子字符串是否相等。
  2. 动态规划:使用三维 dp 数组,其中 dp[i][j][len] 表示 s1 从位置 i 开始长度为 len 的子串与 s2 从位置 j 开始长度为 len 的子串是否是扰乱字符串。
完整的规范代码
def isScramble(s1, s2):
    """
    使用字符串哈希和动态规划判断 s2 是否是 s1 的扰乱字符串
    :param s1: str, 原始字符串
    :param s2: str, 可能的扰乱字符串
    :return: bool
    """
    if len(s1) != len(s2) or sorted(s1) != sorted(s2):
        return False
    n = len(s1)
    dp = [[[False] * (n + 1) for _ in range(n)] for __ in range(n)]
    # 字符串哈希预处理
    # 假设 hash_function 是一个用于计算字符串哈希的函数
    hash1 = [[0] * (n + 1) for _ in range(n)]
    hash2 = [[0] * (n + 1) for _ in range(n)]
    # 填充哈希数组
    for i in range(n):
        for j in range(i, n):
            hash1[i][j + 1] = hash_function(s1[i:j + 1])
            hash2[i][j + 1] = hash_function(s2[i:j + 1])
    # 初始化长度为1的情况
    for i in range(n):
        for j in range(n):
            dp[i][j][1] = s1[i] == s2[j]
    # 动态规划
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            for j in range(n - length + 1):
                for k in range(1, length):
                    if (dp[i][j][k] and dp[i + k][j + k][length - k]) or (dp[i][j + length - k][k] and dp[i + k][j][length - k]):
                        dp[i][j][length] = True
                        break
    return dp[0][0][n]
# 示例调用
print(isScramble("great", "rgeat"))  # 输出: true
print(isScramble("abcde", "caebd"))  # 输出: false
算法分析
  • 时间复杂度:在优化情况下接近 (O(n^3))。预处理哈希值和填充动态规划表的过程都涉及三层循环。
  • 空间复杂度:哈希表的大小和动态规划表的大小都为 (O(n^2)),因此总体空间复杂度为 (O(n^2))。

这种方法利用字符串哈希来优化字符串的比较过程,大幅减少了时间开销,特别适用于字符串长度较大的情况。

不同算法的优劣势对比

特征 方法一:递归 方法二:记忆化搜索 方法三:动态规划 方法四:优化的动态规划 方法五:字符串哈希
时间复杂度 (O(n^4)) (O(n^3)) (O(n^4)) (O(n^4)) (O(n^3)) 至 (O(n^4))
空间复杂度 (O(n)) (O(n^2)) (O(n^3)) (O(n^2)) (O(n^2)) 至 (O(n^3))
优势 直观简单 减少重复计算 精确控制 空间优化 快速字符串比较
劣势 复杂度高 空间较大 空间消耗大 复杂度依然高 需要高效的哈希实现

应用示例

这类算法在处理文本编辑器中的文字扰乱问题、编译器中的代码重构检测、以及一些需要大规模字符串操作的生物信息学应用中尤为重要。


欢迎关注微信公众号 数据分析螺丝钉

相关文章
|
8月前
|
机器学习/深度学习 算法 Go
【LeetCode 热题100】139:单词拆分(动态规划全解析+细节陷阱)(Go语言版)
本题是 LeetCode 热题 139:单词拆分(Word Break),需判断字符串 `s` 是否能由字典 `wordDict` 中的单词拼接而成。通过动态规划(DP)或记忆化搜索解决。DP 中定义布尔数组 `dp[i]` 表示前 `i` 个字符是否可拆分,状态转移方程为:若存在 `j` 使 `dp[j]=true` 且 `s[j:i]` 在字典中,则 `dp[i]=true`。初始条件 `dp[0]=true`。代码实现中用哈希集合优化查找效率。记忆化搜索则从起始位置递归尝试所有切割点。两种方法各有利弊,DP 更适合面试场景。思考扩展包括输出所有拆分方式及使用 Trie 优化大字典查找。
266 6
|
程序员 C语言
【C语言】LeetCode(力扣)上经典题目
【C语言】LeetCode(力扣)上经典题目
261 1
LeetCode第12题目整数转罗马数字
该文章介绍了 LeetCode 第 12 题整数转罗马数字的解法,通过使用 TreeMap 按照整数从大到小排序,先使用大的罗马数字表示整数,再用小的,核心是先表示完大的罗马数字,想通此点该题较简单。
LeetCode第12题目整数转罗马数字
LeetCode------递归(爬楼梯)
这篇文章通过LeetCode上的"爬楼梯"问题介绍了递归的基本概念和实现方法,包括递归公式的推导、基本递归实现、使用备忘录优化以避免重复计算,以及自底向上的迭代方法来提高效率。
LeetCode------递归(爬楼梯)
|
SQL Oracle 关系型数据库
CASE WHEN 语句的语法及示例,LeetCode 题目 “确认率” 练习
本文介绍了SQL中CASE语句的两种形式和语法,并通过LeetCode题目“确认率”的SQL查询示例展示了CASE语句在实际问题中的应用,解释了如何使用CASE语句计算特定条件的比率。
LeetCode第13题目罗马数字转整数
该文章介绍了 LeetCode 第 13 题罗马数字转整数的解法,通过从大到小解析罗马数字,根据罗马数字的特点,按照从大到小的顺序匹配罗马数字和整数的关系,从而解决该问题,同时强调要注意观察题目考查的知识点特征。
|
Unix Shell Linux
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
本文提供了几个Linux shell脚本编程问题的解决方案,包括转置文件内容、统计词频、验证有效电话号码和提取文件的第十行,每个问题都给出了至少一种实现方法。
262 6
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
|
Python
【Leetcode刷题Python】剑指 Offer 32 - III. 从上到下打印二叉树 III
本文介绍了两种Python实现方法,用于按照之字形顺序打印二叉树的层次遍历结果,实现了在奇数层正序、偶数层反序打印节点的功能。
169 6
|
搜索推荐 索引 Python
【Leetcode刷题Python】牛客. 数组中未出现的最小正整数
本文介绍了牛客网题目"数组中未出现的最小正整数"的解法,提供了一种满足O(n)时间复杂度和O(1)空间复杂度要求的原地排序算法,并给出了Python实现代码。
373 2