704. 二分查找
首先介绍一下二分法。对于元素有序且不重复的数组,可以使用二分法查找某个特定元素的下标。
二分法简单来说就是循环的将数组对半分,每次都先取一个最中间的值,判断要找的元素比这个中间值大还是小,如果元素比中间值小,就将搜索区间变为上一次的搜索区间的前一半,反之就将搜索区间变为上一次的搜索区间的后一半。第一次搜索之前,将搜索区间设为整个数组。
搜索区间的变化可以使用left和right两个变量来实现,这两个变量也可以理解为指针。left和right的作用是划定要搜索的数组下标。left的初值自然是数组第一个的元素的下标,也就是0。right的初值和经过搜索后的值与区间的设定有关。最常被使用的区间是左闭右开和左闭右闭区间。
如果是左闭右开区间,则区间内的最后一个下标值对应的元素不会被搜索,需要让right的值比最后一个元素的下标大,才能保证数组内的所有元素都被搜索,因此应该将其设为nums.length。
然而,如果是左闭右闭区间, 区间内所有的下标值对应的元素都需要被搜索,此时right的初值应为数组最后一个元素的下标,nums.length - 1。
同时,区间设定的不同也会造成循环条件和右边界重新设定的轻微不同。左闭右开区间的循环条件为left < right,因为使用左闭右开区间时,left不能等于right。当target小于中间元素,需要重新确定右边界时,右边界也应该设为middle,因为右边界不会被搜索。但是对于左闭右闭区间就要使用left <= right,否则可能会遗漏元素。重新确定右边界时也应该设为middle-1。
LeetCode704即为一个最简单的二分查找的实现。以下是本题Java版的参考答案:
参考答案
注意,对于middle的计算,推荐使用右移(>>)而不是直接用left + right / 2,因为当left + right很大时有可能超过基本类型所能容纳的最大值,使用位运算可以避免这个问题,并且位运算的速度比直接使用除更快。
// 左闭右开
class Solution {
public int search(int[] nums, int target){
int left = 0;
int right = nums.length;
while (left < right) {
int middle = left + ((right - left) >> 1);
if (target < nums[middle]) {
right = middle;
} else if (target > nums[middle]) {
left = middle + 1;
} else {
return middle;
}
}
return -1;
}
}
// 左闭右闭
class Solution {
public int search(int[] nums, int target){
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int middle = left + ((right - left) >> 1);
if (target < nums[middle]) {
right = middle - 1;
} else if (target > nums[middle]) {
left = middle + 1;
} else {
return middle;
}
}
return -1;
}
}
27. 移除元素
本题可以直接使用暴力解法做出,不过一般使用的解法还是双指针解法,时间复杂度更低。
暴力解法就是直接遍历数组,碰到与要求的val相同的元素就用后一个元素替换。不过在替换时需要引入j再写一个循环,因为不用j直接写num[i]=num[i+1]的话,如果最后一个元素需要被替换,程序将无法找到num[i+1]。在遍历之前先将数组长度定义为原始长度,每替换一个元素,数组长度就-1,同时,i也需要-1,因为此位置的元素已被替换,要通过i--来重新对此位置上元素的值进行检查。
双指针法(快慢指针法)是指通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。在本题中快指针用来遍历原数组,慢指针只指向不需要被移除的元素,用来记录新数组的长度。碰到要移除的元素时,慢指针不动。直到遍历到不需移除的元素时,才将慢指针的位置填充上快指针指向的元素,之后移动一位慢指针。
这两个解法其实有相似之处。快指针的作用与暴力解法中用来遍历的i一样,因此我的代码中直接将快指针记作i。慢指针用来记录新数组的长度,也直接记为length。只不过在暴力解法中,length的初值为数组长度,随着元素被后一个元素替换而减少;而在双指针法中,length直接指向新数组的下标,因此初值为0,只有快指针找到不为val的元素时,才将那个元素的值赋给慢指针指向的下标,然后增加length的值(这一步也意味着一个元素更新完成,慢指针向后移动)。
以下是本题Java版的参考答案:
参考答案
// 暴力解法
class Solution {
public int removeElement(int[] nums, int val) {
int length = nums.length;
for (int i = 0; i < length; i++) {
if (nums[i] == val) {
// 需要引入j而不是直接使用num[i] = num[i+1],
// 否则替换最后一个元素时会因为num[i+1]不存在而越界
for (int j = i + 1; j < length; j++) {
nums[j - 1] = nums[j];
}
length--;
i--;
}
}
return length;
}
}
// 双指针
class Solution {
public int removeElement(int[] nums, int val) {
// 将新数组长度初始为0
int length = 0;
for (int i = 0; i < nums.length; i++) {
// 只有当找到值不为val的元素时,才向新数组的慢指针位置填充元素
if (nums[i] != val) {
// 原数组的前length-1个元素都会被更新,确保它们是值不为val的元素
nums[length] = nums[i];
// 同时更新此次操作后的新数组长度
length++;
}
}
return length;
}
}