704二分查找(注意细节)
题目要求
给定一个 n
个元素有序的(升序)整型数组 nums
和一个目标值 target
,写一个函数搜索 nums
中的 target
,如果目标值存在返回下标,否则返回 -1
。
思路
数据结构中小low大high;时间复杂度为O(log n)
细节
一刷
注意while中的符号<=;注意mid值的定义,为什么要使用减法而不是加法
因为原本学习的相加/2,两个int类型加法有可能会导致溢出,为了防止溢出,先用减法再用除法最后用加法,十分巧妙
二刷
对于return的理解不深刻,return可以看作是程序的结束口。详细细节见注释。
区间问题
- 如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。
代码一(闭区间)
class Solution {
public int search(int[] nums, int target) {
int low = 0, high = nums.length - 1;
while(low <= high){//有等号是因为目前使用的是闭区间,low=high是可以成立的
int mid = low + ((high - low) / 2 );//这一步为什么能够防止溢出
if(nums[mid] < target){//因为mid处的值已经小于target了,这个值以后永远取不到,所以+1
low = mid + 1;
}else if(nums[mid] > target){
high = mid - 1 ;
}else{
return mid;
}
}
return -1;
}
}
代码二(左闭右开区间)
class Solution {
public int search(int[] nums, int target) {
int i = 0;
int j = nums.length;
while( i < j ){//左闭右开区间,i与j无法相等(不满足数学里区间的定义)[1,1)不成立
int mid = i + ((j - i ) >> 1) ;
if(nums[mid] < target){
i = mid + 1;
}else if(nums[mid] > target){
j = mid;//这里不-1是因为要满足左闭右开区间的条件。
}else{
return mid;
}
}
return -1;
}
}
27移除元素
题目要求
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1)
额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
思路
注意数组元素的删除其本身位置不会删除,原理应该是后面元素向前移动进行覆盖,原数组的长度是不变的。在使用高级编程语言后,会显示长度变化,但本身不变。时间复杂度为O(n)
暴力法
既然删除元素要覆盖,写一个嵌套for循环,当找到对应值时进行覆盖(需要for循环从后面一个一个覆盖)。
注意当某次覆盖完成之后,第一层for循环i的位置需要向前回调,准备开始继续往后面新来的里面找val。
双指针
利用两个指针在原有数组基础上创建新数组。
快指针记录当前新数组需要的元素,慢指针记录当前新数组最后一个元素的下标;当不需要删除时,即获取到新数组需要的元素,二者一起向后移动,并将快指针记录的值赋给慢指针位置上;当需要删除时,快指针向后移动慢指针不动。
使用for循环来控制快指针自发地向后移动。
代码一
class Solution {
public int removeElement(int[] nums, int val) {
//暴力法怎么删——依次遍历遇到等于val的删除,并且将数组长度-1
//删除的具体操作呢?覆盖
int size = nums.length;
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需要向后退。因为还要去找新的等于val的位置
i --;
size --;
}
}
return size;//时间复杂度是n方
}
}
代码二
class Solution {
public int removeElement(int[] nums, int val) {
//方法二:快慢指针
int slow = 0;
for(int fast = 0;fast < nums.length; fast ++){
if(nums[fast] != val){
//遇到需要的元素,即非val的其他元素
nums[slow ++] = nums[fast];
}
}
return slow;//最终返回的值也很巧妙,因为slow记录着新数组的最终下标值(这个下标值还是在原数组的基础上的)
}
}
35搜索插入位置(分情况讨论最终的返回值)
题目要求
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n)
的算法。
思路
有序数组使用二分查找,但是最终的返回值很巧妙。需要分类讨论找到最后的规律
代码
class Solution {
public int searchInsert(int[] nums, int target) {
int low =0, high = nums.length - 1;
//先写个二分查找
while(low <= high){
int mid = low + (( high - low) / 2);
if(nums[mid] < target){
low = mid + 1;
}else if(nums[mid] > target){
high = mid - 1;
}else{
return mid;
}
}
//分情况讨论[1,3,5,6]
//如果值在数组的外面的话比如0.最后high=-1,low=0,mid=0,插入位置为0
//如果插入元素在数组右边外面,最后low=4,high=3,mid=3,插入位置为4
//插入元素在数组之间的话,2,high=0,low=1,mid=0,插入位置为1;4high=1,low=2,mid=2,插入位置为2
return high + 1;//好像还能够返回low
}
}
34排序数组中查找元素的第一个和最后一个位置
题目要求
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
思路
1使用两个二分查找来分别找到左右边界,其中找的过程十分巧妙,最后分情况讨论返回值
2先用二分查找找到所在位置,如果在数组里那就继续,如果不在数组里直接返回-1;继续找的时候进行左右的遍历,满足条件向左右不断地走
代码
方法一(找左右边界设置的border很巧)
class Solution {
public int[] searchRange(int[] nums, int target) {
//本道题的思路是利用二分查找去分别找到左右边界,并且将最终结果分情况讨论
int rightBorder = searchRight(nums,target);
int leftBorder = searchLeft(nums,target);
//分情况讨论
//1目标值在数组范围之外,这种情况会导致一个结果就是rightborder和left全是-2,因为一直在往一个方向移动,即一直在某个if循环当中
if(rightBorder == -2 || leftBorder == -2){
return new int [] {-1,-1};//注意返回数组怎么返回
}
//2目标值就在数组某个元素,直接返回左右边界值,注意函数里面边界值是要比正确值偏移1的
if(rightBorder - leftBorder > 1){
return new int [] {leftBorder + 1 ,rightBorder - 1};
}
//3目标值在数组之间但不是其中的元素,直接return即可
return new int [] {-1,-1};//注意这里的返回要新创建一个数组
}
//寻找右边界
int searchRight(int[] nums,int target){
int low= 0,high = nums.length - 1;
int rightBorder = -2;//记录右边边界值的下一个位置
while(low <= high){
int mid = low + ((high - low ) / 2);
if(nums[mid] > target){
high = mid - 1;
}else{//既包含了相等又包含了小于
low = mid + 1;
rightBorder = low;//low不断地在向左边移动,最后移到的位置要比high大1
}
}
return rightBorder;
}
//寻找左边界
int searchLeft(int [] nums,int target){
int low= 0,high = nums.length - 1;
int leftBorder = -2;//记录右边边界值的下一个位置
while(low <= high){
int mid = low + ((high - low ) / 2);
if(nums[mid] >= target){//这里直接包含了大于等于
high = mid - 1 ;
leftBorder = high;
}else{
low = mid + 1;
}
}
return leftBorder;
}
}
方法二(更好理解)
class Solution {
public int[] searchRange(int[] nums, int target) {
int index = BinarySearch(nums , target);//
if(index == -1){
//没有找到,即目标值不在数组当中
return new int[]{-1,-1};
}
//找到了开始向左右遍历
int left = index;
int right = index;
while(left-1 >=0 && nums[left -1 ] == nums[index]){//注意这里的left>1,因为里面left--,最终可以减到0跳出循环,如果改成>0则会减到-1越界
left --;
}
while(right < nums.length -1 &&nums[right+1] == nums[index]){//注意这里范围不要越界
right ++;
}
return new int[]{left,right};
}
/**二分查找 */
public int BinarySearch(int[] nums , int target){
int low=0,high=nums.length - 1;
while(low<=high){
int mid = low+((high-low) / 2);
if(nums[mid] < target){
low = mid + 1;
}else if(nums[mid] > target){
high = mid -1;
}else{
return mid;
}
}
return -1;//没有找到
}
}