前言
数组类型的题目一般是 给定一个数组nums,要求在数组中查找目标数target、组合其中的元素得到某个值、对该数组进行操作满足要求。
对于某些问题,通常会有一些通用的方法作为其中的一环甚至直接可以得到答案。在刷 leetcode 之前,可以先记住这些方法,有的放矢。
排序
主要掌握快速排序。
核心步骤:
- 从序列中挑出一个元素作为基准(pivot)
- 将序列中比基准小的元素排在基准前面,比基准值大的元素排在基准后面,相等的元素可以摆在任 意一边。这一步称为 partition
- 在第二步完成后,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解法的题目汇总:
排列组合(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解法的题目汇总
使用组合模版题目汇总
位运算
异或运算性质:
- 任何数和 0 做异或运算,结果仍然是原来的数,即 a ⊕ 0 = a。
- 任何数和其自身做异或运算,结果是 0,即 a ⊕ a = 0。
- 异或运算满足交换律和结合律,即 a ⊕ b ⊕ a = b ⊕ a ⊕ a = b ⊕ (a ⊕ a) = b ⊕ 0 = b。
备注
tip1:
当找不到时间复杂度低于 O(n²) 的算法时,可以想象是否可以通过先将数组排序之后再寻找算法,往往会有不一样的想法。数组排序的时间复杂度可以默认为 O(nlogn)
tip2:
此文章会根据在刷题过程中遇到的相似的问题中不断总结,逐步更新,笔者的目标是总结出常规的套路,方便更好的刷题。加油吧,骚年!