时间复杂度(后续更新)
- 推导公式:T(n)=T(n/2)+O(1)
- 根据时间复杂度倒退算法是面试中常用的策略
- 带名字的某算法在面试中大概率不会考察,因为这类算法一般都是针对解决某一种问题,适用性低
复杂度 | 可能对应的算法 | 备注 |
---|---|---|
O(1) | 位运算 | 常数级复杂度,一般面试中不会有 |
O(logn) | 二分法,倍增法,快速幂算法,辗转相除法 | |
O(n) | 枚举法,双指针算法,单调栈算法,KMP算法,Rabin Karp,Manacher’s Algorithm | 又称作线性时间复杂度 |
O(nlogn) | 快速排序,归并排序,堆排序 | |
O(n^2) | 枚举法,动态规划,Dijkstra | |
O(n^3) | 枚举法,动态规划,Floyd | |
O(2^n) | 与组合有关的搜索问题 | |
O(n!) | 与排列有关的搜索问题 |
二分法
-
面试时能不递归就不要递归,因为递归是一个不好的coding pattern。可以和面试官讨论用不用recursion。
-
二分法的时间复杂度为Ο(log n)
模板1
while的目的是缩小一半的区间
class Solution{
//left ≤ target ≤ right (left和right相邻)
public int binarySearch(int[] nums, int target) {
if (nums == null || nums.length == 0){
return -1;
}
int left = 0, right = nums.length - 1;
// 要点1: left + 1 < right
while (left + 1 < right) {
// 要点2:left + (right - left) / 2
int mid = left + (right - left) / 2;
//要点3:=, <, > 三种情况分开讨论,mid 不+1也不-1
if (nums[mid] == target) {
right = mid;//要点4
} else if (nums[mid] < target) {
left = mid;
} else if (nums[mid] > target) {
right = mid;
}
}
//要点5:出循环的时候,判断谁是题目所要的答案
if (nums[left] == target) {
return left;
}
if (nums[right] == target) {
return right;
}
return -1;
}
}
常见问题
Q: 要点1处为什么要用 left + 1 < right?而不是 left < right 或者 left <= right?
A: 为了避免死循环。二分法的模板中,整个程序架构分为两个部分:
- 通过 while 循环,将区间范围从 n 缩小到 2 (只有 left 和 right 两个点)。
- 在 left 和 right 中判断是否有解。
left < right 或者 left <= right 在寻找目标最后一次出现的位置的时候,会出现死循环。
Q: 要点2处为什么要用 left + (right - left) / 2?而不是 (left + right) / 2?
A: 为了避免可能出现加法溢出,也就是说加法的结果大于整型能够表示的范围,因为这里的/号的结果多向左取数。但是left 和 right都为正数,因此 right - left 不会出现加法溢出问题。关于为什么left + (right - left) / 2 不会Overflow,可以参考:why-leftright-left-2-will-not-overflow。
Q: 要点3处为什么明明可以 left = mid + 1 偏偏要写成 left = mid?
A: 大部分时候,mid 是可以 +1 和 -1 的。在一些特殊情况下,比如寻找目标的最后一次出现的位置时,当 target 与 nums[mid] 相等的时候,是不能够使用 mid + 1 或者 mid - 1 的。因为会导致漏掉解。那么为了节省脑力,统一写成 left = mid 或 right = mid 并不会造成任何解的丢失,并且也不会损失效率——log(n) 和 log(n+1) 没有区别。
Q: 要点4处为什么不直接return mid?
A: 分成三种情况。
- 当寻找左侧边界的二分搜索,此处需要设置为
right = mid;
。找到 target 时不要立即返回,而是缩小「搜索区间」的上界right
,在区间[left+1, right)
中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。 - 当查找目标值在排序数组中的末尾位置时候(寻找右侧边界的二分搜索),此处需要设置为
left = mid;
; - 当只是单纯的查找一个在排序数组中的目标值时,则可以设置为return mid。
Q: 要点5处为什么要设置判断?
A:该模板中,当left和right相邻时候,就不会走while循环,此时target就是left和right两个值的其中一个,故此处做判断。
变形
- Binary Search on Index - OO==X==X
先用ArrayList倍增找到≥target的一个区间范围,再去用二分法找到题目所要求的target。
- Binary Search on Index - Half half:上上下下,多个谷峰
分成四种情况讨论mid所在的位置,即波谷,波峰,上升趋势的某点,下降趋势的某点
模板2
- 寻找一个数
- 寻找左侧边界的一个数
- 寻找右侧边界的一个数
//迭代写法 <=
//right < target < left
int binary_search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; //[left,right]
while(left <= right) { //只有right + 1 = left时才跳出此循环
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if(nums[mid] == target) {
// 直接返回
return mid;
}
}
// 直接返回
return -1;
}
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定左侧边界
right = mid - 1;
}
}
// 最后要检查 left 越界的情况
if (left >= nums.length || nums[left] != target)
return -1;
return left;
}
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定右侧边界
left = mid + 1;
}
}
// 最后要检查 right 越界的情况
if (right < 0 || nums[right] != target)
return -1;
return right;
}