学习目标
- 复习数组理论基础
- 完成二分查找和移除元素相关题目
学习内容
数组理论基础
文章链接:数组理论基础
那么二维数组在内存的空间地址是连续的么?
不同编程语言的内存管理是不一样的,以C++为例,在C++中二维数组是连续分布的。
…
像Java是没有指针的,同时也不对程序员暴露其元素的地址,寻址操作完全交给虚拟机。
所以看不到每个元素的地址情况…
…
所以Java的二维数组可能是如下排列的方式:
小结:数组是连续空间内存放相同元素的集合,但不同的语言对二维数组的实现很可能不一样,C++存储二维数组仍然是一段连续的空间,而Java由于看不到内部,且二维数组内各组数组地址不一,很可能是上述图示的排列方式。
704.二分查找(Easy)
题目链接:704.二分查找(Easy)
题目:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
思路:双指针分别指向数组 index=0
和 index=nums.length
处,mid = ((left + right) >> 1)
根据 nums[mid]
和target
大小关系的对left
和right
进行迭代。
时间复杂度:O(logn)
空间复杂度:O(1)
解决方案:
这是一种[0, nums.length-1]
的解法,个人认为比较直观易记。
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = ((left + right) >> 1);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
}
这是[0, nums.length)
的解法,与上述不同的地方有二:
- 初始化时
right = nums.length
- 迭代时
right = mid
而不是 mid+1
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length;
while(left < right) {
int mid = (left + right) >> 1;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return -1;
}
}
小结:上面是二分查找的两种方法,个人认为第一种左闭右闭区间感觉上比较对称,不容易出错。
27. 移除元素(Easy)
题目链接:27. 移除元素
题目:给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
思路1:暴力法,遇到一个就将其后面的数依次向前覆盖。
时间复杂度:O(n^2)
空间复杂度:O(1)
解决方案:
class Solution {
public int removeElement(int[] nums, int val) {
int cnt = nums.length;
for(int i=0; i < cnt; i++) {
if (nums[i] == val) {
for(int j=i+1; j < cnt; j++) {
nums[j-1] = nums[j];
}
cnt --;
i --;
}
}
return cnt;
}
}
思路2:对于数组的操作,可以考虑是否适用双指针法。这里快指针遍历查找新数组的元素,慢指针指新数组的最后一位。
时间复杂度:O(n)
空间复杂度:O(1)
解决方案:
class Solution {
public int removeElement(int[] nums, int val) {
int fast = 0, slow = 0;
while(fast < nums.length) {
if(nums[fast] != val) {
nums[slow] = nums[fast];
slow ++;
}
fast ++;
}
return slow;
}
}
小结:乍一写暴力法还略有失误,因为涉及到遍历时遍历范围的变化。因此也带来一个小思考:最好不要在遍历过程中改变遍历的范围。 过于危险。
35. 搜索插入位置(Easy)
题目链接:35. 搜索插入位置
题目:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
思路:
二分法找到目标值索引,若数组中不存在
目标值,则可能存在的位置有三处:
- 数组左侧
- 数组右侧
- 数组中间
但是这上述三种情况都会收敛于left == right
的情况(最后一次循环中),此时若:
nums[mid] > target
,则 target 位置必定是在 mid 左侧,执行的是right = mid - 1
, 此时left
不变,正好是 target 位置;nums[mid] < target
,target 位置必定在 mid 右侧 + 1,执行的是left = mid + 1;
, 正好是 target 位置。
因此找不到 target 时返回 left
的值是合理的。
时间复杂度:O(logn)
空间复杂度:O(1)
解决方案:
这是一种[0, nums.length-1]
的解法,个人认为比较直观易记。
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if(nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
}
小结:这是一种比较取巧的方法,练习该题目主要目的还是为了锻炼二分法的使用。
总结
- 二分查找可以有
[0, nums.length]
和[0, nums.length)
两种范围,两种范围写法略有差异,建议使用前闭后闭区间。 - 对于数组的操作,可以考虑是否适用双指针法。
- 最好不要在遍历过程中改变遍历的范围。