Day1-D2-数组-leetcode704,367,69,34,35,27,977,209,
数组
理论
-
数组是存放在连续内存空间上的相同类型数据的集合。
-
数组可以通过下标索引的方式获取到下标对应的数据。
-
数组下标都是从0开始的。
-
数组内存空间的地址是连续的
-
数组的元素是不能删的,只能覆盖。
题目
- 二分查找
-
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。你必须编写一个具有 O(log n) 时间复杂度的算法。
-
思路:
-
区间定义分两种:
左闭右闭[left, right],左闭右开[left, right)
// 左闭右闭[left, right]
var search = function(nums, target) {
// right是数组最后一个数的下标,num[right]在查找范围内,是左闭右闭区间
let mid, left = 0, right = nums.length - 1;
// 当left=right时,由于nums[right]在查找范围内,所以要包括此情况
while (left <= right) {
// 位运算 + 防止大数溢出
// >> 1:是位运算,表示除以2,等价于 Math.floor((right - left) / 2),但效率更高。
// left + ...:保证区间起点是 left,防止直接用 (left + right) / 2 时,left + right 可能溢出。
mid = left + ((right - left) >> 1);
// 如果中间数大于目标值,要把中间数排除查找范围,所以右边界更新为mid-1;如果右边界更新为mid,那中间数还在下次查找范围内
if (nums[mid] > target) {
right = mid - 1; // 去左面闭区间寻找
} else if (nums[mid] < target) {
left = mid + 1; // 去右面闭区间寻找
} else {
return mid;
}
}
return -1;
};
var search = function(nums, target) {
// right是数组最后一个数的下标+1,nums[right]不在查找范围内,是左闭右开区间
let mid, left = 0, right = nums.length;
// 当left=right时,由于nums[right]不在查找范围,所以不必包括此情况
while (left < right) {
// 位运算 + 防止大数溢出
mid = left + ((right - left) >> 1);
// 如果中间值大于目标值,中间值不应在下次查找的范围内,但中间值的前一个值应在;
// 由于right本来就不在查找范围内,所以将右边界更新为中间值,如果更新右边界为mid-1则将中间值的前一个值也踢出了下次寻找范围
if (nums[mid] > target) {
right = mid; // 去左区间寻找
} else if (nums[mid] < target) {
left = mid + 1; // 去右区间寻找
} else {
return mid;
}
}
return -1;
};
- 有效的完全平方数
- 给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
- 完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。
- 不能使用任何内置的库函数,如 sqrt 。
- 思路
/**
* 用二分查找在 [0, num] 区间内找一个整数 mid,使得 mid * mid == num。
* 如果找到了,返回 true;如果查找结束还没找到,返回 false。
* 时间复杂度 O(log n)。
*/
var isPerfectSquare = function(num) {
// 负数和0都不是完全平方数
if (num < 1) return false
let left = 0, mid = 0, right = num
while(left <= right) {
// 取中间数,防止溢出
mid = left + Math.floor((right - left) / 2)
// 计算中间数的平方
let square = mid * mid
// 找到了,num是完全平方数
if (square === num) {
return true
} else if (square < num) {
// 平方小于num,去右半区间
left = mid + 1
} else {
// 平方大于num,去左半区间
right = mid - 1
}
}
// 没找到,num不是完全平方数
return false
};
- x 的平方根
- 给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
- 由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
- 注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
- 思路
/**
* 用二分查找在 [1, x] 区间内找一个整数 mid,使得 mid * mid 最接近但不超过 x。
* 如果找到精确平方根,直接返回。
* 如果没有找到,循环结束后 right 指向的就是小于等于 x 的最大整数平方根。
*/
var mySqrt = function(x) {
if(x < 2) return x; // 0和1的平方根就是它本身
let left = 1, right = x;
while(left <= right) {
let mid = left + Math.floor((right - left) / 2); // 防止溢出
const square = mid * mid;
if (square === x) {
return mid; // 找到精确平方根
} else if (square < x) {
left = mid + 1; // 平方小于x,去右半区间
} else {
right = mid - 1; // 平方大于x,去左半区间
}
}
return right; // 返回整数部分的平方根
};
- 在排序数组中查找元素的第一个和最后一个位置
- 给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
- 如果数组中不存在目标值 target,返回 [-1, -1]。
- 你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
-
思路
-
二分查找只能找到一个 target 的下标(可能是中间的某一个),但题目要求返回 target 在数组中第一个和最后一个出现的位置。需要二分查找找到 target 的左边界和右边界。
-
情况一:target 在数组范围的右边或者左边,例如数组[3, 4, 5],target为2或者数组[3, 4, 5],target为6,此时应该返回[-1, -1]
-
情况二:target 在数组范围中,且数组中不存在target,例如数组[3,6,7],target为5,此时应该返回[-1, -1]
-
情况三:target 在数组范围中,且数组中存在target,例如数组[3,6,7],target为6,此时应该返回[1, 1]
/**
* 该方法通过两次二分查找,分别找到 target 的左边界和右边界,然后判断区间是否合法。
返回 [起始位置, 结束位置],如果不存在则返回 [-1, -1]。
1. getLeftBorder(nums, target)
查找目标值左边界的辅助函数。
目标:找到小于 target 的最后一个位置(即 target 左边的下标)。
如果 nums[middle] >= target,说明目标在左边,right 往左缩,并记录 leftBorder = right。
否则,left 右移。
2. getRightBorder(nums, target)
查找目标值右边界的辅助函数。
目标:找到大于 target 的第一个位置(即 target 右边的下标)。
如果 nums[middle] > target,right 左移。
否则,left 右移,并记录 rightBorder = left。
3. 主函数逻辑
先分别获取左右边界。
如果有一边没找到(-2),说明数组里没有 target,返回 [-1, -1]。
如果 rightBorder - leftBorder > 1,说明 target 存在,返回 [leftBorder + 1, rightBorder - 1]。
其他情况返回 [-1, -1]。
*/
var searchRange = function(nums, target) {
const getLeftBorder = (nums, target) => {
let left = 0, right = nums.length - 1;
let leftBorder = -2;// 记录一下leftBorder没有被赋值的情况
while(left <= right){
let middle = left + ((right - left) >> 1);
if(nums[middle] >= target){ // 寻找左边界,nums[middle] == target的时候更新right
right = middle - 1;
leftBorder = right;
} else {
left = middle + 1;
}
}
return leftBorder;
}
const getRightBorder = (nums, target) => {
let left = 0, right = nums.length - 1;
let rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
while (left <= right) {
let middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle - 1;
} else { // 寻找右边界,nums[middle] == target的时候更新left
left = middle + 1;
rightBorder = left;
}
}
return rightBorder;
}
let leftBorder = getLeftBorder(nums, target);
let rightBorder = getRightBorder(nums, target);
// 情况一
if(leftBorder === -2 || rightBorder === -2) return [-1,-1];
// 情况三
if (rightBorder - leftBorder > 1) return [leftBorder + 1, rightBorder - 1];
// 情况二
return [-1, -1];
};
- 搜索插入位置
- 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。请必须使用时间复杂度为 O(log n) 的算法。
- 思路
- 目标值在数组所有元素之前
- 目标值等于数组中某一个元素
- 目标值插入数组中的位置
- 目标值在数组所有元素之后
/**
使用二分查找,查找目标值 target 在有序数组 nums 中的位置。
如果找到目标值,直接返回其下标。
如果找不到,最终 right + 1 就是 target 应该插入的位置(因为区间是左闭右闭)。
时间复杂度 O(log n)
*/
function searchInsert(nums, target) {
let left = 0;
let right = nums.length - 1; // 定义target在左闭右闭区间 [left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效
let middle = left + ((right - left) >> 1); // 防止溢出,等同于Math.floor((left + right)/2)
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else {
return middle; // 找到目标值,直接返回下标
}
}
/**
1. 目标值在数组所有元素之前 [0, -1],比如 nums = [3,4,5], target = 1此时循环结束后 left = 0, right = -1,返回 right + 1 = 0,说明应该插入到最前面。
2. 目标值等于数组中某一个元素 return middle;找到后直接 return middle。
3. 目标值插入数组中的位置 [left, right],return right + 1;比如 nums = [1,3,5,6], target = 4,循环结束后 left = 2, right = 1,返回 right + 1 = 2,说明应该插入到下标2的位置。
4. 目标值在数组所有元素之后的情况 [left, right], 因为是右闭区间,所以 return right + 1,比如 nums = [1,3,5], target = 7,循环结束后 left = 3, right = 2,返回 right + 1 = 3,说明应该插入到最后。
5. 没找到目标值,left 是插入位置,由于区间是左闭右闭,最终 left 会停在插入位置,right + 1 也等于 left。
*/
return right + 1;
}
/**
1. ans 初始化为 nums.length
如果 target 比所有元素都大,最终插入位置就是数组末尾(即 nums.length)。
2. 二分查找过程
如果 target > nums[mid],说明目标在右边,l = mid + 1。
否则(target <= nums[mid]),说明目标在左边或当前位置,记录当前 mid 为可能的插入位置,并继续向左查找(r = mid - 1)。
3. 返回 ans
最终 ans 就是 target 应该插入的位置(即第一个大于等于 target 的下标)。
*/
var searchInsert = function (nums, target) {
let l = 0, r = nums.length - 1, ans = nums.length;
while (l <= r) {
const mid = l + Math.floor((r - l) >> 1);
if (target > nums[mid]) {
l = mid + 1;
} else {
ans = mid; // 记录当前可能的插入位置
r = mid - 1; // 继续向左查找
}
}
return ans;
};
- 移除元素
-
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。
-
假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:
-
更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
-
返回 k。
-
思路:
-
要知道数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。
-
方法1:暴力解法,两层循环,一个for循环遍历数组元素,第二个for循环更新数组,发现要移除的元素,就将数组整体向前移动一位
-
方法2:双指针法(快慢指针法),通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
- 定义快慢指针
- 快指针:寻找新数组的元素,新数组就是不含有目标元素的数组
- 慢指针:指向更新 新数组下标的位置
// 两层循环
var removeElement = function (nums, val) {
let n = nums.length
for(let i = 0; i< n; i++) {
if (nums[i] === val) {
// 从i位置开始,后面所有元素都向前移动一位
for (let j = i + 1; j < n; j++) {
nums[j - 1] = nums[j]
}
i--
n--
}
}
return n
}
// 快指针遍历所有元素,慢指针只记录不等于 val 的元素。
// 每遇到一个不等于 val 的元素,就把它放到慢指针的位置,并让慢指针后移。
// 最终,前 slow 个元素就是不等于 val 的元素,返回 slow 即为新数组长度。
var removeElement2 = function(nums, val) {
let slow = 0; // 慢指针,指向下一个要保留元素的位置
for (let fast = 0; fast < nums.length; fast++) { // 快指针,遍历所有元素
if (nums[fast] !== val) { // 如果当前元素不是要移除的值
nums[slow] = nums[fast]; // 把当前元素赋值到慢指针位置
slow++; // 慢指针后移
}
}
return slow; // 返回新数组的长度(不含val的元素个数)
}
- 有序数组的平方
- 给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
- 思路
- 暴力解法:数组每一项先平方,然后再重新排序
- 双指针法:定义一个新数组用于存放结果,i指向原数组的开始位置,j指向数组的结尾位置,k指向新数组的结尾位置,比较首尾位置的数值的平方,大的值赋值给新数组结尾数据,指针对应移动,继续寻找第二大的数据
- 由于数组 nums 是非递减排序的(可能包含负数),平方后的最大值要么在最左边(负数平方后可能很大),要么在最右边(正数平方后较大)。因此可以采用双指针法:
- 1.初始化:
- i 指向数组开头(左指针),j 指向数组末尾(右指针),k 指向结果数组的末尾(用于填充最大值)。
- 创建一个和 nums 长度相同的结果数组 res,初始填充 0。
- 2.比较平方值:
- 计算 nums[i] 的平方 left 和 nums[j] 的平方 right。
- 如果 left < right,说明 right 更大,将其放入 res[k],并移动右指针 j--。
- 否则,说明 left 更大或相等,将其放入 res[k],并移动左指针 i++。
- 每次操作后,k--(向前填充结果数组)。
- 3.终止条件:当 i > j 时,说明所有元素都已处理,返回 res。
// 暴力解法
var sortedSquares = function(nums) {
for (let i = 0; i < nums.length; i++) {
nums[i] = nums[i] * nums[i]
}
nums.sort((a, b) => a - b)
return nums
};
/**
1. 初始化:
res 是结果数组,长度和 nums 相同,初始值为 0。
i 是左指针(初始 0),j 是右指针(初始 n-1),k 是结果数组的填充指针(初始 n-1)。
2. 双指针遍历:
计算 nums[i] 和 nums[j] 的平方 left 和 right。
如果 left < right,说明右边的平方更大,将 right 存入 res[k],并移动右指针 j--。
否则,说明左边的平方更大或相等,将 left 存入 res[k],并移动左指针 i++。
每次操作后,k--(从后往前填充结果数组)。
3. 返回结果:当 i > j 时,所有元素处理完毕,返回 res。
*/
var sortedSquares = function(nums) {
let n = nums.length;
let res = new Array(n).fill(0); // 初始化结果数组
let i = 0, j = n - 1, k = n - 1; // i: 左指针,j: 右指针,k: 结果数组指针
while (i <= j) {
let left = nums[i] * nums[i], // 左指针元素的平方
right = nums[j] * nums[j]; // 右指针元素的平方
if (left < right) {
res[k--] = right; // 右指针平方更大,存入结果数组
j--; // 移动右指针
} else {
res[k--] = left; // 左指针平方更大或相等,存入结果数组
i++; // 移动左指针
}
}
return res;
};
- 长度最小的子数组
- 给定一个含有 n 个正整数的数组和一个正整数 target 。
- 找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
-
思路
-
暴力解法
-
1.初始化变量:
- result 初始化为 Infinity,表示最短子数组长度,初始为一个极大值。
- sum 用于计算子数组的和。
- subLength 记录当前子数组的长度。
-
2.双重循环:
- 外层循环(i):遍历数组,作为子数组的起始位置。
- 内层循环(j):从 i 开始,作为子数组的结束位置,累加子数组的和 sum。
- 如果 sum >= s,计算当前子数组长度 subLength,并更新 result 为更小的值,然后跳出内层循环(因为要找最短的子数组)。
-
3.返回结果:
- 如果 result 仍然是 Infinity,说明没有满足条件的子数组,返回 0。
- 否则返回 result。
function minSubArrayLen(s, nums) {
let result = Infinity; // 初始化为无穷大
let sum = 0; // 子数组的和
let subLength = 0; // 子数组的长度
for (let i = 0; i < nums.length; i++) { // 子数组的起始位置
sum = 0; // 每次更换起始位置时重置和
for (let j = i; j < nums.length; j++) { // 子数组的结束位置
sum += nums[j]; // 累加子数组的和
if (sum >= s) { // 如果和 ≥ s
subLength = j - i + 1; // 计算子数组长度
result = Math.min(result, subLength); // 更新最短长度
break; // 找到后立即跳出内层循环
}
}
}
return result === Infinity ? 0 : result; // 如果没有找到,返回 0
}
console.log(minSubArrayLen(7, [2, 3, 1, 2, 4, 3])); // 输出 2(子数组 [4, 3])
console.log(minSubArrayLen(4, [1, 4, 4])); // 输出 1(子数组 [4])
console.log(minSubArrayLen(11, [1, 1, 1, 1, 1, 1, 1, 1])); // 输出 0(没有满足条件的子数组)
- 双指针解法
- 滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出结果。
/**
1. 扩展窗口(end++)
每次迭代将 nums[end] 加入 sum,并移动右指针 end,直到 sum >= target。
2. 收缩窗口(start++)
当 sum >= target 时,记录当前窗口长度 end - start + 1,并尝试缩小窗口(左指针右移)以找到更短的满足条件的子数组。
*/
var minSubArrayLen = function(target, nums) {
let start, end
start = end = 0 // 窗口的左右指针,初始都指向数组开头
let sum = 0 // 当前窗口内元素的和
let len = nums.length // 数组长度
// 初始化最短子数组长度为无穷大(后续取最小值)
let ans = Infinity
// 滑动窗口逻辑
while(end < len){ // 外层循环:扩展窗口右边界
sum += nums[end]; // 将 nums[end] 加入当前窗口的和
// 内层循环:收缩窗口左边界(当 sum >= target 时)
while (sum >= target) {
ans = Math.min(ans, end - start + 1); // 更新最短长度
sum -= nums[start]; // 从窗口中移除 nums[start]
start++; // 左指针右移
}
end++; // 右指针右移,扩展窗口
}
// 如果 ans 未被更新(即没有满足条件的子数组),返回 0。否则返回最短子数组长度 ans。
return ans === Infinity ? 0 : ans
};
/**
1. 初始化变量:
result 初始化为 Infinity,表示最短子数组长度,初始为一个极大值。
sum 用于计算滑动窗口的和。
i 是滑动窗口的左边界(起始位置)。
subLength 记录当前子数组的长度。
2. 滑动窗口逻辑:
外层循环(j):遍历数组,作为滑动窗口的右边界。
内层循环(while):当 sum >= s 时,计算当前子数组长度 subLength,并更新 result 为更小的值,然后收缩窗口左边界(i++)。
3. 返回结果:
如果 result 仍然是 Infinity,说明没有满足条件的子数组,返回 0。
否则返回 result。
*/
function minSubArrayLen(s, nums) {
let result = Infinity; // 初始化为无穷大,相当于 C++ 的 INT32_MAX
let sum = 0; // 滑动窗口数值之和
let i = 0; // 滑动窗口起始位置
let subLength = 0; // 滑动窗口的长度
for (let j = 0; j < nums.length; j++) {
sum += nums[j];
// 使用 while 循环,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = j - i + 1; // 取子序列的长度
result = Math.min(result, subLength); // 更新最短长度
sum -= nums[i++]; // 滑动窗口的精髓:不断变更 i(子序列的起始位置)
}
}
// 如果 result 没有被赋值的话,就返回 0,说明没有符合条件的子序列
return result === Infinity ? 0 : result;
}
console.log(minSubArrayLen(7, [2, 3, 1, 2, 4, 3])); // 输出 2(子数组 [4, 3])
console.log(minSubArrayLen(4, [1, 4, 4])); // 输出 1(子数组 [4])
console.log(minSubArrayLen(11, [1, 1, 1, 1, 1, 1, 1, 1])); // 输出 0(没有满足条件的子数组)
- 螺旋矩阵 II
- 给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
- 思路
- 坚持循环不变量原则
var generateMatrix = function(n) {
let startX = startY = 0; // 定义每循环一个圈的起始位置
let loop = Math.floor(n/2); // 旋转圈数,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
let mid = Math.floor(n/2); // 中间位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
let offset = 1; // 控制每一层填充元素个数
let count = 1; // 更新填充数字,用来给矩阵中每一个空格赋值
let res = new Array(n).fill(0).map(() => new Array(n).fill(0));
while (loop--) {
let 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++;
}
// 更新起始位置
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startX++;
startY++;
// 更新offset,offset 控制每一圈里每一条边遍历的长度
offset += 1;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2 === 1) {
res[mid][mid] = count;
}
return res;
};