这是我参与「第五届青训营 」伴学笔记创作活动的第 16 天
1. 数组
数组是存放在连续内存空间上的相同类型数据的集合。
数组可以方便的通过下标索引的方式获取到下标下对应的数据。
二分查找
数组为 有序数组,同时数组中无重复元素, 因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件
左闭右闭 [left, right]********
- while (left <= right) ****要使用 <= ,因为left == right是有意义的,所以使用 <=
- if (nums[middle] > target) ****right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
左闭右开 [left, right)****
- while (left < right) ,这里使用 < ,因为left == right在区间[left, right)是没有意义的
- if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
left + ((right -left) >> 1) == (left + right) /2
>>: 二进制右移
举个例子: 1010 >> 1 == 0101
1010 十进制 10
0101 十进制 5
综上 >> 1 作用相当于除二
所以 left + ((right -left) >> 1) ==> left + ((right -left)/2)
==> left + right/2 -left/2 ==> left/2 + right/2 ==> (left + right) /2
问题 :为什么不直接用(left + right) /2 而用left + ((right -left) >> 1)
答: 是因为left + right 在某种情况下可能会超过基本类型所能容纳的最大值,而且 >> (位运算) 比 / 运算要快一点
在排序数组中查找元素的第一个和最后一个位置
解题思路:
找左右边界,如果在数组中有一个target,在下标3,左右边界为【4,5】
找边界:target在左边记录right,会越往左走
target在右边记录left,会越往右走
在return时,
- 只要有一个边界为-2, target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1} if(leftBorder === -2 || rightBorder === -2 )
- target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
if (rightBorder - leftBorder > 1)
return [leftBorder + 1, rightBorder - 1]
- target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1} return [-1, -1]
移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
暴力解法
这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。
双指针法
双指针法(快慢指针法) : 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针
- 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
- 慢指针:指向更新 新数组下标的位置
注意这些实现方法并没有改变元素的相对位置
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (val != nums[fastIndex]) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
题目:外面有宝,赶紧捡回来按序放好,不能重样哟 有点像小夫妻俩,老公q在外面淘宝,找到后运回来,找到一个新的宝,老婆p在家里就给挖个新坑放好,最后外面没宝了,就结束咯
中间对话
老公:老婆,这个家里有没?(if) 老婆:有了。(nums[p] == nums[q])你再找找(q++)
老公:老婆,这个家里有没?(if) 老婆:有了。(nums[p] == nums[q])你再找找(q++)
老公:老婆,这个家里有没?(if) 老婆:这个没有,拿回来吧 (nums[p] != nums[q]) 放好了,我到下一个位置等你(p++) 你再继续找吧(q++)
貌似双指针都可以这么理解
相向双指针方法,基于元素顺序可以改变的题目描述改变了元素相对位置,确保了移动最少元素
int leftIndex = 0;
int rightIndex = nums.size() - 1;
while (leftIndex <= rightIndex) {
// 找左边等于val的元素
while (leftIndex <= rightIndex && nums[leftIndex] != val){
++leftIndex;
}
// 找右边不等于val的元素
while (leftIndex <= rightIndex && nums[rightIndex] == val) {
-- rightIndex;
}
// 将右边不等于val的元素覆盖左边等于val的元素
if (leftIndex < rightIndex) {
nums[leftIndex++] = nums[rightIndex--];
}
}
return leftIndex; // leftIndex一定指向了最终数组末尾的下一个元素
www.programmercarl.com/0027.%E7%A7…
题目
- 删除有序数组中的重复项leetcode.cn/problems/re…
- 844.比较含退格的字符串 没看懂双指针
- 977.有序数组的平方
双指针法,i指向起始位置,j指向终止位置。
定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置。
如果A[i] * A[i] < A[j] * A[j] 那么result[k--] = A[j] * A[j]; 。
如果A[i] * A[i] >= A[j] * A[j] 那么result[k--] = A[i] * A[i]; 。
滑动窗口
滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。
那么滑动窗口如何用一个for循环来完成这个操作呢。
如果用一个for循环,那么应该表示 滑动窗口的起始位置,还是终止位置。
- 如果只用一个for循环来表示 滑动窗口的起始位置,遍历剩下的终止位置难免再次陷入 暴力解法的怪圈。
- 所以 只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置。
实现滑动窗口,主要确定如下三点:
-
窗口内是什么?
-
如何移动窗口的起始位置?
-
如何移动窗口的结束位置?
题目:长度最小的子数组
在本题中
- 窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。
- 窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
- 窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)。
var minSubArrayLen = function(target, nums) {
let sum = 0,
result = nums.length ,
j = 0,
i = 0,
subL = 0;
for (; j < nums.length; j ++) {
sum += nums[j]
while (sum >= target) {
subL = j - i + 1;
result = result < subL ? result : subL;
sum -= nums[i ++];
}
}
return result
};
题目
/**
* @param {number[]} fruits
* @return {number}
*/
var totalFruit = function(fruits) {
let l = 0;//起始指针
let maxLen = 0;//窗口的最大长度 其中最多包涵两种水果
let n = 0//前一类水果的结束位置
let arr = [fruits[l]]//水果的种类数组
console.log(arr)
for(let r = 0; r < fruits.length; r++){//窗口的右指针不断前进
if(!arr.includes(fruits[r])){//如果窗口中不包含 进窗口的水果
if(arr.length <= 1){//如果只有一种水果
arr[1] = fruits[r]//将这种水果加入arr数组
}else{//如果有两种水果
l = n//更新窗口的左边界
arr[0] = fruits[r-1]//更新arr中水果的种类
arr[1] = fruits[r]
}
}
if(fruits[r] !== fruits[n]){//如果进来了一种新的类型的水果 更新前一种水果的位置
n = r
}
maxLen = Math.max(maxLen,r-l+1)//更新滑动窗口的最大值
}
return maxLen
};
螺旋矩阵leetcode.cn/problems/sp…
给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
坚持循环不变量原则
模拟顺时针画矩阵的过程:
- 填充上行从左到右
- 填充右列从上到下
- 填充下行从右到左
- 填充左列从下到上
由外向内一圈一圈这么画下去
每画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来
math函数库中的一个函数,math.floor (x) 返回小于参数x的最大整数,即对浮点数向下取整。x[]的取值。
Array.fill
arr.fill(value[, start[, end]])
fill() 方法用一个固定值填充一个数组中,
从起始索引到终止索引内的全部元素,
不包括终止索引,
返回被修改后的数组。
value:用来填充数组元素的值。
start:起始索引,默认值为0。
end:终止索引,默认值为 this.length。
/**
* @param {number} n
* @return {number[][]}
*/
var generateMatrix = function(n) {
let loop = Math.floor(n / 2);
let mid = Math.floor(n / 2);
let count = 1
let row = col = 0
let startX = startY = 0
let offset = 1
let res = new Array(n).fill(0).map(() => new Array(n).fill(0));
while(loop --) {
row = startX
col = startY
for(; col < n - offset ; col ++)
res[row][col] = count ++
for (; row < n - offset ; row ++)
res[row][col] = count ++
for (; col > startY; col --)
res[row][col] = count ++
for (; row > startX; row --)
res[row][col] = count ++
startX ++
startY ++
offset += 1
}
if(n % 2 === 1)
res[mid][mid] = count
return res
};