双指针 (快慢指针) 原地操作数组 (Golang)

584 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情

通过LeetCode 27、26、283 总结双指针法 (快慢指针法) 操作数组题型

注意:双指针法 (快慢指针法) 在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法

27. Remove Element

LeetCode链接:27. 移除元素

题目大意

给定一个数组 nums 和一个数值 val,将数组中所有等于 val 的元素删除,并返回剩余的元素个数

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

解题思路

思路1:双指针 (快慢指针)

由于题目要求删除数组中等于 val 的元素,因此输出数组的长度一定小于等于输入数组的长度,我们可以把输出的数组直接写在输入数组上

使用双指针(快慢指针):快指针 fast 指向当前将要处理的元素,慢指针 slow 指向下一个将要赋值的位置

  • 如果快指针指向的元素不等于 val,它一定是输出数组的一个元素,我们就将快指针指向的元素复制到慢指针位置,然后将快慢指针同时右移

  • 如果快指针指向的元素等于 val,它不能在输出数组里,此时慢指针不动,快指针右移一位

这道题和第 283 题基本一致,283 题是删除 0,这一题是给定的一个 val,实质是一样的

代码实现

// 使用双指针,用一层for循环做了两层for循环的事情(在一个数组上操作)
// 快指针 fast 指向当前将要处理的元素,慢指针 slow 指向下一个将要赋值的位置
// 时空复杂度O(n)、O(1)
func removeElement(nums []int, val int) int {
	slow := 0                                 // 慢指针slow是新数组的下标值
	for fast := 0; fast < len(nums); fast++ { // 快指针fast移动操作
		if nums[fast] != val {
			nums[slow] = nums[fast]   // 快指针的值赋给慢指针
			slow++
		}
	}
	return slow
}

for range 写法:(省略 fast 指针和 nums[fast],用 for _, v) (Go特有)

func removeElement(nums []int, val int) int {
    slow := 0
    for _, v := range nums { // v 即 nums[fast]
        if v != val {
            nums[slow] = v
            slow++
        }
    }
    return slow
}

思路2:双指针优化

如果要移除的元素恰好在数组的开头,例如序列 [1,2,3,4,5],当 val 为 11 时,我们需要把每一个元素都左移一位。注意到题目中说:「元素的顺序可以改变」。实际上我们可以直接将最后一个元素 5 移动到序列开头,取代元素 1,得到序列 [5,2,3,4],同样满足题目要求

这个优化在序列中 val 元素的数量较少时非常有效

实现方面,依然使用双指针,两个指针初始时分别位于数组的首尾,向中间移动遍历该序列

如果左指针 left 指向的元素等于 val,此时将右指针 right 指向的元素复制到左指针 left 的位置,然后右指针 right 左移一位。如果赋值过来的元素恰好也等于 val,可以继续把右指针 right 指向的元素的值赋值过来 (左指针 left 指向的等于 val 的元素的位置继续被覆盖),直到左指针指向的元素的值不等于 val 为止

当左指针 left 和右指针 right 重合的时候,左右指针遍历完数组中所有的元素

这样的方法两个指针在最坏的情况下合起来只遍历了数组一次

与思路1 不同的是,思路2 避免了需要保留的元素的重复赋值操作

代码实现

// 避免了需要保留的元素的重复赋值操作
func removeElement(nums []int, val int) int {
    left, right := 0, len(nums)
    for left < right {
        if nums[left] == val {
            nums[left] = nums[right-1]
            right--
        } else {
            left++
        }
    }
    return left
}

26. Remove Duplicates from Sorted Array

LeetCode链接:26. 删除有序数组中的重复项

题目大意

给定一个有序数组 nums,对数组中的元素进行去重,使得原数组中的每个元素只有一个。最后返回去重以后数组的长度值

解题思路

这道题和第 283 题,第 27 题基本一致,283 题是删除 0,27 题是删除指定元素,这一题是删除重复元素,实质是一样的

双指针 (快慢指针)

  • 使用双指针,一个指针slow始终指着已经排好的结果的数组的最后一位,一个指针fast始终往后移动
  • 初始时第一位是不用去判断重复的,那么第一位就是已经排好的最后一个位置,初始slow的下标就是0,从第二才开始要去判断是否和前面的相同,所以移动的指针fast下标为1
  • 如果slow所指向的元素和fast所指向的元素相同,fast继续往后走一个,如果fast所指向的值和slow所指向的值不同,此时fast指向的值应该排到slow所指向的值的后面,由于slow始终指向已经排好的数组的最后一个数组所以slow也需要加1,slow的位置到fast的位置这中间的相同的值是不需要的,fast加1继续下一轮判断,直到fast到达了数组末尾结束,最终slow也是指向已经排好的数组的最后一个值的下标,那么返回的个数就是slow+1

代码实现

func removeDuplicates(nums []int) int {
	slow := 0
	for fast := 1; fast < len(nums); fast++ {
		if nums[fast] != nums[slow] {
			slow++
			nums[slow] = nums[fast]
		}
	}
	return slow + 1 // return slow我研究了几个小时才发现应该是slow+1
}

注意: return slow + 1

283. Move Zeroes

LeetCode链接:283. 移动零

题目大意

题目要求不能采用额外的辅助空间,将数组中 0 元素都移动到数组的末尾,并且维持所有非 0 元素的相对位置

解题思路

这一题可以只扫描数组一遍,不断的用 slow,fast 标记 0 和非 0 的元素,然后相互交换,最终到达题目的目的

与这一题相近的题目有第 26 题,第 27 题,第 80 题

代码实现

方法1:

func moveZeroes(nums []int)  {
    slow := 0
    for fast := 0; fast < len(nums); fast++ {
        if nums[slow] == 0 {
            if nums[fast] != 0 {    
                nums[slow], nums[fast] = nums[fast], nums[slow]  // Go的独特交换方式
                slow++
            }
        } else {
            slow++
        }
    }
    // return nums[slow+1]
}

方法2:

func moveZeroes(nums []int)  {
    slow := 0
    for fast := 0; fast < len(nums); fast++ {
        if nums[fast] != 0 {
	    if fast != slow {
	        nums[slow], nums[fast] = nums[fast], nums[slow]	  
	    } 
	slow++
}

问题:这种等于0的单独判断加了会提高效率吗?

if len(nums) == 0 {
	return
}