【算法刷题系列】第1天 704. 二分查找,27. 移除元素, 977.有序数组的平方

57 阅读5分钟

学习内容

学习文档:数组理论基础

收获总结

  • 快速排序

    快速排序是一种基于分治法的排序算法。首先,选择一个基准元素,然后通过分区操作将数组划分为两部分,一部分元素小于基准值,另一部分元素大于基准值。递归地对这两部分进行排序,最终将数组排序完成。

    package main
    
    import "fmt"
    
    func quickSort(arr []int, low, high int) {
        if low < high {
            pi := partition(arr, low, high)
            quickSort(arr, low, pi-1)
            quickSort(arr, pi+1, high)
        }
    }
    
    func partition(arr []int, low, high int) int {
        pivot := arr[high]
        i := low - 1
        for j := low; j < high; j++ {
            if arr[j] < pivot {
                i++
                arr[i], arr[j] = arr[j], arr[i]
            }
        }
        arr[i+1], arr[high] = arr[high], arr[i+1]
        return i + 1
    }
    
  • 二分查找

    二分查找是一种在有序数组中查找目标值的算法。它通过不断将数组分成两半,并与中间元素进行比较,从而快速缩小查找范围,最终确定目标值的位置。如果目标值不存在,则返回 -1

    package main
    
    import "fmt"
    
    func binarySearch(arr []int, low, high, target int) int {
        if low <= high {
            mid := low + (high-low)/2
            if arr[mid] == target {
                return mid
            } else if arr[mid] > target {
                return binarySearch(arr, low, mid-1, target)
            } else {
                return binarySearch(arr, mid+1, high, target)
            }
        }
        return -1
    }
    
  • 双指针,对撞指针,快慢指针常用初始化设计

    1. 双指针:可以从数组两端开始,常用于查找两个数的和问题。初始化 i 为数组起始位置,j 为数组结束位置,当 i < j 时迭代。另一种情况是从同一端开始,适用于查找具有特定差值的元素对。

    2. 快慢指针:快慢指针常用于处理链表或数组中的问题,比如删除重复元素。ij 初始化为数组或链表的起始位置,迭代条件是 j 指针不越界。

    // 双指针 - 从两端开始
    func twoSum(arr []int, target int) []int {
        i, j := 0, len(arr)-1
        for i < j {
            sum := arr[i] + arr[j]
            if sum == target {
                return []int{i, j}
            } else if sum < target {
                i++
            } else {
                j--
            }
        }
        return nil
    }
    
    // 双指针 - 从同一端开始
    func findPairWithDiff(arr []int, diff int) []int {
        i, j := 0, 1
        for i < len(arr) && j < len(arr) {
            if i != j && arr[j]-arr[i] == diff {
                return []int{i, j}
            } else if arr[j]-arr[i] < diff {
                j++
            } else {
                i++
            }
        }
        return nil
    }
    
    // 快慢指针
    func removeDuplicates(arr []int) int {
        i, j := 0, 1
        for j < len(arr) {
            if arr[i] != arr[j] {
                i++
                arr[i] = arr[j]
            }
            j++
        }
        return i + 1
    }
    

题目解析

题目1:704. 二分查找

  • 题目描述: 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

  • 示例:

    输入: nums = [-1,0,3,5,9,12], target = 9
    输出: 4
    
  • 解法总结: 使用二分查找方法。初始化左右指针 lowhigh,每次计算中间元素并与目标值进行比较,更新指针范围,直到找到目标值或确认不存在。

  • 代码实现:

    func search(nums []int, target int) int {
        low := 0
        high := len(nums) - 1
        for low <= high {
            mid := low + (high-low)/2
            if target == nums[mid] {
                return mid
            } else if target > nums[mid] {
                low = mid + 1
            } else {
                high = mid - 1
            }
        }
        return -1
    }
    

    时间复杂度: O(log n),因为每次查找都将搜索范围缩小一半。 空间复杂度: O(1),只使用了常数空间。

题目2:27. 移除元素

  • 题目描述: 给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,返回剩余元素的数量。元素的顺序可能发生改变。

  • 示例:

    输入: nums = [3,2,2,3], val = 3
    输出: 2, nums = [2,2]
    
  • 解法总结: 使用双指针方法。ij 都从数组的头部开始遍历,当 j 指向的元素不等于 val 时,将其赋值给 i 指向的位置,并递增 i,最终返回 i 的值作为新数组的长度。

  • 代码实现:

    func removeElement(nums []int, val int) int {
        i, j := 0, 0
        count := 0
        for j <= len(nums)-1 {
            if nums[j] != val {
                nums[i] = nums[j]
                count++
                i++
            }
            j++
        }
        return count
    }
    

    时间复杂度: O(n),遍历数组一次即可完成操作。 空间复杂度: O(1),只使用了常数空间。

题目3:977.有序数组的平方

  • 题目描述: 给你一个按非递减顺序排序的整数数组 nums,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。

  • 示例:

    输入: nums = [-4,-1,0,3,10]
    输出: [0,1,9,16,100]
    
  • 解法总结: 可以采用两种方法。一种是先对所有元素平方后使用快速排序,另一种是使用双指针从数组两端向中间遍历,将较大的平方值放在结果数组的末尾。第二种方法要注意,因为给定的数组是非递减排序的,所以平方值最大的元素一定在数组的两端(中间小),所以当我们遍历需要for循环从后往前遍历!

  • 代码实现:

    方法一:先平方后排序

    func sortedSquares(nums []int) []int {
        for i := 0; i < len(nums); i++ {
            nums[i] = nums[i] * nums[i]
        }
        quickSort(nums, 0, len(nums)-1)
        return nums
    }
    
    func quickSort(arr []int, low, high int) {
        if low < high {
            pivot := partition(arr, low, high)
            quickSort(arr, low, pivot-1)
            quickSort(arr, pivot+1, high)
        }
    }
    
    func partition(arr []int, low, high int) int {
        pivotValue := arr[high]
        i := low - 1
        for j := low; j < high; j++ {
            if arr[j] < pivotValue {
                i++
                arr[i], arr[j] = arr[j], arr[i]
            }
        }
        arr[i+1], arr[high] = arr[high], arr[i+1]
        return i + 1
    }
    

    时间复杂度: O(n log n),因为对所有元素进行平方操作后,再使用快速排序。 空间复杂度: O(log n),递归调用栈的空间开销。

    方法二:双指针法

    func sortedSquares(nums []int) []int {
        i, j := 0, len(nums)-1
        result := make([]int, len(nums))
    
        for h := len(nums) - 1; h >= 0; h-- {
            if nums[i]*nums[i] > nums[j]*nums[j] {
                result[h] = nums[i] * nums[i]
                i++
            } else {
                result[h] = nums[j] * nums[j]
                j--
            }
        }
        return result
    }
    

    时间复杂度: O(n),因为只需要一次遍历即可完成操作。 空间复杂度: O(n),因为使用了额外的数组来存储结果。