数组
1. 数组理论基础
2. 二分查找
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
// right是数组最后一个数的下标,num[right]在查找范围内,是左闭右闭区间
let left = 0, right = nums.length - 1;
// 当left=right时,由于nums[right]在查找范围内,所以要包括此情况
while (left <= right) {
let mid = parseInt((left + right) / 2);
// 如果中间数大于目标值,要把中间数排除查找范围,所以右边界更新为mid-1;如果右边界更新为mid,那中间数还在下次查找范围内
if (target < nums[mid]) {
right = mid - 1; // 去左面闭区间寻找
} else if ( target > nums[mid]) {
left = mid + 1; // 去右面闭区间寻找
} else {
return mid;
}
}
return -1;
};
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
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;
};
关注点1:循环条件的判断
对于[],[1,1]是有意义的,所以要left <= right,对于[),[1,1)是无意义的,所以要left = right
关注点2:right的指向
因为mid被排除,根据定义,对于[],right = mid - 1,对于[),right = mid;
关注点3:因为js是弱类型,所以计算mid时要parseInt((left + right) / 2)
3. 移除元素
应用场景:
- 给定一个数组和指定的值,移除所有数组中与该值相等的元素。
- 移除数组中的重复元素。
- 移除数组中某个范围内的元素,或者筛选出某个范围内的元素。
- 在数组中移除特定位置的元素,并将其余元素向左或向右移动。
- 通过某种条件筛选出数组中满足条件的元素,并将其移除。
- 移除数组中的空元素或者无效元素,例如null或undefined。
- 将数组中的元素按照某种规则排序,然后移除一定数量的元素。
算法逻辑
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
/**
* @param {number[]} nums
* @param {number} val
* @return {number}
*/
//两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组
var removeElement = function(nums, val) {
for (let i = 0; i < nums.length; i++) {
if (nums[i] === val) {
for (let j = i + 1; j < nums.length; j++) {
nums[j-1] = nums[j];
}
i--;
nums.length--;
}
}
return nums.length;
};
/**
* @param {number[]} nums
* @param {number} val
* @return {number}
*/
//一个指针遍历数组,一个指针存储新数组
var removeElement = function(nums, val) {
let slowIndex = 0;
for (let fastIndex = 0; fastIndex < nums.length; fastIndex++) {
if (nums[fastIndex] !== val) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
};
4. 有序数组的平方
/**
* @param {number[]} nums
* @return {number[]}
*/
//每个数平方之后,排个序
var sortedSquares = function(nums) {
for (let i = 0; i < nums.length; i++) {
nums[i] *= nums[i]
}
nums.sort((value1, value2) => value1 - value2);
return nums;
};
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortedSquares = function(nums) {
let left = 0, right = nums.length - 1, k = nums.length - 1;
let result = new Array(nums.length);
while (left <= right) { // 注意这里要i <= j,因为最后要处理两个元素
if (nums[left] * nums[left] < nums[right] * nums[right]) {
result[k--] = nums[right] * nums[right];
right--;
} else {
result[k--] = nums[left] * nums[left]
left++;
}
}
return result;
};
5. 长度最小的子数组
/**
* @param {number} target
* @param {number[]} nums
* @return {number}
*/
var minSubArrayLen = function(target, nums) {
let result = Infinity;// 最终的结果
let subLength = 0;// 子序列的长度
let sum = 0;// 子序列的数值之和
for (let i = 0; i < nums.length; i++) {// 设置子序列起点为i
sum = 0;
for (let j = i; j < nums.length; j++) {// 设置子序列终止位置为j
sum += nums[j];
console.log(`j${j},sum${sum}\n`)
if (sum >= target) { // 一旦发现子序列和超过了s,更新result
subLength = j - i + 1;// 取子序列的长度
result = subLength < result? subLength : result;
break;// 因为我们是找符合条件最短的子序列,所以一旦符合条件就break
}
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return length < Infinity ? length : 0;
};
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
// 滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果
// 只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置
// 滑动窗口根据当前子序列和大小的情况,不断调节子序列的起始位置
/**
* @param {number} target
* @param {number[]} nums
* @return {number}
*/
var minSubArrayLen = function(target, nums) {
let result = Infinity;// 最终的结果
let subLength = 0;// 子序列的长度
let sum = 0;// 滑动窗口数值之和
let i = 0; // 滑动窗口起始位置
for (let j = 0; j < nums.length; j++) {// 设置子序列起点为i
sum += nums[j];
// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= target) {
subLength = j - i + 1;// 取子序列的长度
result = subLength < result? subLength : result;
sum -= nums[i++];// 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
}
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return length < Infinity ? length : 0;
};
// 时间复杂度:O(n)
// 空间复杂度:O(1)
6. 螺旋矩阵II
应用场景
螺旋矩阵算法主要应用于二维矩阵的遍历,其实现方式是从矩阵的外围向内部逐层遍历。
在实际应用中,螺旋矩阵算法有很多应用场景,比如:
- 图像处理:在图片上按照螺旋矩阵遍历像素点,可以进行特定的图像处理,旋转,缩放,滤镜等等。
- 数据可视化:在数据可视化中,可以将数据按照螺旋矩阵展示,以达到更好的展示效果。
- 游戏开发:在游戏中,可以应用螺旋矩阵算法实现地图的生成以及怪物、道具等随机分布。
- 数据结构算法:在许多算法中,螺旋矩阵算法也有其独特的应用,例如矩阵搜索、旋转图像等。
综上所述,螺旋矩阵算法在实际应用中具有广泛的应用场景。
算法逻辑
/**
* @param {number} n
* @return {number[][]}
*/
var generateMatrix = function(n) {
let startX = startY = 0;// 定义每循环一个圈的起始位置
let offSet = 1;// 需要控制每一条边遍历的长度,每次循环右边界收缩一位
let count = 1;// 用来给矩阵中每一个空格赋值
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 result = new Array(n).fill(0).map(() => new Array(n).fill(0));//定义一个二维数组
let i, j;
while (loop--) {
j = startY;
i = startX;
// 模拟填充上行从左到右(左闭右开)
for (; j < n - offSet; j++) {
result[i][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (; i < n - offSet; i++) {
result[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > startY; j--) {
result[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > startX; i--) {
result[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startX++;
startY++;
// offset 控制每一圈里每一条边遍历的长度
offSet++;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2) {
result[mid][mid] = count;
}
return result;
};
拓展
除了按顺序遍历和按螺旋矩阵算法遍历二维数组的方法,常见的遍历二维数组的方法还有以下几种:
- 按行遍历:按照行的顺序逐个访问二维数组中的每个元素。该方法适用于处理行之间有关联的数据,例如查找某一行中的最大值、最小值等。缺点是可能需要使用外部变量(例如行索引),增加了代码的复杂度。
- 按列遍历:按照列的顺序逐个访问二维数组中的每个元素。该方法适用于处理列之间有关联的数据,例如查找某一列中的最大值、最小值等。缺点同样是可能需要使用外部变量(例如列索引)。
- 按对角线遍历:按照对角线的顺序逐个访问二维数组中的每个元素。该方法适用于处理与对角线有关的数据,例如矩阵的主对角线或副对角线元素之和等。优点是代码简单易懂,缺点是可能需要额外的条件判断,增加了代码的复杂度。
- 按莫顿曲线遍历:按照莫顿曲线的顺序逐个访问二维数组中的每个元素。莫顿曲线就是把二维矩阵按照一定的规则映射成一维数组,然后按照一维数组的顺序遍历。该方法适用于需要利用数据局部性质优化访问效率的场景,例如循环展开、cache友好等。缺点是需要实现莫顿曲线映射逻辑,增加了代码的复杂度。
不同的遍历方法适用于不同的场景,需要根据具体情况选择合适的遍历方法。