- 螺旋矩阵
- 搜索插入位置
- 查找元素第一个位置和最后一个位置
螺旋矩阵
解法:模拟,核心在于你怎么转,还有就是处理边界,边界如何收缩,什么时候停止旋转。最内圈的时候怎么处理。
通过上图的模拟来解决这个问题:
1.每条边都采取这种左闭右开来进行统一的处理。
2.分别设置四个边界,left = 0,right = matrix[0].length-1, top = 0,bottom = matrix.length-1。
3.每轮:从左上角开始转一圈,这个转圈就相当于遍历然后把结果加入进结果集。这里建议自己创建一个变量用于遍历,不要用前面预定义的量。每条边处理完就边界往内收缩1。
4.什么时候停:从图中,最内部那里的处理显然就是已经停止转了。这里显然就不是循环的逻辑了,所以到内部这里就该停止。
5.这里就分两种情况,上面图里面画出来了。
第一种:属于长比宽高,这样转最终导致最后需要处理一行,此时上边界和下边界相等,而左边界和右边界不等。
第二种:属于宽比长高,这样转最终导致最后需要处理一列,此时左边界和右边界相等,而上边界和下边界不等。
所以就可以总结出啥时候停止转动,就是当上两种有其中一种情况出现时,就可以停止转了。
while(top<bottom && left<right) 。 一旦有两个边界相等就表示转到最内层了。
这两种情况搞个if else 做判断处理,分别对两种情况进行处理。
第一种情况:由于top==buttom,所以这里可以判断出横坐标为top,纵坐标起点就是left左边界,终点就是right有边界。依次将元素加入结果集
第二种情况:由于left==right,所以这里可以判断列坐标为left。纵坐标起点top上边界,终点就是bottom下边界。依次将元素加入结果集
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
int top = 0;
int bottom = matrix.length-1;
int left = 0;
int right = matrix[0].length-1;
List<Integer> list = new ArrayList<>();
while(top<bottom && left<right){
for(int i = left;i<right;i++)list.add(matrix[top][i]);
for(int i = top;i<bottom;i++)list.add(matrix[i][right]);
for(int i = right;i>left;i--)list.add(matrix[bottom][i]);
for(int i = bottom;i>top;i--)list.add(matrix[i][left]);
++left;
--right;
++top;
--bottom;
}
if(bottom == top){
for(int i = left;i<=right;i++){
list.add(matrix[top][i]);
}
}else if(left == right){
for(int i = top;i<=bottom;i++){
list.add(matrix[i][left]);
}
}
return list;
}
}
最新感悟:转完所在的一行或者一列,最好不要立即收缩边界,因为后面还会用到边界,后面用到了边界,如果收缩了,那么会导致,漏结果
搜索插入位置
注意这个题有一个很大的前提:自增不重复。
由于是有序自增不重复,涉及搜索,最快的肯定就是二分查找。所以直接写二分。
写法1:二分,左闭右闭写法。
这种处理,即left = 0。right = nums.length-1。每次处理,两侧都是封闭的区间。
所以while的条件是left<=right。可以取等,因为两侧闭区间,当相遇的时候是有意义的。
从模拟的结果来说
这里还有一个剪枝的优化。nums[mid] == target这个位置可以停,因为当相等的时候,插前面后面都一样。
否则最终插入位置都会在left停止的位置。(自己模拟一下)。
如果左闭右闭,你不进行+1或者-1操作,会发生死循环。看下面这个图的这个例子就懂了
快速记忆:二分,左闭右闭,mid剪枝早停,最终left。
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0; //这个就是左闭右的写法,那就是维护的区间初始化就是左右两边。
int right = nums.length-1; //封闭区间
while(left<=right){ //由于是封闭区间,所以是有可能左边等于右边的,因为封闭所以他们的格子是有意义的
int mid = left + (right-left)/2; //计算中点
if (nums[mid] == target){
return mid; //技巧剪枝判断早停
}else if(nums[mid]>target){
right = mid - 1; //这是封闭区间的处理,就是由于那个格子有意义,所以才进行-1.下面的left的处理是同理。
}else{
left = mid +1 ;
}
}
return left; //自己模拟,最后就是在left停下
}
}
查找元素第一个位置和最后一个位置
解法仍然是二分
**这个题仍然是递增,但是和上一题的区别就在于这个题有重复数字。上一题是没有的。**这个题如果还用上面那个题的思维,那么会出现一个情况,虽然插入后得到的数组看上去没问题,但实际上这个插入位置不一定是题目想要的第一个位置和最后一个位置。看下面这个图。
用上一个题的思路二分下来就会有这样的问题,这个指向的位置,不一定是边界。
怎么办:思路:
这里我肯定是想继续往左边找,想办法找到左边界。我们只需改在
target <= nums[mid] 时,让 right = mid - 1即可,这样我们就可以继续在 mid 的左区间继续找 5 。
怎么理解这个操作:
本来是target<nums[mid]。这里变<=。当target<nums[mid]时,就是要往左区间去找目标。而这里取一个等,就是说明这里是有可能有target = nums[mid]的可能性。但是这个mid不一定是最左,所以此时改变右区间的时候,右边界往左边再移动一格(往左边继续搜)。
这里可能有一个问题,万一这个就是左边界怎么办?
继续往后推导,最后left会停在结果的位置。所以不用担心。
直接上推导图。
找上边界就是反过来。逻辑同理,想办法往右边搜,那就是target>=nums[mid],即左边有可能会摸到,但是我还是要往右边挪动一格,那就left = mid + 1.最后right会停在最终的最后一个元素。
现在就直接写两个函数调用就完事了。
class Solution {
public int[] searchRange(int[] nums, int target) {
int left = leftIndex(nums,target);
int right = rightIndex(nums,target);
// if(nums[left] != target || nums[right] != target){
// return new int[]{-1,-1};
// }
if(left > right ){
return new int[]{-1,-1};
}
return new int[]{left,right};
}
public static int leftIndex(int[] nums,int target){
int left = 0;
int right = nums.length - 1;
while(left<=right){
int mid = left + (right - left)/2;
if(target<=nums[mid]){
right = mid -1;
}else{
left = mid + 1;
}
}
return left;
}
public static int rightIndex(int[] nums,int target){
int left = 0;
int right = nums.length - 1;
while(left<=right){
int mid = left + (right-left)/2;
if (target>=nums[mid]){
left = mid + 1;
}else{
right = mid -1;
}
}
return right;
}
}
要注意的细节:
1.关于target<=nums[mid]和target>=nums[mid],为什么找左边界要用前者,右边界要用后者。这里用反了就做不出来?
前者:可以发现找左边界的原理是不断的压缩左边维护的区间,所以target<=nums[mid]就可以使得维护的重复右边界往左边收缩。
后者:右边界的原理就是不断的压缩右边维护的区间,所以target<=nums[mid]就可以使得维护的重复左边界往左边收缩。
2.特判
// if(nums[left] != target || nums[right] != target){
// return new int[]{-1,-1};
// }
这里千万不能这么写。必须这样写:
if(left > right ){
return new int[]{-1,-1};
}
这个例子用于判断当target不在数组中。
先说结果:
当leftIndex执行结束后,left变量指向的是第一个大于target元素的位置,或者是数组长度(如果target大于所有元素)。
当rightIndex指向结束后,right指向的是最后一个小于target的元素的位置,或者是-1(如果target小于所有元素)
看模拟就懂了
nums = [5, 7, 7, 8, 8, 10],目标值 target = 6。
执行 leftIndex:
初始:left = 0, right = 5
第一次循环:mid = 2 (nums[2] = 7), 7 > 6 -> right = 1
第二次循环:mid = 0 (nums[0] = 5), 5 < 6 -> left = 1
第三次循环:mid = 1 (nums[1] = 7), 7 > 6 -> right = 0
循环结束:left = 1
可以看出最后left停留在第一个大于target元素的位置。
执行 rightIndex:
初始:left = 0, right = 5
第一次循环:mid = 2 (nums[2] = 7), 7 > 6 -> right = 1
第二次循环:mid = 0 (nums[0] = 5), 5 < 6 -> left = 1
第三次循环:mid = 1 (nums[1] = 7), 7 > 6 -> right = 0
循环结束:right = 0
可以看出最后left停留在第一个小于target元素的位置。
综上,在这个例子中,最后那肯定left>right了。
3.有可能会想到这个例子nums = []。
这个也包含在里面处理了,这样一开始就left>right了。最终left = 0,right = -1.所以也满足left>right.