前言
近几年的技术圈被leetcode带火了,尤其是各大厂如微软、字节跳动逢面试必出算法题,很多初学者一头扎入leetcode,满腔热血按顺序去刷题,结果头破血流。如果能找到一些规律和方法再去做,会起到事倍功半的效果。算法的强弱确实是可以辨别出一个高级工程师的能力,如果你只是单纯写业务代码,随时可以被人替代,所以向大厂高级工程师进阶必须要有一个自己精通的方向,比如:数据可视化、前端工程化、nodejs等方向。
一、怎么学习数据结构与算法
对于非计算机专业的同学来说,数据结构、计算机底层知识对他们来说是很吃 力的,所以不要急,先打好基础。这里推荐一本书《javascript数据结构与算法》,仔细看完,至少对基本的数据结构要有了解,栈、队列、链表、集合、二叉树至少要深入了解,后面还会有排序算法、搜索算法、二分查找、动态规划、滑动窗口等一些方法。如果你对这些不了解,直接刷leetcode是很懵逼的,所以磨刀不误砍柴功,先从基本功开始练起,可以在leetcode顶部导航栏 探索分类学习。
二、刷leetcode方法
OK,基于上一步你已经对数据结构和基础算法有一定了解了,开始刷leetcode,推荐一个方法,分类练习、由简入难。比如先从二分查找这类型的题目做起,leetcode有分类功能。接下来看到一道题目,先审题,如果脑子里有思路可以自己动手,一般都是暴力破解方法,先写出来,如果对这道题毫无头绪,好,直接看题解,按热度排行,一般前三个就足够了,里面的大神讲解法写的很详细,看明白之后自己再回过头去写,如果还记不住,反复背,形成肌肉记忆。写完之后去leetcode国际站(链接去掉-ch就是国际站)Discuss->Most Votes->右边选择自己的语言,可以看看国际上的一些大神的解法,同样看完前三个就可以了,明白最优解的思想,反复练习。
三、二分查找介绍
想要在一个排序数组中查找到一个数,一般的for循环时间复杂度是O(n),面试官就会问你有没有更好的方法,这时候可以用二分查找,原理就是改变左右指针,每次缩小一半查找区间,所以二分查找的时间复杂度为O(logn)。
二分查找基本框架
function binarySearch(nums, target) {
let left = 0, right = ...;
while(...) {
let mid = left + (right - left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。比如这里有一个最简单题目在已排序数组[1, 2, 3, 4, 5, 6]查找到一个目标数2,并返回它的下标,如果没找到则返回-1。用我们上面的框架就可以这样写:
function binarySearch(nums, target) {
if (nums.length === 0)return -1;
let left = 0, right = target.length - 1;
while(left <= right) {
let mid = left + Math.floor((right - left) / 2);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
}
}
return -1;
}
四、二分查找左右边界
在我们上面的简单示例中数组是不重复的,假如有这样的数组[1,2,2,2,3],target为2用上面的方法得到的索引值为2,但是如果我们想得到左右边的边界索引(1,3)呢?上面的方法就不太适用了,接下来改造一下。
查找左边界
function leftbound(nums, target) {
if (nums.length == 0) return -1;
let left = 0;
let right = nums.length; // 注意1
while (left < right) { // 注意2
let mid = (left + right) >> 1; // 注意3
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid; // 注意4
}
}
return left;
}
注意2,为什么终止条件是left < right,因为注意1right = nums.length,使得查找区间变为[left, right)左闭右开,自然left === right就可以结束循环。
注意3,>>右移运算,可以快速对两个整数求平均数,速度要比Math.floor((left + right) / 2)快,想了解更多js位运算,点击这里
注意四,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间[left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
怎么返回-1,上面的代码找不到的话是返回不了-1的,可以这样做:
while (left < right) {
//...
}
// target 比所有数都大
if (left == nums.length) return -1;
// 类似之前算法的处理方式
return nums[left] == target ? left : -1;
查找右边界
function rightbound(nums, target) {
if (nums.length == 0) return -1;
let left = 0;
let right = nums.length;
while (left < right) {
let mid = (left + right) >> 1;
if (nums[mid] == target) {
left = mid + 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
return left - 1;
}
解释:1、当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的下界 left,使得区间不断向右收缩,达到锁定右侧边界的目的。
2、为什么返回left - 1, 因为
if (nums[mid] == target) {
left = mid + 1;
// 可以看成mid = left - 1
3、返回-1
while (left < right) {
// ...
}
if (left == 0) return -1;
return nums[left-1] == target ? (left-1) : -1;
用[left, right]闭区间改造上面方法
function rightbound(nums, target) {
let left = 0, right = nums.length - 1; // 注意1
while (left <= right) { // 注意2
let mid = (right - left) >> 1;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1; // 注意3
} else if (nums[mid] == target) {
// 这里改成收缩左侧边界即可
left = mid + 1;
}
}
// 这里改为检查 right 越界的情况,见下图
if (right < 0 || nums[right] != target)
return -1;
return right;
}
大家可以使用闭区间的方法改造一下查找左边界,注意的点已标注。
结束
二分查找的基本框架和思想已经讲完了,重点要了解它的区间,掌握了它的区间范围就可以游刃有余的去套用模版。去leetcode二分查找专题下试试吧!