704.二分查找
题目链接: link
使用二分查找的前提:
1.数组为有序数组
2.数组中无重复元素
(一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的)
二分法的主要逻辑问题:边界条件的判断
对区间的定义很重要,区间的定义就是不变量。
要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
左闭右闭[left, right]
首先是常规的一些定义问题,定义两个左右变量作为区间去包含整个范围。
对于右边界,因为要求返回的是数组下标,因此right具体数组是数组的实际长度-1。
middle的定义细节使用left+ (right - left )/2;减小数组上限还可防止溢出的情况发生。
对于左闭右闭情况的考虑, 首先对于while循环的条件判断便是使用*<=* 作为条件将左右区间全部包含进去。
如果目标值小于中间值,此时将区间调整为target 在左区间,所以[left, middle - 1];
right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
同理,target 在右区间,所以[middle + 1, right]
因为闭合的那一个边界在上一轮已经包含进去讨论过了,下一轮就+1/-1给移除掉。
int left=0;
int right = nums.size() - 1 ; //因为表示数组下标,要-1
while (left <= right){
int middle = left+ (right - left )/2;
if( nums[middle] > target){
right = middle - 1; //右边闭合,以知上个middle不在target里面,可以省去
}
else if (nums[middle] < target){
left = middle + 1;
}
else if (nums[middle] == target){
return middle; //返回下标
}
}
return -1;
时间复杂度:O(log n)
空间复杂度:O(1)
左闭右开[left, right)
对于左闭右开,在基础定义上right的数值这里不需要再-1了,因为我们的初始情况永远是需要包含进去最右边的数值的,这里让right等于size的数值,右开情况下包含的最大数值就是size-1,正好就是数组下标最大值。
但在while循环进行判断时候,需要使用 < ,因为left == right在区间[left, right)是没有意义的。
if (nums[middle] > target) :
right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]。
(在右闭情况下之所以需要-1,是因为包含了middle的情况,讨论过了,这里不用包含,不用-1,在while循环的时候本身就是middle-1的数值去比较的)。
对于取右区间,因为左边界一直是闭合的,条件判断同上不用改变。
int left = 0;
int right = nums.size() ;
while (left < right){
int middle = left + (right - left)/2;
if(nums[middle] > target){
right = middle;
}
else if (nums[middle] < target){
left = middle + 1;
}
else return middle;
}
return -1;
27. 移除元素
题目链接: link
数组不能删除,只能覆盖!!!
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素
暴力解法
直接按照逻辑关系,用数组嵌套双循环来逐一判断和覆盖,最终输出结果。
一个for循环遍历数组元素 ,第二个for循环更新数组。
时间复杂度是O(n^2) //两个循环n*n
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--; //因为下标i以后的数值都向前移动了一位,所以i也向前移动一位,这样下次判断时候仍然从此前还未判定的那个下位开始判定
size --;
}
}
return size;
双指针法
(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。
定义快慢指针
*快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
慢指针:指向更新 新数组下标的位置
//5.10二次书写:
int slowIndex =0;
int fastIndex = 0;
for (fastIndex =0; fastIndex < nums.size(); fastIndex ++){
if (nums[fastIndex] != val){
nums[slowIndex] = nums [fastIndex];
slowIndex ++;
}
//如果遇到要删除数值,慢指针停留不动,快指针向后,要删除的数值就不进行覆盖,后面再进行上述判断时候被重新覆盖掉。
}
return slowIndex;
时间复杂度:O(n) //只进行了一个循环
空间复杂度:O(1) //没有新增新的变量和空间
对于双指针思想的灵活运用是非常重要的,通过类似空间换取时间的思想,一个指针代表元素,一个指针代表原有位置,替代了原本暴力思想里面的两个循环一个找元素,一个进行覆盖的操作。
另一种双向指针方法的解法:
此处便改变了元素原来的顺序,当左指针数值是要移除的,而右边是不需要移除的,就把右边的覆盖到左边,再同时缩小区间范围。
代码随想录解法(自己只是看懂了,没写)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int leftIndex = 0;
int rightIndex = nums.size() - 1;
while (leftIndex <= rightIndex) {
// 找左边等于val的元素
while (leftIndex <= rightIndex && nums[leftIndex] != val){
++leftIndex;
}
// 找右边不等于val的元素
while (leftIndex <= rightIndex && nums[rightIndex] == val) {
-- rightIndex;
}
// 将右边不等于val的元素覆盖左边等于val的元素
if (leftIndex < rightIndex) {
nums[leftIndex++] = nums[rightIndex--];
}
}
return leftIndex; // leftIndex一定指向了最终数组末尾的下一个元素
}
};
26.删除排序数组中的重复项
题目链接: link
这里同样是对于双指针的一个应用;(也可以两个循环暴力解法)
注意在测试的时候要把:
1.数组为0的情况
2.数组有重复元素
3.数组没有重复元素
等多种情况都考虑进去进行代码设计编写。
解题思想:
同样的我们用到快慢指针,如27题那样,快指针来找新的元素,慢指针进行更新操作。
我们从第二个元素开始,因为题目说明了第一个元素开始有序的递增数组,所以第二元素要么和第一个一样要么不同大于它。
如果快指针指向的元素和前一个不一样,就让慢指针等于这个元素,两个指针同时向后移动。
如果快指针此时元素和前一个一样,那么慢指针在原来位置不变,快指针继续向后移动即可。
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int slowIndex = 1;
int fastIndex = 1; //从第二个元素下标开始
int length = nums.size();
if(length == 0){ //数组为0的情况
return 0;
}
for( fastIndex= 1; fastIndex < nums.size(); fastIndex++){
if(nums[fastIndex] != nums[fastIndex-1]){
nums[slowIndex] = nums[fastIndex];
slowIndex ++;
}
if(nums[fastIndex] == nums[fastIndex-1]){
// fastIndex ++;
}
}
return slowIndex++;
}
};
时间复杂度:O(n),其中n是数组的长度。快指针和慢指针最多各移动 n 次。
空间复杂度:O(1)。只需要使用常数的额外空间。
35. 搜索插入位置
这里同样是一个二分查找的方法
代码使用的左右闭合:
唯一要注意的是题目要求如果是数组当中没有的元素,需要将其插入到对应的位置。因为前面按照mid进行区间判断已经将我们的元素锁定到最后的两个元素之间位置插入或者首尾段,那么插入只需要在最后更新的left处即可。
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid]>target){
right = mid -1;
}
else if (nums[mid]< target){
left = mid +1;
}
else if (nums[mid]= target){
return mid;
}
}
return left;
}
};