LeetCode 数组类型题目做前必看

973 阅读4分钟

前言

数组类型的题目一般是 给定一个数组nums,要求在数组中查找目标数target、组合其中的元素得到某个值、对该数组进行操作满足要求。
对于某些问题,通常会有一些通用的方法作为其中的一环甚至直接可以得到答案。在刷 leetcode 之前,可以先记住这些方法,有的放矢。

排序

主要掌握快速排序。
核心步骤:

  1. 从序列中挑出一个元素作为基准(pivot)
  2. 将序列中比基准小的元素排在基准前面,比基准值大的元素排在基准后面,相等的元素可以摆在任 意一边。这一步称为 partition
  3. 在第二步完成后,pivot 元素就处于最终有序序列的位置了,保持 pivot 的元素不动,将 pivot元素两边的序列递归进行第一步和第二步
function quickSort(arr, left, right) {
    if(left < right) {
        let partitionIndex = partition(arr, left, right)
        quickSort(arr, left, partitionIndex - 1) // 递归左半部分
        quickSort(arr, partitionIndex + 1, right) // 递归右半部分
    }
}

function partition(arr, left, right) {
    let pivot = arr[right] // 最右端定为基准
    while(left < right) {
        while(left < right && arr[left] <= pivot) { // 从左向右遍历
            left ++
        }
        arr[right] = arr[left] // 比基准大的移到最右边
        while(left < right && arr[right] > pivot) { // 从右向左遍历
            right --
        }
        arr[left] = arr[right] // 比基准小的移到左边
    }
    arr[right] = pivot // 将pivot放在最终的位置
    return right // 返回该位置
}

时间复杂度可视为 O(nlogn)

二分搜索

当题目中描述,数组为有序数组或者要求算法时间复杂度为 logn 时,首先要想到的就是二分法(binary search)。
首先要记住的是二分法的前提是针对有序数组的。
二分法的思想就是,定义数组的左右边界,left, right, 以及通过左右边界计算出的中间位置 mid, 通过 mid 位置的值来判断是否符合要求,假设满足题目中规定的要找出一个元素满足函数 g,那么二分搜索的结果就是满足函数 g 的最小元素(这句话很重要!) 模版为:

const binarySearch = (nums) => {
    let l = 0
    let r = nums.length // 搜索范围为 [l,r)
    while (l < r) {
        let m = l + Math.floor((l - r) / 2)
        if (g(m)) {   // g(m) 可以理解为满足要求的条件
            r = m     // 缩小搜索范围为 [l, m)
        } else {
            l = m + 1 // 缩小范围为 [m+1, r)
        }
    }
}

此模版对应的搜索范围为左闭右开区间,如果是封闭区间的话,可以参考这个模版:

const binarySearch = (nums) => {
    let l = 0
    let r = nums.length - 1 // 搜索范围为 [l,r]
    while (l < r) {
        let m = l + Math.floor((l - r) / 2)
        if (g(m)) {   // g(m) 可以理解为满足要求的条件
            r = m - 1     // 缩小搜索范围为 [l, m-1]
        } else {
            l = m + 1 // 缩小范围为 [m+1, r]
        }
    }
}

有了这个二分搜索的模版,就可以解决一些数组的问题了,比如 leetcode35 就是完完全全的模版套用就可以得到答案。

使用二分搜索解法的题目汇总:

leetcode 4
leetcode 33
leetcode 34 leetcode 81

双指针

故名思义,就是用两个指针 left, right 分别指向数组的最左边和最右边,根据条件再移动 left 向右或者移动 right 向左,直到两个指针相遇。双指针算法可以减少算法的时间复杂度。模版为:

const doublePointer = (nums) => {
    let l = 0
    let r = nums.length - 1
    while (l < r) {
        if (g(nums[l])) { // g(x)为条件函数
            l ++
        } else {
            r --
        }
    }
}

当然有时候用到的双指针不是左右指针,而是快慢指针,同样可以解决对应的问题。

使用双指针解法的题目汇总:

leetcode 11
leetcode 15
leetcode 16
leetcode 18
leetcode 26
leetcode 27

空间换时间

空间换时间,其实就是利用额外的存储空间将需要的信息用 map 存储起来,等到查找的时候,直接通过查找 map 的 key 来判断有没有对应的数据,直接在 map 中取数只需要 O(1) 的时间复杂度,大大减少了在数组中查找对应元素的时间。 leetcode 1就是采用 hash table 的方法,将时间复杂度从 O(n²) 降到了 O(n)。

使用hash解法的题目汇总:

leetcode 1
leetcode 36

排列组合(dfs)

经常会遇到要求在给定的数组中,组合或者排列其中的元素,来找出符合要求的子数组.针对这种问题,一般可以用深度遍历优先算法(dfs)来解决,这里给出两个dfs的模板,对于排列问题和组合问题有各自的模板.
排列:

const Permutation = (nums: number[], d: number, n: number, used: boolean[], curr: number[], ans: number[][]) {
    if (d === n) {
        ans.push([...curr])
        return
    }
    for (let i = 0; i < nums.length; ++i) {
        if (used[i]) continue
        used[i] = true
        curr.push(nums[i])
        Permutation(nums, d+1, n, curr, ans)
        curr.pop()
        used[i] = false
    }
}

组合:

const Combination = (nums: number[], d: number, n: number, s: number, curr: number[], ans: number[][]) {
    if (d === n) {
        ans.push([...curr])
    }
    for (let i = s; i < nums.lenght; ++i) {
        curr.push(nums[i])
        Combination(nums, d + 1, n, i + 1, curr, ans)
        curr.pop()
    }
}
使用dfs解法的题目汇总

leetcode 79

使用组合模版题目汇总

leetcode 77
leetcode 78

位运算

异或运算性质:

  1. 任何数和 0 做异或运算,结果仍然是原来的数,即 a ⊕ 0 = a。
  2. 任何数和其自身做异或运算,结果是 0,即 a ⊕ a = 0。
  3. 异或运算满足交换律和结合律,即 a ⊕ b ⊕ a = b ⊕ a ⊕ a = b ⊕ (a ⊕ a) = b ⊕ 0 = b。

备注

tip1:
当找不到时间复杂度低于 O(n²) 的算法时,可以想象是否可以通过先将数组排序之后再寻找算法,往往会有不一样的想法。数组排序的时间复杂度可以默认为 O(nlogn)
tip2:
此文章会根据在刷题过程中遇到的相似的问题中不断总结,逐步更新,笔者的目标是总结出常规的套路,方便更好的刷题。加油吧,骚年!