Swift 数据结构与算法(39) + M_Leetcode18. 四数之和(燃烧)

92 阅读6分钟

Swift 数据结构与算法( ) + Leetcode 掘金 #日新计划更文活动

题目

18. 四数之和

labuladong 题解思路

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abc 和 d 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

你可以按 任意顺序 返回答案 。

 

示例 1:

输入: nums = [1,0,-1,0,-2,2], target = 0
输出: [[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

示例 2:

输入: nums = [2,2,2,2,2], target = 8
输出: [[2,2,2,2]]

 

提示:

  • 1 <= nums.length <= 200
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
class Solution {
    func fourSum(_ nums: [Int], _ target: Int) -> [[Int]] {

    }
}

解题思路🙋🏻‍ ♀️

题目要求:给定一个整数数组和一个目标值,我们需要找到所有独特的四元组,它们的和等于目标值。

函数返回值:返回所有满足条件的四元组的列表。

题目类型:这是一个数组遍历和双指针问题。


解题思路

  1. 排序:首先,对数组进行排序。排序的目的是使得我们可以使用双指针技术,并且可以轻松地跳过重复的元素以获得独特的四元组。

  2. 固定两个元素:对于每对元素 ( nums[i] ) 和 ( nums[j] ),使用双指针技术查找两个其他元素,使得四个元素的和为目标值。

  3. 使用双指针技术:对于每对 ( nums[i] ) 和 ( nums[j] ),将一个指针放在 ( j+1 ) 的位置,另一个指针放在数组的末尾。然后,根据这三个元素的和与目标值的关系,移动指针。

  4. 跳过重复的元素:为了得到独特的四元组,当我们移动指针时,我们需要跳过所有重复的元素。


现在,让我们图形化地描述这种方法。考虑以下数组和目标值的例子:

nums = [1,0,-1,0,-2,2], target = 0

排序后的数组为:

nums = [-2,-1,0,0,1,2]

演示

  1. 当 ( i = 0 ) (即 ( nums[i] = -2 )) 和 ( j = 1 ) (即 ( nums[j] = -1 )) 时,左指针 ( L ) 指向 ( j+1 ),右指针 ( R ) 指向数组的末尾。

    -2, -1, 0, 0, 1, 2
     i   j  L         R
    

    这四个元素的和为 0,所以我们得到一个结果:([-2,-1,0,2])。

    然后,我们移动左指针 ( L ) 到下一个独特的元素,即 ( nums[L] = 0 )。

    -2, -1, 0, 0, 1, 2
     i   j      L     R
    

    这四个元素的和为 1,大于目标值,所以我们移动右指针 ( R )。 但是,由于没有更多的元素,所以 ( j ) 移动到下一个位置。

  2. 重复上述过程,直到 ( i ) 和 ( j ) 指向数组的最后两个元素。


边界思考🤔

代码

class Solution {
    func fourSum(_ nums: [Int], _ target: Int) -> [[Int]] {
        // 对数组进行排序,这样我们可以利用双指针技巧
        var sortedNums = nums.sorted()
        
        // 存放最终的四元组结果
        var result: [[Int]] = []

        var i = 0
        // 外层循环,遍历第一个数
        while i < sortedNums.count - 3 {
            
            // 跳过与前一个数相同的数,避免重复
            while i > 0 && i < sortedNums.count - 3  && sortedNums[i] == sortedNums[i - 1] {
                i += 1
            }
            
            var j = i + 1
            // 第二层循环,遍历第二个数
            while  j < sortedNums.count - 2 {
                
                // 跳过与前一个数相同的数,避免重复
                while j > i + 1 && j < sortedNums.count - 2 && sortedNums[j] == sortedNums[j - 1] {
                    j += 1
                }
                
                // 使用双指针技巧遍历剩余的两个数
                var left = j + 1
                var right = sortedNums.count - 1
                
                // 当左指针小于右指针时,继续遍历
                while left < right {
                    
                    // 计算当前四个数的和
                    var sum = sortedNums[i] + sortedNums[j] + sortedNums[left] + sortedNums[right]

                    // 当和等于目标值时
                    if sum == target {
                        
                        // 跳过左指针重复的数
                        while left < right && sortedNums[left] == sortedNums[left + 1] {
                            left += 1
                        }
                        
                        // 跳过右指针重复的数
                        while left < right && sortedNums[right] == sortedNums[right - 1] {
                            right -= 1
                        }
                        
                        // 添加结果到最终数组
                        result.append([sortedNums[i],sortedNums[j],sortedNums[left],sortedNums[right]])
                        left += 1
                        right -= 1
                        
                    } else if sum > target { // 当和大于目标值时,右指针左移
                        right -= 1
                    } else { // 当和小于目标值时,左指针右移
                        left += 1
                    }
                }
                j += 1
            }
            i += 1
        }
        return result // 返回最终结果
    }
}

时空复杂度分析

错误与反思

1. 初始代码有误

错误片段:

for i in 0..<nums.count {
    if nums[i] > 0 {
        continue
    }
    ...
}

错误原因:

试图通过检查 nums[i] > 0 来提前结束循环,但这种方法并不总是有效的,因为存在负数和正数之和为目标值的情况。

当时的思路:

可能想减少计算量,尝试提前结束循环。

修改:

删除这部分提前结束循环的代码。

2. 未考虑数组排序

错误片段:

原始代码中没有对 nums 进行排序。

错误原因:

双指针技巧在未排序的数组上不可靠。

当时的思路:

可能是想直接对原始数组进行操作,没有意识到排序的重要性。

修改:

在代码开始时对数组进行排序:

var sortedNums = nums.sorted()

3. 重复值处理不当

错误片段:

while sortedNums[i] == sortedNums[i + 1] {
    continue
}

错误原因:

这种处理方式会导致无限循环,因为没有增加 i 的值。此外,这种方法无法正确处理重复值。

当时的思路:

可能试图避免添加重复的组合到结果中。

修改:

修改为以下逻辑,确保 i 值增加,并正确处理重复值:

while i > 0 && i < sortedNums.count - 3 && sortedNums[i] == sortedNums[i - 1] {
    i += 1
}

以后如何避免:

  1. 明确思路:在开始编码之前,先明确解题思路,确保考虑到了所有可能的情况。
  2. 注意边界条件:确保所有循环和条件语句都有明确的边界,避免无限循环或数组越界。
  3. 逐步调试:不要等到整个解决方案都写完再测试。可以分阶段地完成并测试代码,确保每一步都是正确的。
  4. 多做类似题目:通过多做类似的题目,可以积累经验,更好地认识到某些常见的陷阱和错误。

概念

使用场景与应用

1. 学习的概念和实际应用

核心概念:

  • 双指针技巧:在有序数组上,使用两个指针从两端开始移动,根据指针所指元素之和与目标值的关系来决定移动哪个指针。
  • 数组排序:为了更高效地搜索和比较,常常需要对数组进行排序。
  • 避免重复解:当数组中存在重复元素时,如何避免得到重复的组合或子序列。

实际应用场景:

  1. 数据库查询优化:当我们需要从大量数据中找到满足特定条件的记录组合时,双指针技巧和排序可以帮助我们更高效地执行查询。

    技术点:数据库索引、查询优化、内存数据结构如B-tree。

  2. 图像处理:在处理像素数据时,双指针可以用于寻找满足特定条件的像素区域或模式。

    技术点:图像二值化、边缘检测、区域增长算法。

2. 在iOS app开发中的应用

  1. 图片搜索与排序:在一个图片编辑应用中,用户可能想要根据特定的颜色或亮度范围搜索图片。双指针和排序可以帮助我们快速找到这些图片。

    示例:用户想要找到所有亮度在40-60范围内的图片。应用首先根据亮度对所有图片进行排序,然后使用双指针找到满足条件的图片区间。

  2. 音频处理:在音频编辑或音乐应用中,用户可能想要找到满足特定频率范围的音频片段。与处理图像类似,我们可以使用排序和双指针来实现。

    示例:在一个音乐应用中,用户想要找到所有频率在300-500Hz的音频片段。应用可以使用FFT (Fast Fourier Transform) 对音频进行频率分析,然后对结果进行排序,并使用双指针找到满足条件的音频片段。

  3. 快速滚动列表:在一个长列表中,用户可能想要快速滚动到满足特定条件的项目。通过预先对列表进行排序并使用双指针,应用可以迅速地导航到正确的位置。

    示例:在一个购物应用中,用户想要看到所有价格在1010-20之间的商品。应用可以根据价格对商品列表进行排序,并使用双指针快速滚动到这个价格范围。