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

应用示例

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


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

相关文章
|
19天前
|
算法
力扣每日一题 6/23 字符串/模拟
力扣每日一题 6/23 字符串/模拟
11 1
|
7天前
|
存储 算法
经典的滑动窗口的题目 力扣 2799. 统计完全子数组的数目(面试题)
经典的滑动窗口的题目 力扣 2799. 统计完全子数组的数目(面试题)
|
19天前
|
算法 索引
力扣每日一题 6/28 动态规划/数组
力扣每日一题 6/28 动态规划/数组
18 0
|
19天前
|
索引
力扣每日一题 6/27 字符串 贪心
力扣每日一题 6/27 字符串 贪心
9 0
|
19天前
|
Python
力扣随机一题 模拟+字符串
力扣随机一题 模拟+字符串
9 0
|
19天前
力扣每日一题 6/22 字符串/贪心
力扣每日一题 6/22 字符串/贪心
8 0
|
1月前
|
算法 C++
【数据结构与算法】:关于时间复杂度与空间复杂度的计算(C/C++篇)——含Leetcode刷题-2
【数据结构与算法】:关于时间复杂度与空间复杂度的计算(C/C++篇)——含Leetcode刷题
|
1月前
|
算法 C++
【数据结构与算法】:关于时间复杂度与空间复杂度的计算(C/C++篇)——含Leetcode刷题-1
【数据结构与算法】:关于时间复杂度与空间复杂度的计算(C/C++篇)——含Leetcode刷题
|
1月前
|
索引
【LeetCode刷题】二分查找:山脉数组的峰顶索引、寻找峰值
【LeetCode刷题】二分查找:山脉数组的峰顶索引、寻找峰值
|
1月前
|
算法
【LeetCode刷题】滑动窗口解决问题:串联所有单词的子串(困难)、最小覆盖子串(困难)
【LeetCode刷题】滑动窗口解决问题:串联所有单词的子串(困难)、最小覆盖子串(困难)