Swift 数据结构与算法(33 ) + Leetcode15. 三数之和(梦破碎的地方)

83 阅读5分钟

掘金 #日新计划更文活动

题目

15. 三数之和 给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意: 答案中不可以包含重复的三元组。

 

 

示例 1:

输入: nums = [-1,0,1,2,-1,-4]
输出: [[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1][-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入: nums = [0,1,1]
输出: []
解释: 唯一可能的三元组和不为 0 。

示例 3:

输入: nums = [0,0,0]
输出: [[0,0,0]]
解释: 唯一可能的三元组和为 0
class Solution {
    func threeSum(_ nums: [Int]) -> [[Int]] {

    }
}

解题思路🙋🏻‍ ♀️

读题: 这个题目, 首先i, j, k互不相等

返回值是个数组. 这个数组里面[abc] 相加之和为 0

如果 nums < 3 , 直接空数组.

不知道怎么搞啊...... 直接三层循环,来一波暴力解题

after 暴力

双指针法

截屏2023-08-14 19.25.04.png

截屏2023-08-14 19.10.57.png

  1. 先排序 固定一个左边的数值 first, 然后开始找右边的两个数. 只要是 左边的和

  2. 固定两个 point left 和 right

right 需要 移动, right = right - 1

这个左右移动规则是. 如果 left + right > first

那么 右边要减小(向左移动). 截屏2023-08-14 19.20.42.png

如果, left + right < first 那么, 左边要增加(向右边移动),

截屏2023-08-14 19.23.43.png

当最终找到了一个合适的三元数组,我们储存起来, 然后移动左边的first , 进行下一次查找.

边界思考🤔

代码

这题没啥思路先搞个 暴力解法.....

class Solution {
    func threeSum(_ nums: [Int]) -> [[Int]] {
        // 存储最终的结果
        var result: [[Int]] = []
        // 用于去重,确保我们的结果中不会有重复的三元组
        var seen: Set<[Int]> = []
        
        // 外层循环,选择第一个数字
        for i in 0..<nums.count {
            // 第二层循环,选择第二个数字,从第一个数字后的位置开始
            for j in i+1..<nums.count {
                // 第三层循环,选择第三个数字,从第二个数字后的位置开始
                for k in j+1..<nums.count {
                    // 检查这三个数字的和是否为0
                    if nums[i] + nums[j] + nums[k] == 0 {
                        // 如果和为0,对这三个数字排序,确保它们的顺序始终一致
                        let triplet = [nums[i], nums[j], nums[k]].sorted()
                        // 检查我们之前是否已经找到过这个三元组
                        if !seen.contains(triplet) {
                            // 如果没有找到过,添加到结果中,并将其添加到“已见过”的集合中
                            result.append(triplet)
                            seen.insert(triplet)
                        }
                    }
                }
            }
        }
        // 返回最终的结果
        return result
    }
}

// 定义解决方案类
class Solution {
    // 主要的解决方法
    func threeSum(_ nums: [Int]) -> [[Int]] {
        
        // 如果数组的数量小于3,则直接返回空数组,因为我们不能形成三元组
        if nums.count < 3 {
            return []
        }
        
        // 对数组进行排序,这是为了后续能够更加方便地避免重复的三元组
        var sortNums = nums.sorted()
        
        // 用于存放结果的数组
        var result:[[Int]] = []
        
        // 开始主循环,遍历每一个数字
        for i in 0..<nums.count {
            
            // 如果当前的数字与前一个数字相同,则跳过,以避免重复的三元组
            if i > 0 && sortNums[i] == sortNums[i-1] {
                continue
            }
            
            // 定义左右指针,用于后续的双指针遍历
            var left:Int = i + 1
            var right:Int = nums.count - 1

            // 当左指针小于右指针时,进行双指针遍历
            while left < right {
                
                // 计算当前左右指针指向的两个数字之和
                let sum = sortNums[left] + sortNums[right]

                // 如果三数之和大于0,则将右指针左移
                if sum + sortNums[i] > 0  {
                    right -= 1
                } 
                // 如果三数之和小于0,则将左指针右移
                else if sum + sortNums[i] < 0  {
                    left += 1
                } 
                // 如果三数之和等于0,则记录这个三元组,并同时移动左右指针
                else {
                    // 避免左指针重复
                    while left < right && sortNums[left] == sortNums[left + 1] {
                        left += 1
                    }
                    // 避免右指针重复
                    while left < right && sortNums[right] == sortNums[right - 1] {
                        right -= 1
                    }
                    // 记录当前三元组
                    result.append([sortNums[i],sortNums[left],sortNums[right]])
                    // 移动左右指针
                    left += 1
                    right -= 1
                }
            }
        }
        // 返回结果数组
        return result
    }
}

时空复杂度分析

时间复杂度:

  1. 排序数组: (O(n \log n)),其中 (n) 是数组 nums 的长度。
  2. 遍历数组: (O(n))。
    • 对于每个数字,最坏情况下使用双指针遍历整个数组: (O(n))。

因此,总的时间复杂度是 (O(n \log n) + O(n^2) = O(n^2))。

空间复杂度:

  1. 排序后的数组:(O(n))。
  2. 结果数组:在最坏的情况下,每个元素都可能参与到结果中,但考虑到三元组的性质,实际的结果数组大小远小于 (n^3),但为了简化分析,我们可以认为其大小为 (O(n^3))。
  3. 其他变量(如指针)使用的空间是常数,即 (O(1))。

但在这个问题的背景下,我们知道结果的三元组数量是有限的(不可能有 (n^3) 个不同的三元组和为0),所以实际的空间复杂度主要取决于排序后的数组和其他常数空间,即 (O(n))。

综上,这个算法的时间复杂度是 (O(n^2)),空间复杂度是 O(n)。

错误与反思

// 跳过重复的元素 
while left < right && sortedNums[left] == sortedNums[left + 1] { left += 1 } 

while left < right && sortedNums[right] == sortedNums[right - 1] { right -= 1 }

这两行代码的核心目的是跳过重复的元素,以防止解集中出现重复的三元组。

首先,我们的数组是排序过的。这意味着重复的数字会被放在一起。因此,检查连续的元素是否相同是检测重复的一种有效方法。

现在,我们一步一步来解释这两行:

  1. while left < right && sortedNums[left] == sortedNums[left + 1] { left += 1 }

这一行代码的目的是跳过左指针left的重复元素。

  • left < right:这是一个安全检查,确保left指针不会越过right指针。
  • sortedNums[left] == sortedNums[left + 1]:这检查当前left指针指向的元素是否与其旁边的元素相同。如果是,则意味着有重复。
  • left += 1:如果当前元素与下一个元素相同(即重复),我们就移动left指针。

因此,这个while循环会一直运行,直到left指针不再指向重复的元素。

  1. while left < right && sortedNums[right] == sortedNums[right - 1] { right -= 1 }

这一行代码的目的与上一行相同,但是针对的是right指针。

  • left < right:同样是一个安全检查。
  • sortedNums[right] == sortedNums[right - 1]:检查right指针是否指向重复的元素。
  • right -= 1:如果当前元素与前一个元素相同(即重复),我们就移动right指针。

这个while循环会一直运行,直到right指针不再指向重复的元素。

总结:这两行代码确保了当我们找到一个满足条件的三元组后,我们不会因为leftright指针指向的重复元素而再次记录相同的三元组。

  1. 未处理重复值:如果不考虑重复值,结果中可能会有重复的三元组。
  2. 双指针移动不当:在找到一个解后,必须正确移动左右指针,否则可能会遗漏某些解或者添加重复的解。
  3. 边界条件:例如,当数组长度小于3时应立即返回空数组,以及循环的范围和条件。

` // 如果当前的数字与前一个数字相同,则跳过,以避免重复的三元组

        if i > 0 && sortNums[i] == sortNums[i-1] {
            continue
        }`

for 循环中,范围是 0..<nums.count,而不是 0..<sortNums.count。虽然这两者在此场景中是等价的,但为了代码的一致性和可读性,最好使用 sortNums.count

为什么会发生这样的错误?

  • 当编写代码时,可能先写了 nums.count,后来又决定使用排序后的数组,但忘记了更新循环范围。

如何避免这样的错误?

  • 当你对一个变量进行修改或替换时,确保更新了与该变量相关的所有引用。

概念

核心概念:

  1. 排序 (Sorting):排序是这个问题的一个关键步骤。通过对数组进行排序,我们可以方便地避免重复的三元组,并使用双指针技巧来找到所有的解。

  2. 双指针技巧 (Two Pointers Technique):在排序后的数组中,固定一个数字后,我们使用两个指针(一个从左边开始,另一个从右边开始)来查找两个数字,使得它们的和满足特定的条件。在这个问题中,我们希望找到两个数字,使得它们的和加上固定的数字等于0。

  3. 数组去重 (De-duplication of Arrays):由于结果中不允许有重复的三元组,我们需要使用一种方法来避免添加重复的三元组。在排序后的数组中,我们可以通过检查连续的相同数字来实现这一点。

  4. 枚举 (Enumeration):尽管这个概念在这个问题中可能不是非常明显,但我们实际上是在对数组中的每个数字进行枚举,然后使用双指针技巧来找到与其组成和为0的三元组。

这些核心概念不仅适用于这个特定的问题,而且在其他很多算法问题中也都很有用。掌握这些概念可以帮助你更有效地解决类似的问题。

使用场景与应用

1. 最需要学习的概念:

  • 排序 (Sorting): 对数据进行排序使得其满足某种顺序,这可以简化后续的操作和搜索过程。
  • 双指针技巧 (Two Pointers Technique): 通过使用两个指针来同时遍历数组,可以有效地解决某些问题,特别是与数组和列表相关的问题。
  • 数组去重 (De-duplication of Arrays): 在处理数据时,去重是一个常见的需求,特别是当我们不希望在结果中有重复的元素时。

实际应用场景及技术点:

  • 搜索引擎排名 (Search Engine Ranking):
    • 技术点: 排序。搜索引擎需要根据网页的相关性和其他因素对搜索结果进行排序。
  • 音乐播放器中的快进和快退 (Fast Forward and Rewind in Music Player):
    • 技术点: 双指针。一般的快进和快退可以通过两个指针实现,一个指针指向当前播放的位置,另一个指针指向用户希望跳转到的位置。
  • 电子邮件或消息应用中的查找重复联系人 (Finding Duplicate Contacts in Email or Messaging Apps):
    • 技术点: 数组去重。在导入联系人或同步联系人时,可能会有重复的联系人,需要使用去重技术来删除这些重复的联系人。

2. iOS app 开发的实际使用场景及与本题技术的应用:

  • 图片浏览器中的缩放功能 (Zooming in Image Viewer):
    • 技术点: 双指针。用户可以使用两个手指进行缩放,这时可以使用双指针技巧来检测两个手指的位置,并据此调整图片的大小。
  • 联系人应用中的排序和查找功能 (Sorting and Searching in Contacts App):
    • 技术点: 排序。联系人可以按照姓名、公司或其他属性进行排序。双指针也可以用于快速地查找联系人。
  • 音乐播放器中的去重功能 (De-duplication in Music Player):
    • 技术点: 数组去重。当用户导入歌曲或播放列表时,可能会有重复的歌曲,这时需要使用去重技术来删除这些重复的歌曲。

最后, 鸣谢,

Youtube 快要秃头小哥 NB!

比 B站好多乱七八糟博主说的好多了.

截屏2023-08-14 21.08.55.png www.youtube.com/watch?v=hNR…