1.二分查找
题目描述
给定一个 n
个元素有序的(升序)整型数组 nums
和一个目标值 target
,写一个函数搜索 nums
中的 target
,如果目标值存在返回下标,否则返回 -1
。
例子1:
输入 nums = [-1,1,3,5,8,12], target = 5
输出 3
例子2:
输入 nums = [-1,1,3,5,8,12], target = -2
输出 -1
思路
这道题目的条件一是数组为有序数组,题目条件二数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,满足以上两个条件,就可以考虑是否使用二分法了。
这道题目的重点是:二分查找涉及的边界条件,逻辑比较简单,但就是写不好。例如写循环条件的时候,到底是 while(left < right)
还是 while(left <= right)
,和写循环体的时候,到底是right = middle
呢,还是要right = middle - 1
呢?
边界条件写不对,究根结底是没有遵循循环不变量规则。而这道题目中的循环不变量,就是你对区间的定义。
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
下面我用左闭右闭即[left, right]的定义来写二分法,左闭右开留给大家写。
二分法(左闭右闭[left.right])
区间的定义决定了如何书写代码。定义target在[left,rigjt]之间,有如下两点很重要:
1.循环条件的判断:while(left <= right) 此时为什么可以等于呢,因为区间是[left,right],所以left = right 是有意义的,使用 <= 。
2.if(nums[middle] > target) 此时目标值小于中间值,在左区间需更新right的值。怎么给right赋值呢?因为已经判断过nums[middle] > target,所以当前nums[middle]不可能是target,则right = middle - 1。
代码如下(以java为例)
时间复杂度:O(logn)
空间复杂度:O(1)
int left = 0;
int rigjt = nums.length - 1;
while(left <= right){ //区间[left,right]
int middle = (left + right) / 2;
if(nums[middle] > target)
right = middle - 1 ;// nums[middle] 不可能是目标值
else if(nums[middle] < target)
left = middle + 1;
else
return middle;
}
return -1;//找不到返回-1
总结
二分法这种非常重要的基础算法,为什么有的同学对边界的鉴定模糊不清呢?
其实最主要的是同学对区间的定义没完全理解,在循环中没有坚持使用区间的定义去做边界的处理。此题中区间的定义就是不变量,在循环中坚持根据区间的定义做边界的定理,就是循环不变原则。
2.移除元素
题目描述
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素。元素的顺序可能发生改变。然后返回 nums
中与 val
不同的元素的数量(返回新数组的长度).假设 nums
中不等于 val
的元素数量为 k
,要通过此题,您需要执行以下操作:
- 更改
nums
数组,使nums
的前k
个元素包含不等于val
的元素。nums
的其余元素和nums
的大小并不重要。 - 返回
k
。 -
示例 1: 给定 nums = [4,2,3,3], val = 3, 函数应该返回新的长度 2。并且 nums 中的前两个个元素为 4,2。
示例 2: 给定 nums = [0,2,3,2,5,7,6], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0,3,5,7,6。
思路
有同学可能会想,遇到等于val的元素直接删掉不就可以了嘛。可是你不要忘记了,数组存放的元素在内存中是连续的,如果删除某个元素,后面的元素就要往前覆盖。既然如此,我们就采用覆盖的方法。下面分享两种方法的思路:
暴力求解法(双层for 循环)
第一层for循环去遍历数组nums,第二层循环去更新数组。
代码如下:
部分主体代码(重点不是这个)
for (int i = 0; i < nums.size; i++) {
if (nums[i] == val) { // 移除目标元素,将数组集体向前移动一位
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
i--; // 数组集体向前移动一位,i下标-1
size--; // 此时数组的大小-1
}
如果数组很大的话,在力扣上可能跑不过(具体看后台给的数据)
上面不难看出
时间复杂度:O(n^2)
空间复杂度:O(1)
快慢指针
其本质就是用快慢指针在一个for循环下完成两个for循环的工作
明确快慢指针的定义
1.快指针:指向新数组所需的元素
2.满指针:指向新数组中需要更新的位置
明确快慢指针的定义,掌握后面的过程就易如反掌了。
过程如下:
代码如下(以Java为例)
class Solution {
public int removeElement(int[] nums, int val) {
int slow = 0; //指向新数组所需的元素(不包括val)
int fast = 0; //指向新数组中所需要更新的位置
for(fast = 0;fast < nums.length; fast++){
if(nums[fast] != val){
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
}
时间复杂度 O(n)
空间复杂度 O(1)
总结
快慢指针,也就是双指针在数组和链表中的操作十分常见,也是我们必须掌握的一种常规解题思想。有的同学遇到这道题,就去背代码,下次遇到还是会缺这缺那的。最为重要的是在不同的题中,明确快慢指针的定义。这才是我们制胜的法宝。