二分查找
主要就是搞清楚是左闭右闭,还是左闭右开。两种思路。
二分查找那个题感觉不难,难的是应用二分查找方法进行各种变形。
704 二分查找题目代码
版本一 左闭右闭
var search = function(nums, target) {
// right是数组最后一个数的下标,num[right]在查找范围内,是左闭右闭区间
let mid, left = 0, right = nums.length - 1;
// 当left=right时,由于nums[right]在查找范围内,所以要包括此情况
while (left <= right) {
// 位运算 + 防止大数溢出
mid = left + ((right - left) >> 1);
// 如果中间数大于目标值,要把中间数排除查找范围,所以右边界更新为mid-1;如果右边界更新为mid,那中间数还在下次查找范围内
if (nums[mid] > target) {
right = mid - 1; // 去左面闭区间寻找
} else if (nums[mid] < target) {
left = mid + 1; // 去右面闭区间寻找
} else {
return mid;
}
}
return -1;
};
(版本二)左闭右开区间 [left, right)
var search = function(nums, target) {
// right是数组最后一个数的下标+1,nums[right]不在查找范围内,是左闭右开区间
let mid, left = 0, right = nums.length;
// 当left=right时,由于nums[right]不在查找范围,所以不必包括此情况
while (left < right) {
// 位运算 + 防止大数溢出
mid = left + ((right - left) >> 1);
// 如果中间值大于目标值,中间值不应在下次查找的范围内,但中间值的前一个值应在;
// 由于right本来就不在查找范围内,所以将右边界更新为中间值,如果更新右边界为mid-1则将中间值的前一个值也踢出了下次寻找范围
if (nums[mid] > target) {
right = mid; // 去左区间寻找
} else if (nums[mid] < target) {
left = mid + 1; // 去右区间寻找
} else {
return mid;
}
}
return -1;
};
35.搜索插入位置
-
大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件。
以后大家只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。
同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的。 -
可以按照情况进行分析,比如采用左闭右开还是左闭右闭,在这个前提条件下,分析可能的几种情况,注意特殊情况的思考与探讨。然后验证程序是不是满足所有情况
-
左闭右闭的区间里,大家要仔细看注释,思考为什么要写while(left <= right), 为什么要return right+1
因为跳出循环的条件是right<left,所以此时right<left。所以right才是那个小的值,所以要right+1,或者left. -
左闭右开的区间里,大家要仔细看注释,思考为什么要写while(left <= right), 为什么要return right
因为跳出循环的条件是right<=left,所以此时right=left。而且right是一直没有取到的那个值,所以输出right即可,left也可以。
34. 在排序数组中查找元素的第一个和最后一个位置
- 写两个二分分别找左边界和右边界
- 还是注意分别讨论情况
- 根据上面的经验,寻找左边界的是right,寻找右边界的是left.
- 想不明白的时候带入例子
- 注意每道题比较的是什么,返回的是什么,之前返回的都是mid,这次返回的是left和right
69.x 的平方根
- 采用二分查找的思想,查找的中间整数,来检验其平方与目标值的大小对比。
- 别一下子合并,几种情况慢慢分析。
- 因为舍去了小数部分,所以在不直接得到等于mid的情况下,采取mid*mid < x下的mid值作为最终值。
367.有效的完全平方数
完美做出
双指针法(快慢指针法):
通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针
快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
慢指针:指向更新 新数组下标的位置
27.移除元素
- 问题关键在于需要原地移除元素,所以需要在一个数组内进行操作。
- 注意这里只需要得到移除元素后的数组长度,并没有要求得到数组。
26.删除排序数组中的重复项
- 主要考虑这行代码所涉及到的条件判断
if(nums[i] != nums[i+1])
283.移动零
- 这个题主要还是考虑上面那个条件判断
- 但是需要注意的是,这次返回的不是数组的长度,而是数组的内容。但是数组内容修改其实只修改到慢指针所在的位置。后面数组的内容其实没有被修改,所以需要重新考虑。
844.比较含退格的字符串
-
其实是两个双指针
注意:
1.不要害怕double代码,像之前二分法就重复使用两次来找到左右边界,这个可以用两个双指针
2.还是注意那个条件判断,在什么条件下指针更新呢,慢指针代表什么意思
3,大循环套着小循环,这个也是,不要怕麻烦。也可以就是说两种情况下的双指针搞完了,看各种情况应该怎么总的处理。
4.有个新的思路非常好,就是去除数组值不是remove等等,而是改变i和j的指向,直接对下面的数组值进行处理,相当于去除了某些值。达到了同样的效果 -
也可以用栈来解决
977.有序数组的平方
- 这道题直接暴力破解比较容易,但是要考虑复杂度等问题
- 数组其实是有序的, 只不过负数平方之后可能成为最大数了。
那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。
此时可以考虑双指针法了,i指向起始位置,j指向终止位置。
定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置。
如果A[i] * A[i] < A[j] * A[j] 那么result[k–] = A[j] * A[j]; 。
如果A[i] * A[i] >= A[j] * A[j] 那么result[k–] = A[i] * A[i]; 。
滑动窗口
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。
那么滑动窗口如何用一个for循环来完成这个操作呢。
209.长度最小的子数组
1.可以使用暴力解法,利用两个循环构建起滑动窗口,求解。
用两个循环寻找在不同起始距离下满足条件的最短长度。
注意:一旦满足长度就跳出当前的小循环
要不要继续循环,考虑继续循环后得到的结果还有意义吗?(更长的长度,没有意义)
每次循环前都要清空sum值:要考虑每次循环的初始条件
即:开始每次循环的条件和结束每次循环的条件
2.也可以使用滑动·窗口,即使用一个for循环
- 窗口内是什么?
窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。 - 如何移动窗口的起始位置?
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。 - 如何移动窗口的结束位置?
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引
结束位置的移动主要用于得到数据相加大于目标值的位置
起始位置的移动主要是为了找到大于目标值时最小的距离
注意考虑特殊情况,比如没有满足的条件,输出结果为0
以及所有数字相加才满足条件的情况。
(以后做算法题时都要先考虑特殊情况)
904.水果成篮
用到哈希表
76.最小覆盖子串
用到哈希表
螺旋矩阵
59.螺旋矩阵2
- 坚持循环不变量原则,坚持左闭右开
- 一条边一条边怎么走的,怎么循环的,慢慢写出来,不要怕麻烦
- 考虑转圈的次数:循环次数(转的圈数)是n/2:如果转一圈,左右都会少一条边,整个正方形的宽度减2,一共宽度是n,每次减2,就是循环n/2次
- 考虑每次转圈后的变量,offset
- 考虑特殊情况,n为奇数时中心点是哪个。
54.螺旋矩阵
剑指Offer 29.顺时针打印矩阵
这两个题是前面一个题的反顺序操作。
总结
可能因为js没有整型的原因,经常会用到math.floor来保证为整数。
时间复杂度和空间复杂度
时间复杂度
所以我们说的时间复杂度都是省略常数项系数的,是因为一般情况下都是默认数据规模足够的大,基于这样的事实,给出的算法时间复杂的的一个排行如下所示:
递归算法的时间复杂度本质上是要看: 递归的次数 * 每次递归中的操作次数。
空间复杂度
递归算法的空间复杂度 = 每次递归的空间复杂度 * 递归深度