二分查找(LeetCode 704)
如果我想从10本厚度逐渐增加的书中找到10 cm的书,正常情况下,我会一本一本的找,那么对于n本书,工作量(时间复杂度)就是O(n)。十本找一本,工作量可能较为轻松。切入现实:如果你是图书管理员,架子上有1000本按厚度摆放的书,馆长让你找到厚度是10 cm的书…
貌似"逐个查找"工作量过大,但二分查找却能很快解决这个问题。
什么是二分查找
二分查找:也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用**顺序存储结构**,而且表中元素按关键字有序排列。
结合上述定义和例子,我们知道了 二分查找的条件
1.用于查找的内容逻辑上是有序的
2.查找的数量只能是一个,不能是多个
比如对于一个有序数组[1,2,2,3,5,9],需要查找5的位置就可以使用二分查找
思路
思想很简单,默认数组是递增的
- 选择数组中间的数和目标值进行对比
- 如果中间数和目标值相等,则返回答案
- 如果不相等
- 如果中间的数字大于目标值,则中间数字向右的所有数字都大于目标值,全部排除
- 如果中间的数字小于目标值,则中间数字向左的所有数字都小于目标值,全部排除
蓝色为排除区域
奇偶问题 和 边界问题
奇偶问题
刚学二分法的时候常常会纠结:数组长度是奇数,很容易找到最中间元素(中位数);但如果是偶数,两边数量就不一样了啊,会不会遗漏某些情况。
答案是:两个情况是一样的,无论是奇数还是偶数,都能正常使用二分法,因为:
- 两边数量不一样是一定会出现的情况
- 但是这种情况并不影响我们对中间数字和目标数字大小关系的判断
- 只要中间数字大于目标数字,就排除右边的
- 只要中间数字小于目标数字,就排除左边的
边界问题
看了很多不同平台的博主的博客和刷题网站的答案,给予的方法也是大不相同,其中主要集中在边界问题:
- while循环中 left 和 right 的关系,到底是 left <= right 还是 left < right
- 迭代过程中 middle 和 right 的关系,到底是 right = middle - 1 还是 right = middle
左闭右闭
对于left <= right,它对应的边界区间是左闭右闭的,那么对应每一次更新边界值时,**nums[middle]**的值是不在查找区间的,更新边界时就需要:right = middle - 1 & left = middle + 1。
int search(int nums[], int size, int target)
{
int left = 0;
int right = size - 1;
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
right = middle - 1;
} else if (nums[middle] < target) {
left = middle + 1;
} else {
return middle;
}
}
return -1;
}
左闭右开
同理,对于left < right,它对应的边界区间是左闭右开的,那么对应每一次更新边界值时,**nums[middle]**的值是在查找区间的两端,更新边界时就需要:right = middle & left = middle。
int search(int nums[], int size, int target)
{
int left = 0;
int right = size - 1;
while (left < right) {
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
right = middle - 1;
} else if (nums[middle] < target) {
left = middle + 1;
} else {
return middle;
}
}
return -1;
}
移除元素(LeetCode 27)
思路
对于数组来说,元素是不能删除的,因为数组的元素在内存地址中是连续存储的,不能单独删除数组中的某个元素,只能覆盖。
BF 和 双指针
暴力解法就是套两层for循环,一个for循环遍历数组元素,查找目标元素。第二个for循环遍历剩余数组元素,更新数组。
暴力算法的时间复杂度是O(n^2)
易错点:两个for循环里面,i,j的循环结束条件都是“i,j < size”,很多人会写成“i,j < nums.size()”,这是不对的。size的大小是随着删除数组元素的过程中,不断变小的。是不断变化的一个边界条件。
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for (int i = 0; i < size; i++) {
if (nums[i] == val) {
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
i--;
size--;
}
}
return size;
}
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针
- 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
- 慢指针:指向更新 新数组下标的位置
双指针的时间复杂度是O(n)
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (val != nums[fastIndex]) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
总结
苟日新,日日新,又日新
SUN YAT-SEN UNIVERSITY
2024/4/17