作者: admin2025

  • Validate IP Address,LeetCode,468

    LeetCode 上的第 468 题 “Validate IP Address” 要求编写一个函数来验证给定的字符串是否是有效的 IPv4 或 IPv6 地址。IPv4 地址由四个十进制数字组成,每个数字介于 0 到 255 之间,用点(.)分隔。IPv6 地址由八个十六进制数字组成,用冒号(:)分隔。

    题目描述

    编写一个函数来验证输入的字符串是否是有效的 IPv4 或 IPv6 地址。

    示例

    示例 1:

    输入: "172.16.254.1"

    输出: "IPv4"

    解释: 这是一个有效的 IPv4 地址, 所以返回 "IPv4"。

    示例 2:

    输入: "2001:0db8:85a3:0:0:8A2E:0370:7334"

    输出: "IPv6"

    解释: 这是一个有效的 IPv6 地址, 所以返回 "IPv6"。

    示例 3:

    输入: "256.256.256.256"

    输出: "Neither"

    解释: 这不是一个有效的 IPv4 或 IPv6 地址, 所以返回 "Neither"。

    解题思路

    1. 判断基础格式
      • 如果字符串包含点(.),则可能是 IPv4 地址。
      • 如果字符串包含冒号(:),则可能是 IPv6 地址。
    2. 验证 IPv4 地址
      • 按点(.)分割字符串,应该得到四个部分。
      • 每个部分必须是介于 0 到 255 之间的十进制数。
      • 每个部分不能有前导零。
    3. 验证 IPv6 地址
      • 按冒号(:)分割字符串,应该得到八个部分。
      • 每个部分必须是 1 到 4 位的十六进制数。

    代码实现

    以下是 Python 实现的示例代码:

    class Solution: def validIPAddress(self, IP: str) -> str: def isIPv4(s): parts = s.split('.') if len(parts) != 4: return False for part in parts: if not part.isdigit() or not 0 <= int(part) <= 255: return False if part != str(int(part)): # 检查是否有前导零 return False return True

        def isIPv6(s):
            parts = s.split(':')
            if len(parts) != 8:
                return False
            for part in parts:
                if len(part) == 0 or len(part) > 4:
                    return False
                for char in part:
                    if not (char.isdigit() or 'a' <= char.lower() <= 'f'):
                        return False
            return True
    
        if isIPv4(IP):
            return "IPv4"
        elif isIPv6(IP):
            return "IPv6"
        else:
            return "Neither"

    示例使用

    sol = Solution() print(sol.validIPAddress("172.16.254.1")) # 输出: "IPv4" print(sol.validIPAddress("2001:0db8:85a3:0:0:8A2E:0370:7334")) # 输出: "IPv6" print(sol.validIPAddress("256.256.256.256")) # 输出: "Neither"

    详细解释

    1. isIPv4 函数
      • 使用 split('.') 将字符串按点分割。
      • 检查分割后的部分数量是否为 4。
      • 对每个部分进行以下检查:
        • 是否为数字。
        • 转换为整数后是否在 0 到 255 范围内。
        • 去除前导零后是否与原字符串相同。
    2. isIPv6 函数
      • 使用 split(':') 将字符串按冒号分割。
      • 检查分割后的部分数量是否为 8。
      • 对每个部分进行以下检查:
        • 长度是否在 1 到 4 之间。
        • 每个字符是否为数字或十六进制字符(a-f 或 A-F)。
    3. 主函数 validIPAddress
      • 调用 isIPv4isIPv6 函数进行验证,根据结果返回 “IPv4″、”IPv6” 或 “Neither”。

    通过这种方式,可以有效地验证给定的字符串是否是有效的 IP 地址,并返回相应的结果。

  • Unique Substrings in Wraparound String,LeetCode,467

    题目描述:

    我们定义字符串 s 的 “环绕字符串” 是将字符串 s 中的字符按照顺序连接成一个圆形。例如,字符串 “abc” 的环绕字符串是 “abcabcabc…” 无限循环。

    给定一个字符串 p,你需要找到 p 中不同环绕字符串的最小子串的数量。

    示例:

    输入: "a" 输出: 1

    解释: 字符串列表 ["a"].

    输入: "cac" 输出: 2

    解释: 字符串列表 ["a", "cac"].

    输入: "zab" 输出: 6

    解释: 字符串列表 ["z", "a", "b", "za", "ab", "zab"].

    解题思路:

    1. 理解环绕字符串的特性
      • 环绕字符串是无限循环的,所以我们只需要考虑字符串 p 中连续的子串。
      • 由于是环绕的,字符 'z' 后面可以接字符 'a'
    2. 动态规划的思想
      • 我们可以用一个数组 count 来记录以每个字符结尾的最长连续子串的长度。
      • 例如,如果 p 中有子串 “abc”,那么 count['c'] 至少为 3。
    3. 具体步骤
      • 初始化一个长度为 26 的数组 count,用于记录每个字符结尾的最长连续子串长度。
      • 遍历字符串 p,记录当前连续子串的长度 length
      • 如果当前字符和前一个字符是连续的(包括 'z''a' 的情况),则 length 增加 1,否则重置为 1。
      • 更新 count 数组:count[current_char] = max(count[current_char], length)
      • 最后,count 数组中的所有元素之和即为答案。

    代码实现:

    class Solution: def findSubstringInWraproundString(self, p: str) -> int: if not p: return 0

        count = [0] * 26  # 用于记录以每个字符结尾的最长连续子串长度
        length = 0  # 当前连续子串的长度
    
        for i in range(len(p)):
            if i > 0 and (ord(p[i]) == ord(p[i - 1]) + 1 or (p[i] == 'a' and p[i - 1] == 'z')):
                length += 1
            else:
                length = 1
            count[ord(p[i]) - ord('a')] = max(count[ord(p[i]) - ord('a')], length)
    
        return sum(count)

    示例用法

    sol = Solution() print(sol.findSubstringInWraproundString("a")) # 输出: 1 print(sol.findSubstringInWraproundString("cac")) # 输出: 2 print(sol.findSubstringInWraproundString("zab")) # 输出: 6

    解释:

    1. 初始化
      • count 数组初始化为全 0,表示每个字符结尾的最长连续子串长度初始为 0。
      • length 初始为 0,表示当前连续子串的长度。
    2. 遍历字符串 p
      • 对于每个字符,检查它是否和前一个字符连续(包括 'z''a' 的情况)。
      • 如果连续,length 增加 1;否则,重置 length 为 1。
      • 更新 count 数组,确保记录的是最长连续子串的长度。
    3. 计算结果
      • 最后,count 数组中的所有元素之和即为不同环绕字符串的最小子串数量。

    这种方法的时间复杂度为 O(n),空间复杂度为 O(1),因为 count 数组的长度是固定的 26。

  • Count The Repetitions,LeetCode,466

    LeetCode 466题 “Count The Repetitions” 是一个中等难度的字符串问题。题目要求我们计算一个字符串在另一个字符串中的重复次数。具体来说,给定两个字符串 s1s2,我们需要确定 s1s2 中重复的最多次数。

    题目描述

    定义两个字符串 s1s2,请编写一个函数来计算 s1s2 中重复的最多次数。这里的“重复”是指 s1 可以在 s2 中连续出现多次,并且每次出现的位置可以重叠。

    示例

    示例 1:

    输入: s1 = "ab", s2 = "abab" 输出: 2 解释: "ab" 在 "s2" 中重复了两次。

    示例 2:

    输入: s1 = "abc", s2 = "abababc" 输出: 1 解释: "abc" 在 "s2" 中重复了一次。

    解题思路

    1. 暴力解法
      • 使用双指针分别遍历 s1s2
      • 每次在 s2 中找到 s1 的一个匹配,记录匹配的次数。
      • 这种方法的时间复杂度为 O(n*m),其中 n 是 s2 的长度,m 是 s1 的长度。
    2. 优化解法
      • 利用字符串的周期性,找到 s1s2 中的周期性出现模式。
      • 通过计算 s1s2 的最小公倍数来优化匹配过程。
      • 这种方法的时间复杂度可以优化到 O(n + m)。

    代码实现

    以下是使用优化解法的 Python 代码实现:

    def getMaxRepetitions(s1, n1, s2, n2):

    记录s1中每个位置在s2中的下一个位置

    index = 0  # s2中的位置
    count = 0  # s1重复的次数
    recall = {}  # 记录出现过的(s1中的位置, s2中的位置, s1重复的次数)
    
    while True:
        # 遍历s1
        for i in range(len(s1)):
            if s1[i] == s2[index]:
                index += 1
                if index == len(s2):
                    index = 0
                    count += 1
    
        # 检查是否已经记录过当前状态
        if (index, count) in recall:
            (prev_index, prev_count) = recall[(index, count)]
            # 计算循环的长度
            circle_len = i - prev_index
            circle_count = count - prev_count
            # 计算总共可以包含多少个循环
            total_count = (n1 - prev_index - 1) // circle_len * circle_count
            # 计算剩余部分
            left_index = prev_index + (n1 - prev_index - 1) % circle_len
            left_count = recall.get((left_index, count), (0, 0))[1]
            return (total_count + left_count) // n2
        else:
            recall[(index, count)] = (i, count)
    
    return count // n2

    示例调用

    s1 = "ab" n1 = 1 s2 = "abab" n2 = 1 print(getMaxRepetitions(s1, n1, s2, n2)) # 输出: 2

    解释

    1. 初始化
      • index 表示当前在 s2 中的位置。
      • count 表示 s1s2 中重复的次数。
      • recall 是一个字典,用于记录出现过的状态 (index, count) 对应的 (i, count)
    2. 遍历 s1
      • 对于 s1 中的每个字符,检查是否与 s2 中的当前字符匹配。
      • 如果匹配,更新 indexcount
    3. 检查循环
      • 如果当前状态 (index, count) 已经在 recall 中出现过,说明找到了一个循环。
      • 计算循环的长度和循环内的重复次数。
      • 根据循环计算总的重复次数。
    4. 返回结果
      • 计算并返回最终的重复次数。

    这个优化解法通过记录状态和检测循环,避免了重复的计算,从而提高了效率。希望这个解释和代码对你理解这道题目有所帮助!

  • Optimal Account Balancing,LeetCode,465

    题目描述:

    你有 n 个账户,初始余额保存在一个长度为 n 的数组 balances 中,其中第 i 个账户的初始余额是 balances[i]。你可以从任意一个账户向另一个账户转账任意金额(可能为 0),手续费为 1 单位金额。

    你需要通过转账使得所有账户的余额都变为 0。请你计算最少需要多少单位金额的手续费。

    示例:

    输入:balances = [1,0,5,-4,-2] 输出:2 解释:

    • 从账户 0 转账 1 单位到账户 4,手续费为 1。
    • 从账户 2 转账 5 单位到账户 3,手续费为 1。 总手续费为 2。

    解题思路:

    1. 理解问题本质
      • 需要将所有账户的余额变为 0,通过转账操作,每次转账有 1 单位的手续费。
      • 目标是最小化手续费。
    2. 数学建模
      • 将所有正余额的总和记为 totalPositive,所有负余额的总和记为 totalNegative
      • 由于每次转账都会产生 1 单位的手续费,实际上我们需要关注的是如何通过最少的转账次数将正余额和负余额相互抵消。
    3. 关键观察
      • 每次转账可以看作是将一个正余额和一个负余额相互抵消的过程。
      • 最少转账次数等于正余额和负余额绝对值之和的最小值。
    4. 具体步骤
      • 计算所有账户余额的总和 totalSum
      • 如果 totalSum 不为 0,则无法通过转账使所有账户余额变为 0,返回 -1。
      • 否则,计算所有正余额的总和 totalPositive 和所有负余额的总和 totalNegative
      • 最少手续费为 min(totalPositive, -totalNegative)

    代码实现:

    class Solution: def minTransfers(self, balances: List[int]) -> int: from collections import defaultdict

        # 计算每个账户的净余额
        net_balances = defaultdict(int)
        for balance in balances:
            net_balances[balance] += 1
    
        # 只保留非零净余额
        debts = []
        for balance, count in net_balances.items():
            if balance > 0:
                debts.extend([balance] * count)
            elif balance < 0:
                debts.extend([balance] * count)
    
        # 使用递归 + 回溯的方法求解最小转账次数
        def dfs(index, debts):
            while index < len(debts) and debts[index] == 0:
                index += 1
            if index == len(debts):
                return 0
    
            min_transfers = float('inf')
            for i in range(index + 1, len(debts)):
                if debts[index] * debts[i] < 0:  # 找到符号相反的账户进行抵消
                    debts[i] += debts[index]
                    min_transfers = min(min_transfers, 1 + dfs(index + 1, debts))
                    debts[i] -= debts[index]  # 回溯
    
            return min_transfers
    
        return dfs(0, debts)

    示例用法

    sol = Solution() print(sol.minTransfers([1,0,5,-4,-2])) # 输出:2

    解释:

    1. 计算净余额
      • 使用 defaultdict 统计每个余额的出现次数。
      • 只保留非零净余额,正余额和负余额分别处理。
    2. 递归 + 回溯
      • 从第一个非零余额开始,尝试与其他符号相反的余额进行抵消。
      • 递归求解剩余余额的最小转账次数,并记录最小值。
      • 回溯恢复状态。
    3. 复杂度分析
      • 时间复杂度:最坏情况下为 O(n!),因为需要尝试所有可能的抵消组合。
      • 空间复杂度:O(n),用于存储净余额和递归调用栈。

    这种方法虽然理论上复杂度较高,但在实际情况下,通过合理的剪枝和优化,可以在可接受的时间内求解中等规模的问题。

  • Can I Win,LeetCode,464

    当然可以!LeetCode 上的 “Can I Win”(题目编号 464)是一个中等难度的动态规划问题。下面我将详细解释这个问题的背景、解题思路以及具体的代码实现。

    题目描述

    在 “Can I Win” 问题中,你正在和一个朋友玩一个游戏。游戏规则如下:

    1. 你们轮流从 1 到 maxChoosableInteger 中选择一个整数。
    2. 每次选择后,该整数将不能再被选择。
    3. 谁选到的整数之和首先达到或超过 desiredTotal,谁就获胜。

    你需要判断在先手的情况下,你是否能保证获胜。

    输入输出

    • 输入
      • maxChoosableInteger:可以选择的最大整数。
      • desiredTotal:目标和。
    • 输出
      • bool:如果你能保证获胜,返回 True,否则返回 False
  • LFU Cache,LeetCode,460

    好的,我们来详细讨论一下 LeetCode 上的第 460 题:LFU Cache(最不经常使用缓存)。

    题目描述

    LFU(Least Frequently Used)缓存是一种缓存替换策略,它根据数据访问的频率来淘汰数据。如果一个数据在缓存中很少被访问,那么它将会被优先淘汰。

    你需要实现以下功能:

    1. LFUCache(int capacity): 用以初始化一个大小为 capacity 的缓存。
    2. int get(int key): 如果键 key 存在于缓存中,则返回其值,同时增加该键的使用频率;如果不存在,则返回 -1
    3. void put(int key, int value): 如果键 key 已存在,则更新其值;如果不存在,则插入该键值对。当缓存容量达到上限时,应该淘汰使用频率最低的数据。

    示例

    LFUCache cache = new LFUCache( 2 / capacity / );

    cache.put(1, 1); cache.put(2, 2); cache.get(1); // 返回 1 cache.put(3, 3); // 去除 key 2 cache.get(2); // 返回 -1 (未找到key 2) cache.get(3); // 返回 3 cache.put(4, 4); // 去除 key 1 cache.get(1); // 返回 -1 (未找到 key 1) cache.get(3); // 返回 3 cache.get(4); // 返回 4

    解题思路

    为了实现 LFU 缓存,我们需要以下几个数据结构:

    1. 哈希表 keyToVal:用于存储键到值的映射。
    2. 哈希表 keyToFreq:用于存储键到其访问频率的映射。
    3. 哈希表 freqToKeys:用于存储频率到具有该频率的所有键的集合的映射。
    4. 变量 minFreq:用于记录当前最小的访问频率。

    详细实现

    初始化

    class LFUCache: def init(self, capacity: int): self.capacity = capacity self.keyToVal = {} self.keyToFreq = {} self.freqToKeys = collections.defaultdict(set) self.minFreq = 0

    获取值

    def get(self, key: int) -> int: if key not in self.keyToVal: return -1

        # 增加该键的使用频率
        old_freq = self.keyToFreq[key]
        self.keyToFreq[key] += 1
        new_freq = self.keyToFreq[key]
    
        # 更新 freqToKeys
        self.freqToKeys[old_freq].remove(key)
        self.freqToKeys[new_freq].add(key)
    
        # 如果 old_freq 是最小频率且其对应的集合为空,更新 minFreq
        if not self.freqToKeys[old_freq] and old_freq == self.minFreq:
            self.minFreq += 1
    
        return self.keyToVal[key]

    插入值

    def put(self, key: int, value: int) -> None: if self.capacity <= 0: return

        if key in self.keyToVal:
            # 更新值并增加频率
            self.keyToVal[key] = value
            self.get(key)  # 利用 get 方法增加频率
            return
    
        # 如果缓存已满,需要淘汰一个频率最低的键
        if len(self.keyToVal) >= self.capacity:
            evict_key = next(iter(self.freqToKeys[self.minFreq]))
            self.freqToKeys[self.minFreq].remove(evict_key)
            del self.keyToVal[evict_key]
            del self.keyToFreq[evict_key]
    
        # 插入新键值对
        self.keyToVal[key] = value
        self.keyToFreq[key] = 1
        self.freqToKeys[1].add(key)
        self.minFreq = 1

    总结

    LFU 缓存的实现相对复杂,需要维护多个映射关系以确保高效的查找和更新操作。通过使用哈希表和集合,我们能够在 O(1) 的时间复杂度内完成 getput 操作。关键在于合理管理频率信息,并在必要时淘汰最不经常使用的元素。

    希望这个详细的解析和代码实现能帮助你理解和解决 LeetCode 上的第 460 题。如果有任何进一步的问题,欢迎继续提问!

  • Repeated Substring Pattern,LeetCode,459

    题目描述:

    给定一个非空的字符串 s,检查它是否可以通过重复某个子串多次构成。你可以假设字符串 s 仅由小写英文字母组成,并且它的长度不会超过 10000。

    示例:

    输入: "abab" 输出: True 解释: 可由子串 "ab" 重复两次构成。

    输入: "aba" 输出: False

    输入: "abcabcabcabc" 输出: True 解释: 可由子串 "abc" 重复四次构成。 (或者子串 "abcabc" 重复两次构成。)

    解题思路:

    1. 字符串拼接法:
      • 将字符串 s 拼接一遍,即形成 s + s
      • 去掉拼接后的字符串的第一个字符和最后一个字符,形成新的字符串 new_s
      • 检查原字符串 s 是否在 new_s 中出现。
      • 如果出现,则说明 s 可以通过重复某个子串多次构成。
    2. 数学法:
      • 设字符串 s 的长度为 n
      • 遍历所有可能的子串长度 i(从 1 到 n/2),检查 n 是否能被 i 整除。
      • 如果 n 能被 i 整除,则检查长度为 i 的子串是否能够通过重复构成原字符串 s
      • 如果找到这样的子串,则返回 True;否则返回 False

    代码实现(字符串拼接法):

    class Solution: def repeatedSubstringPattern(self, s: str) -> bool: if not s: return False

        new_s = (s + s)[1:-1]
        return s in new_s

    代码实现(数学法):

    class Solution: def repeatedSubstringPattern(self, s: str) -> bool: n = len(s) for i in range(1, n // 2 + 1): if n % i == 0: substring = s[:i] if substring * (n // i) == s: return True return False

    解释:

    1. 字符串拼接法:
      • new_s = (s + s)[1:-1] 的目的是去掉拼接后的字符串的首尾字符,避免直接包含原字符串。
      • s in new_s 检查原字符串是否在新字符串中出现,如果出现则说明可以通过重复某个子串构成。
    2. 数学法:
      • n % i == 0 检查 n 是否能被 i 整除,确保子串长度 in 的因数。
      • substring = s[:i] 提取长度为 i 的子串。
      • substring * (n // i) == s 检查通过重复子串 substring 是否能构成原字符串 s

    复杂度分析:

    • 字符串拼接法:
      • 时间复杂度:O(n),其中 n 是字符串的长度。
      • 空间复杂度:O(n),因为生成了新的字符串 new_s
    • 数学法:
      • 时间复杂度:O(n * sqrt(n)),最坏情况下需要遍历所有可能的子串长度,并检查子串的重复构成。
      • 空间复杂度:O(1),只使用了常数额外空间。

    这两种方法各有优缺点,字符串拼接法实现简单且效率较高,而数学法更直观地展示了问题的本质。根据实际情况选择合适的方法即可。

  • Circular Array Loop,LeetCode,457

    LeetCode 457题 “Circular Array Loop” 是一个中等难度的算法题。题目要求我们检测一个数组中是否存在一个循环,该循环满足以下条件:

    1. 循环中的元素值必须一致(即循环中的所有元素值要么都是正数,要么都是负数)。
    2. 循环的长度必须大于1。

    数组的每个元素都指向数组中的一个位置,即 nums[i] 表示从位置 i 跳到位置 (i + nums[i]) % n(其中 n 是数组的长度)。

    题目描述

    给定一个含有正整数和负整数的数组 nums,如果数组中存在一个循环,则返回 true,否则返回 false

    示例

    输入: [2,-1,1,2,2] 输出: true 解释: 有一个循环,从索引 0 -> 2 -> 3 -> 0。

    输入: [-1,2] 输出: false 解释: 没有循环。

    解题思路

    我们可以使用快慢指针(Floyd’s Tortoise and Hare)算法来检测循环。具体步骤如下:

    1. 初始化快慢指针:慢指针 slow 和快指针 fast 都从当前索引 i 开始。
    2. 移动指针
      • 慢指针每次移动一步。
      • 快指针每次移动两步。
    3. 检查循环
      • 如果快指针或慢指针指向的值与当前值符号不同,则不存在循环。
      • 如果快慢指针相遇,检查循环长度是否大于1。
    4. 避免重复检查:使用一个集合或标记数组来记录已经检查过的索引。

    代码实现

    以下是使用Python实现的代码:

    def circularArrayLoop(nums): n = len(nums)

    def next_index(i):
        return (i + nums[i]) % n
    
    for i in range(n):
        if nums[i] == 0:
            continue
    
        slow, fast = i, i
        while True:
            slow = next_index(slow)
            fast = next_index(next_index(fast))
    
            if slow == fast:
                break
    
            if nums[slow] * nums[i] < 0 or nums[fast] * nums[i] < 0:
                break
    
        if slow == fast:
            # 检查循环长度是否大于1
            loop_length = 1
            fast = next_index(slow)
            while fast != slow:
                fast = next_index(fast)
                loop_length += 1
    
            if loop_length > 1:
                return True
    
    return False

    示例

    print(circularArrayLoop([2, -1, 1, 2, 2])) # 输出: True print(circularArrayLoop([-1, 2])) # 输出: False

    解释

    1. next_index函数:计算下一个索引位置。
    2. 外层循环:遍历每个元素作为起点。
    3. 内层循环:使用快慢指针检测循环。
    4. 循环长度检查:如果快慢指针相遇,检查循环长度是否大于1。

    复杂度分析

    • 时间复杂度:O(n),每个元素最多被访问两次。
    • 空间复杂度:O(1),使用了常数额外空间。

    通过上述方法,我们可以有效地检测数组中是否存在满足条件的循环。希望这个解释对你理解这道题目有所帮助!如果有更多问题,欢迎继续提问。

  • 4Sum II,LeetCode,454

    题目描述:

    给定四个包含整数的数组 nums1, nums2, nums3, nums4,计算有多少个元组 (i, j, k, l) 使得 nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

    示例:

    输入: nums1 = [1, 2] nums2 = [-2, -1] nums3 = [-1, 2] nums4 = [0, 2]

    输出: 2

    解释: 两个元组如下:

    1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
    2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0

    解题思路:

    这道题可以使用哈希表来优化求解过程。具体步骤如下:

    1. 构建哈希表:
      • 创建一个哈希表 sum_map,用于存储 nums1nums2 中所有可能的两数之和及其出现的次数。
    2. 填充哈希表:
      • 遍历 nums1nums2,计算每一对数的和 sum,并将该和及其出现的次数存储在 sum_map 中。
    3. 查找匹配的元组:
      • 遍历 nums3nums4,计算每一对数的和的相反数 -sum
      • sum_map 中查找是否存在该相反数,如果存在,则将其出现的次数累加到结果中。

    代码实现:

    def fourSumCount(nums1, nums2, nums3, nums4): sum_map = {} result = 0

    # 构建哈希表,存储 nums1 和 nums2 的所有两数之和及其出现的次数
    for num1 in nums1:
        for num2 in nums2:
            sum_ = num1 + num2
            if sum_ in sum_map:
                sum_map[sum_] += 1
            else:
                sum_map[sum_] = 1
    
    # 遍历 nums3 和 nums4,查找匹配的元组
    for num3 in nums3:
        for num4 in nums4:
            target = -(num3 + num4)
            if target in sum_map:
                result += sum_map[target]
    
    return result

    示例

    nums1 = [1, 2] nums2 = [-2, -1] nums3 = [-1, 2] nums4 = [0, 2] print(fourSumCount(nums1, nums2, nums3, nums4)) # 输出: 2

    复杂度分析:

    • 时间复杂度: O(n^2),其中 n 是每个数组的长度。我们需要遍历 nums1nums2 来构建哈希表,再遍历 nums3nums4 来查找匹配的元组。
    • 空间复杂度: O(n^2),哈希表的大小取决于 nums1nums2 中所有可能的两数之和的数量。

    总结:

    这道题通过巧妙地使用哈希表,将四数之和的问题转化为两数之和的问题,大大减少了计算复杂度。这种方法在处理多数组求和问题时非常有效,值得借鉴。

  • Delete Node in a BST,LeetCode,450

    好的,我们来详细讨论一下 LeetCode 上的第 450 题:删除二叉搜索树(BST)中的节点。

    题目描述

    给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回删除后的二叉搜索树的根节点。

    解题思路

    删除二叉搜索树中的节点可以分为以下几种情况:

    1. 节点不存在:如果 root 为空或者找不到 key,直接返回 root
    2. 节点是叶子节点:直接删除该节点,返回 null
    3. 节点只有一个子节点:删除该节点,并用其子节点替代。
    4. 节点有两个子节点
      • 找到该节点的右子树中的最小节点(即后继节点)。
      • 将该节点的值替换为后继节点的值。
      • 删除后继节点。

    代码实现

    以下是使用递归实现的代码:

    class TreeNode: def init(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right

    class Solution: def deleteNode(self, root: TreeNode, key: int) -> TreeNode: if not root: return root

        if key < root.val:
            root.left = self.deleteNode(root.left, key)
        elif key > root.val:
            root.right = self.deleteNode(root.right, key)
        else:
            # 找到了要删除的节点
            if not root.left:
                return root.right
            elif not root.right:
                return root.left
            else:
                # 找到右子树中的最小节点
                min_node = self.findMin(root.right)
                root.val = min_node.val
                root.right = self.deleteNode(root.right, min_node.val)
    
        return root
    
    def findMin(self, node: TreeNode) -> TreeNode:
        while node.left:
            node = node.left
        return node

    详细解释

    1. 基本情况
      • 如果 root 为空,直接返回 null
    2. 递归查找
      • 如果 key 小于当前节点的值,递归地在左子树中查找并删除。
      • 如果 key 大于当前节点的值,递归地在右子树中查找并删除。
    3. 删除操作
      • 当找到要删除的节点时,根据其子节点的数量进行不同的处理:
        • 如果没有左子节点,返回右子节点。
        • 如果没有右子节点,返回左子节点。
        • 如果有两个子节点,找到右子树中的最小节点(后继节点),将其值赋给当前节点,并递归地删除后继节点。
    4. 辅助函数 findMin
      • 用于找到某个节点的右子树中的最小节点。

    复杂度分析

    • 时间复杂度:O(h),其中 h 是树的高度。在最坏情况下,树可能是一条链,时间复杂度为 O(n)。
    • 空间复杂度:O(h),递归调用栈的深度。

    总结

    删除二叉搜索树中的节点是一个经典问题,需要处理多种情况。通过递归的方式,我们可以简洁地实现这一功能。关键在于正确处理有两个子节点的节点,通过找到后继节点并进行替换,保证二叉搜索树的性质不变。

    希望这个详细的解答对你有所帮助!如果有任何进一步的问题,欢迎继续提问。