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))
优势 直观简单 减少重复计算 精确控制 空间优化 快速字符串比较
劣势 复杂度高 空间较大 空间消耗大 复杂度依然高 需要高效的哈希实现

应用示例

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


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

相关文章
|
5月前
|
JavaScript
力扣3333.找到初始输入字符串Ⅱ
【10月更文挑战第9天】力扣3333.找到初始输入字符串Ⅱ
54 1
|
5月前
|
C++
Leetcode第43题(字符串相乘)
本篇介绍了一种用C++实现的字符串表示的非负整数相乘的方法,通过逆向编号字符串,将乘法运算转化为二维数组的累加过程,最后处理进位并转换为字符串结果,解决了两个大数相乘的问题。
44 9
|
5月前
|
程序员 C语言
【C语言】LeetCode(力扣)上经典题目
【C语言】LeetCode(力扣)上经典题目
|
5月前
|
算法 C++
Leetcode第八题(字符串转换整数(atoi))
这篇文章介绍了LeetCode上第8题“字符串转换整数(atoi)”的解题思路和C++的实现方法,包括处理前导空格、正负号、连续数字字符以及整数溢出的情况。
43 0
|
5月前
【LeetCode 22】459.重复的子字符串
【LeetCode 22】459.重复的子字符串
51 0
|
5月前
【LeetCode 20】151.反转字符串里的单词
【LeetCode 20】151.反转字符串里的单词
38 0
|
6月前
|
Unix Shell Linux
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
本文提供了几个Linux shell脚本编程问题的解决方案,包括转置文件内容、统计词频、验证有效电话号码和提取文件的第十行,每个问题都给出了至少一种实现方法。
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
|
7月前
|
Python
【Leetcode刷题Python】剑指 Offer 32 - III. 从上到下打印二叉树 III
本文介绍了两种Python实现方法,用于按照之字形顺序打印二叉树的层次遍历结果,实现了在奇数层正序、偶数层反序打印节点的功能。
80 6
|
7月前
|
搜索推荐 索引 Python
【Leetcode刷题Python】牛客. 数组中未出现的最小正整数
本文介绍了牛客网题目"数组中未出现的最小正整数"的解法,提供了一种满足O(n)时间复杂度和O(1)空间复杂度要求的原地排序算法,并给出了Python实现代码。
162 2
|
4月前
|
机器学习/深度学习 人工智能 自然语言处理
280页PDF,全方位评估OpenAI o1,Leetcode刷题准确率竟这么高
【10月更文挑战第24天】近年来,OpenAI的o1模型在大型语言模型(LLMs)中脱颖而出,展现出卓越的推理能力和知识整合能力。基于Transformer架构,o1模型采用了链式思维和强化学习等先进技术,显著提升了其在编程竞赛、医学影像报告生成、数学问题解决、自然语言推理和芯片设计等领域的表现。本文将全面评估o1模型的性能及其对AI研究和应用的潜在影响。
113 1