代码随想录——数组

127 阅读6分钟

数组

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. 移除元素

应用场景:

  1. 给定一个数组和指定的值,移除所有数组中与该值相等的元素。
  2. 移除数组中的重复元素。
  3. 移除数组中某个范围内的元素,或者筛选出某个范围内的元素。
  4. 在数组中移除特定位置的元素,并将其余元素向左或向右移动。
  5. 通过某种条件筛选出数组中满足条件的元素,并将其移除。
  6. 移除数组中的空元素或者无效元素,例如null或undefined。
  7. 将数组中的元素按照某种规则排序,然后移除一定数量的元素。

算法逻辑

// 时间复杂度: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;
};

27.移除元素-双指针法.gif

/**
 * @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)

209.长度最小的子数组.gif

6. 螺旋矩阵II

应用场景
螺旋矩阵算法主要应用于二维矩阵的遍历,其实现方式是从矩阵的外围向内部逐层遍历。
在实际应用中,螺旋矩阵算法有很多应用场景,比如:

  1. 图像处理:在图片上按照螺旋矩阵遍历像素点,可以进行特定的图像处理,旋转,缩放,滤镜等等。
  2. 数据可视化:在数据可视化中,可以将数据按照螺旋矩阵展示,以达到更好的展示效果。
  3. 游戏开发:在游戏中,可以应用螺旋矩阵算法实现地图的生成以及怪物、道具等随机分布。
  4. 数据结构算法:在许多算法中,螺旋矩阵算法也有其独特的应用,例如矩阵搜索、旋转图像等。

综上所述,螺旋矩阵算法在实际应用中具有广泛的应用场景。
算法逻辑

/**
 * @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;
};

拓展
除了按顺序遍历和按螺旋矩阵算法遍历二维数组的方法,常见的遍历二维数组的方法还有以下几种:

  1. 按行遍历:按照行的顺序逐个访问二维数组中的每个元素。该方法适用于处理行之间有关联的数据,例如查找某一行中的最大值、最小值等。缺点是可能需要使用外部变量(例如行索引),增加了代码的复杂度。
  2. 按列遍历:按照列的顺序逐个访问二维数组中的每个元素。该方法适用于处理列之间有关联的数据,例如查找某一列中的最大值、最小值等。缺点同样是可能需要使用外部变量(例如列索引)。
  3. 按对角线遍历:按照对角线的顺序逐个访问二维数组中的每个元素。该方法适用于处理与对角线有关的数据,例如矩阵的主对角线或副对角线元素之和等。优点是代码简单易懂,缺点是可能需要额外的条件判断,增加了代码的复杂度。
  4. 按莫顿曲线遍历:按照莫顿曲线的顺序逐个访问二维数组中的每个元素。莫顿曲线就是把二维矩阵按照一定的规则映射成一维数组,然后按照一维数组的顺序遍历。该方法适用于需要利用数据局部性质优化访问效率的场景,例如循环展开、cache友好等。缺点是需要实现莫顿曲线映射逻辑,增加了代码的复杂度。

不同的遍历方法适用于不同的场景,需要根据具体情况选择合适的遍历方法。

7. 总结篇