Swift 数据结构与算法(三 ) 数组 + Leetcode27. 移除元素(快慢指针)

96 阅读3分钟

概念

快慢指针

快慢指针是一种常用的编程技巧,主要用于处理数组或链表等线性结构。这种技术使用两个指针(或者说两个索引)来遍历数据结构:一个指针移动得比另一个快,因此被称为“快指针”,而另一个被称为“慢指针”

2 快慢指针的一些常见应用
  1. 找到链表的中间节点: 假设我们有一个包含七个节点的链表:1 → 2 → 3 → 4 → 5 → 6 → 7,我们希望找到中间的节点。
初始状态:
1(s,f) → 234567

第一步(快指针移动两步,慢指针移动一步):
12(s) → 34(f) → 567

第二步(快指针移动两步,慢指针移动一步):
123(s) → 45(f) → 67

第三步(快指针移动两步,慢指针移动一步):
1234(s) → 567(f)

当快指针到达链表的末尾时,慢指针恰好在链表的中间。

  1. 检测链表中的循环: 假设我们有一个包含环的链表:1 → 2 → 3 → 4 → 5 → 6 → 3(3后面的箭头表示这是一个环,回到了3)。
初始状态:
1(s,f) → 23456
          ↖______________↙

第一步(快指针移动两步,慢指针移动一步):
12(s) → 34(f) → 56
          ↖______________↙

第二步(快指针移动两步,慢指针移动一步):
123(s) → 45(f) → 6
          ↖______________↙

第三步(快指针移动两步,慢指针移动一步):
1234(s) → 56(f)
          ↖______________↙

第四步(快指针移动两步,慢指针移动一步):
123(f,s) → 456
          ↖______________↙

当快慢指针相遇时,说明链表存在环。

  1. 数组去重: 假设我们有一个有序数组 [1, 1, 2, 2, 3, 4, 4],我们希望删除重复的元素。
初始状态:
1(s,f), 1, 2, 2, 3, 4, 4

第一步(快指针移动,因为快慢指针指向的值相同):
1(s), 1(f), 2, 2, 3, 4, 4

第二步(快指针移动,快慢指针指向的值不同,交换元素并移动慢指针):
1, 2(s,f), 2, 3, 4, 4

......

最后一步(数组完全遍历后):
1, 2, 3, 4(s), 2, 4, 4(f)

当快指针遍历完数组后,慢指针前面的部分就是不重复的元素。

题目

27. 移除元素

给你一个数组 nums **和一个值 val,你需要 原地 移除所有数值等于 val **的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

 

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

 

示例 1:

输入: nums = [3,2,2,3], val = 3
输出: 2, nums = [2,2]
解释: 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

示例 2:

输入: nums = [0,1,2,2,3,0,4,2], val = 2
输出: 5, nums = [0,1,4,0,3]
解释: 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

 

提示:

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 50
  • 0 <= val <= 100

解题思路🙋🏻‍ ♀️

Q: 为什么不能直接遍历去数呢? 或者通过直接删除数组的元素来计算?(我们大家都会想到直接删除,然后获取出不包含 val 的元素个数)

空间复杂度为 O(1) 我们无法新增空间

时间复杂度可能增加:在数组中删除元素通常需要 O(n) 的时间复杂度,其中 n 是数组的长度。因为在删除元素后,你需要将后面的所有元素向前移动一位以填补空位。如果你在遍历数组的同时删除元素,那么总的时间复杂度可能会达到 O(n^2),这比使用双指针的方法要慢。

可能会导致错误:在遍历数组的同时修改数组(例如删除元素)可能会导致错误或者未定义的行为。例如,你可能会错过一些元素,或者遍历到不应该遍历到的元素。这是因为当你删除一个元素时,后面的元素会向前移动,这可能会影响到你的遍历。

思路

有一个数组 nums = [0, 1, 2, 2, 3, 0, 4, 2],并且 val = 2

  1. 初始状态:快慢指针都指向数组的第一个元素。
nums = [0,1,2,2,3,0,4,2]
       s,f
  1. 移动快指针:当 nums[fast] 不等于 val 时,我们复制 nums[fast]nums[slow] 的位置,并将 slowfast 都向前移动一步。
nums = [0,1,2,2,3,0,4,2]
         s,f
  1. 快指针遇到 val:当 nums[fast] 等于 val 时,我们只移动 fast 指针。
nums = [0,1,2,2,3,0,4,2]
         s   f
  1. 再次移动快指针:当 nums[fast] 不等于 val 时,我们复制 nums[fast]nums[slow] 的位置,并将 slowfast 都向前移动一步。
nums = [0,1,3,2,3,0,4,2]
           s   f
  1. 重复这个过程:我们会重复上述过程,直到 fast 指针遍历完整个数组。在每次 nums[fast] 不等于 val 的情况下,我们都会将 nums[fast] 的值复制到 nums[slow] 的位置,并将 slow 指针向前移动一步。
nums = [0,1,3,0,4,2,4,2]
                 s   f
  1. 返回结果:最后,我们返回 slow 指针的位置,它表示数组中不等于 val 的元素的数量。

在这个例子中,数组中不等于 val 的元素的数量是 5,因此我们返回 5。同时,数组的前5个元素被修改为所有不等于 val 的元素,这就相当于我们已经删除了所有等于 val 的元素。

边界思考🤔

  1. 数组为空:如果数组 nums 为空,函数应该直接返回 0,因为没有任何元素需要删除。

  2. 数组为空

var nums: [Int] = []
let val = 3
let len = removeElement(&nums, val)  // 返回 0

这里,因为 nums 是空的,所以函数应该返回 0。

代码

第一遍错误

这道题第一遍写错了.

         if nums[fast] == val {
            fast += 1
         }

nums[fast] == val 时,fast 指针会前进一步,然后代码将立即检查 nums[fast] != val。如果 nums[fast] 在前进一步后仍然等于 val,那么 nums[fast] != val 的检查将失败,而 fast 指针不会再前进。这将导致 fast 指针陷入死循环,无法前进。

import Foundation

class Solution {
    func removeElement(_ nums: inout [Int], _ val: Int) -> Int {

        if nums.count == 0 {
           return 0 
        }

         var fast = 0 
         var slow = 0

         while fast <= nums.count - 1 {
             //  当快指针 == val 的时候, 直接跳过. 因为要把不等于 val 才置换
             if nums[fast] == val {
                fast += 1
             }
             // 当快指针 != val 的时候, 交换值, 一起 +1
             if nums[fast] != val {
                 nums[slow] = nums[fast]
                 slow += 1
                 fast += 1
             }
         }
        
         return slow 
    }
}

修改后版本

import Foundation

class Solution {
    // 定义一个名为 removeElement 的方法,它接受一个整数数组(通过引用传递)和一个整数值,并返回一个整数
    func removeElement(_ nums: inout [Int], _ val: Int) -> Int {
        // 如果数组为空,则直接返回0
        if nums.count == 0 {
           return 0 
        }

        // 定义两个指针:快指针 fast 和慢指针 slow,都初始化为0
        var fast = 0 
        var slow = 0

        // 当快指针没有遍历完数组时,执行循环体
        while fast <= nums.count - 1 {
            // 当快指针指向的元素不等于 val 时
            if nums[fast] != val {
                // 将快指针指向的元素复制到慢指针指向的位置
                nums[slow] = nums[fast]
                // 将慢指针向前移动一步
                slow += 1
            }
            // 将快指针向前移动一步
            fast += 1
        }
        
        // 返回慢指针的值,这就是数组中不等于 val 的元素的数量
        return slow 
    }
}

时空复杂度分析

o (n)