算法总结 -- 数组篇

76 阅读10分钟

数组是一种线性结构,是存放在连续内存空间上的相同类型数据的集合

二分查找

二分查找元素

二分查找基础条件:有序数组

704. 二分查找

解法一:把目标元素target定义在一个双闭区间(左闭右闭)

function search(num: number[], target: number) {
  // 初始化双闭区间
  let i = 0;
  let j = num.length - 1;
  //循环结束条件 i > j
  while (i <= j) {
    // 中点索引,向下取整
    const mid = Math.floor((j - i) / 2) + i;
    if (num[mid] > target) {
      // target 在索引[i,mid-1]区间
      j = mid - 1;
    } else if (num[mid] < target) {
      // target 在索引[mid+1,j]区间
      i = mid + 1;
    } else {
      return mid;
    }
  }
  return -1;
}

解法二:把目标元素taregt定义在(左闭右开)区间

function search(num: number[], target: number) {
  // 初始化左闭右开区间,j就要初始化为num.length,因为num[j]不在查找范围
  let i = 0;
  let j = num.length;
  //循环结束条件 i >= j
  while (i < j) {
    // 中点索引,向下取整
    const mid = Math.floor((j - i) / 2) + i;
    if (num[mid] > target) {
      // target 在索引[i,mid)区间
      j = mid;
    } else if (num[mid] < target) {
      // target 在索引[mid+1,j)区间
      i = mid + 1;
    } else {
      return mid;
    }
  }
  return -1;
}

69. x 的平方根

var mySqrt = function (x) {
  let l = 1;
  let r = x;
  // 除了0外整数x的平方根一定是在1到x的范围内
  while (l <= r) {
    const m = Math.floor((r - l) / 2) + l;
    if (m <= x / m) {
      // x的平方根大于等于m
      if (m + 1 > x / (m + 1)) {
        // x的平方根小于m+1,此时确定m就是x的平方根
        return m;
      }
      // x的平方根大于m+1,在[m+1,r]之间
      l = m + 1;
    } else {
      // x的平方根小于m,在[l,m-1]之间
      r = m - 1;
    }
  }
  return 0;
};

367. 有效的完全平方数

var isPerfectSquare = function (x) {
  let l = 1;
  let r = x;
  // 除了0外整数x的平方根一定是在1到x的范围内
  while (l <= r) {
    const m = Math.floor((r - l) / 2) + l;
    if (m < x / m) {
      // x的平方根大于m,在[m+1,r]之间
      l = m + 1;
    } else if (m > x / m) {
      // x的平方根小于m,在[l,m-1]之间
      r = m - 1;
    } else {
      return true;
    }
  }
  return false;
};

二分查找插入点

35. 搜索插入位置 (无重复元素)

解法一:闭区间

function searchInsert(num: number[], target: number) {
  // 初始化双闭区间
  let i = 0;
  let j = num.length - 1;
  //循环结束条件 i > j
  while (i <= j) {
    // 中点索引,向下取整
    const mid = Math.floor((j - i) / 2) + i;
    if (num[mid] > target) {
      // target 在索引[i,mid-1]区间
      // j向小于等于 target 的元素靠近
      j = mid - 1;
    } else if (num[mid] < target) {
      // target 在索引[mid+1,j]区间
      // i向大于等于 target 的元素靠近
      i = mid + 1;
    } else {
      // 找到target,返回索引
      return mid;
    }
  }
  // 此时i>j,i指向首个大于等于target的元素,j指向首个小于target的元素
  // target 应该插入i或者j+1这个位置
  return i;
}

解法二:左闭右开区间

function searchInsert(num: number[], target: number) {
  // 初始化左闭右开区间
  let i = 0;
  let j = num.length;
  //循环结束条件 i >= j
  while (i < j) {
    // 中点索引,向下取整
    const mid = Math.floor((j - i) / 2) + i;
    if (num[mid] > target) {
      // target 在索引[i,mid)区间
      j = mid;
    } else if (num[mid] < target) {
      // target 在索引[mid+1,j)区间
      i = mid + 1;
    } else {
      // 找到 target ,返回插入点
      return mid;
    }
  }
  // 此时i===j,i指向首个大于等于target的元素,j指向首个小于等于target的元素
  // target 应该插入i或者j这个位置
  return j;
}

有重复元素

把上题改为有重复元素的升序排列数组,插入按顺序的最左边。

function searchLeftInsert(num: number[], target: number) {
  // 初始化双闭区间
  let i = 0;
  let j = num.length - 1;
  // 循环结束条件 i > j
  while (i <= j) {
    // 中点索引,向下取整
    const mid = Math.floor((j - i) / 2) + i;
    if (num[mid] > target) {
      // target 在索引[i,mid-1]区间
      j = mid - 1;
    } else if (num[mid] < target) {
      // target 在索引[mid+1,j]区间
      i = mid + 1;
    } else {
     // 当num[mid]===target时,说明小于target的元素在区间[i,m−1]中,
     // 因此采用j=m−1来缩小区间,从而使指针j向小于target的元素靠近
      j = mid - 1;
    }
  }
  // 循环完成后,i指向最左边的 target,j指向首个小于target的元素
  return i;
}

二分查找边界

34. 在排序数组中查找元素的第一个和最后一个位置

function searchRange (num, target) {
  // 复用上面查找有重复元素的左边界
  const l = searchLeftInsert(num, target);
  
  // l指向最左边的target
  // l===num.length说明target大于num中所有元素
  // num[l]!==target 说明num中没有与target相等的元素
  if (l === num.length || num[l] !== target) return [-1, -1];
  
  //查找target+1的左边界
  const r = getLeftBorder(num, target + 1);
  // target的右边界
  const j = r - 1;
  // j===-1,target小于num中所有元素
  if (j === -1 || num[j] !== target) return [-1, -1];
  
  return [l, j];
};

哈希:空间换时间

1. 两数之和

function twoSum(nums, target) {
  const map = new Map();

  for (let i = 0; i < nums.length; i++) {
    const index = map.get(target - nums[i]);
    // 注意索引0;所以条件要不等于undefined
    if (index !== undefined) {
      return [index, i];
    } else {
      map.set(nums[i], i);
    }
  }
}

1365. 有多少小于当前数字的数字

// 使用哈希
var smallerNumbersThanCurrent = function (nums) {
  //记录数字 nums[i] 有多少个比它小的数字
  const map = new Map();
  const res = [...nums];
  res.sort((a, b) => a - b);

  for (let i = 0; i < res.length; i++) {
    // 遇到相同的不需要处理
    if (!map.has(res[i])) {
      map.set(res[i], i);
    }
  }
  // 此时map里保存小于nums[i]这个数值的个数
  for (let i = 0; i < res.length; i++) {
    res[i] = map.get(nums[i]);
  }
  return res;
};

1207. 独一无二的出现次数

var uniqueOccurrences = function (arr) {
  const map = new Map();

  for (let i = 0; i < arr.length; i++) {
    map.set(arr[i], (map.get(arr[i]) ?? 0) + 1);
  }
  // 去重后还是否数量一致
  return map.size === new Set(map.values()).size;
};

双指针

27. 移除元素

var removeElement = function (nums, val) {
  // 左指针l指向下一个赋值元素
  let l = 0;
  // 右指针r指向当前元素
  for (let r = 0; r < nums.length; r++) {
    // 如果r指向的元素不等于val,将r指向的元素复制到l位置,然后将左右指针同时右移
    if (nums[r] !== val) {
      nums[l] = nums[r];
      l++;
    }
    // 如果等于,l不动,r指向下一位
  }
  return l;
};

26. 删除有序数组中的重复项

var removeDuplicates = function (nums) {
  if (!nums.length) return 0;
  // 慢指针s指向数组第一位
  let s = 0;
  // 快指针f指向数组第二位
  for (let f = 1; f < nums.length; f++) {
    if (nums[s] !== nums[f]) {
      // 快慢指针位置不等时,
      // 慢指针后移一位,快指针的值赋值给慢指针
      nums[++s] = nums[f];
    }
  }
  return s + 1;
};

283. 移动零

var moveZeroes = function (nums) {
  // 慢指针指向要更新的位置
  let s = 0;
  // 快指针寻找不为0的元素
  for (let f = 0; f < nums.length; f++) {
    if (nums[f] !== 0) {
      if (s !== f) {
        [nums[s], nums[f]] = [nums[f], nums[s]];
      }
      // 指向下一个更新位置
      s++;
    }
  }
};

941. 有效的山脉数组

var validMountainArray = function (arr) {
  if (arr.length < 3) {
    return false;
  }
  let l = 0;
  let r = arr.length - 1;
  // 左指针往右移到最高
  while (arr[l] < arr[l + 1]) {
    l++;
  }
  // 右指针往左移到最高
  while (arr[r - 1] > arr[r]) {
    r--;
  }
  // 顶峰相见,并且不是单调,那就是山脉
  if (l === r && l !== 0 && r !== arr.length - 1) {
    return true;
  }
  return false;
};

977. 有序数组的平方

var sortedSquares = function (nums) {
  const res = Array(nums.length).fill(0);
  const n = nums.length;
  // 指针l指向开头
  let l = 0;
  // 指针r指向结尾
  let r = n - 1;
  // 指针k指向res结尾
  let k = n - 1;
  while (l <= r) {
    const left = nums[l] * nums[l];
    const right = nums[r] * nums[r];
    if (left < right) {
      res[k--] = right;
      r--;
    } else {
      res[k--] = left;
      l++;
    }
  }
  return res;
};

844. 比较含退格的字符串

var backspaceCompare = function (s, t) {
  let i = s.length - 1;
  let j = t.length - 1;
  // "#"个数
  let sCount = 0;
  let tCount = 0;
  
  // 比较s和t 用"#"出现的个数,并退格后,其余字符是否相等
  while (i >= 0 || j >= 0) {
    // 循环s
    // i 指向不是"#"且已经退格后的位置
    while (i >= 0) {
      if (s[i] === "#") {
        sCount++;
        i--;
      } else if (sCount > 0) {
        sCount--;
        i--;
      } else {
        break;
      }
    }
    // 循环t
    // j 指向不是"#"且已经退格后的位置
    while (j >= 0) {
      if (t[j] === "#") {
        tCount++;
        j--;
      } else if (tCount > 0) {
        tCount--;
        j--;
      } else {
        break;
      }
    }    
    if (s[i] !== t[j]) {
      return false;
    }
    // 如果s和t循环退格后,s[i]===t[j],继续比较
    i--;
    j--;
  }
  
  return true;
};

滑动窗口(双指针法一种)

209. 长度最小的子数组

var minSubArrayLen = function (target, nums) {
  let minLen = Infinity; // 最小长度
  let l = 0; // 左指针
  let r = 0; // 右指针
  let sum = 0;

  while (r < nums.length) {
    sum += nums[r];
    
    while (sum >= target) {
      minLen = Math.min(minLen, r - l + 1);
      sum -= nums[l];
      // 左指针移动,看是否有更小的 minLen
      l++;
    }
    // 右指针移动
    r++;
  }

  return minLen === Infinity ? 0 : minLen;
};

904. 水果成篮

var totalFruit = function (fruits) {
  // map 存储 fruits 中水果种类和出现次数
  const map = new Map();
  // 左指针
  let l = 0;
  let maxCount = 0;

  // 右指针 r
  for (let r = 0; r < fruits.length; r++) {
    // 存 fruits 中水果种类和出现次数
    map.set(fruits[r], (map.get(fruits[r]) || 0) + 1);
    // 只有2个篮子,当种类超过 2
    while (map.size > 2) {
      // 左指针的水果种类出现次数减 1
      map.set(fruits[l], map.get(fruits[l]) - 1);
      if (map.get[fruits[l]] === 0) {
        // 出现次数为0就删掉
        map.delete(fruits[l]);
      }
      // 移动左指针
      l++;
    }
    maxCount = Math.max(maxCount, r - l + 1);
  }
  return maxCount;
};

76. 最小覆盖子串

var minWindow = function (s, t) {
  //左边界
  let l = 0;
  //右边界
  let r = 0;
  //存储t的每个目标字符所需数
  const need = new Map();
  for (let c of t) {
    need.set(c, (need.get(c) || 0) + 1);
  }
  //目标字符种类数
  let types = need.size;
  //符合的最小子串
  let res = "";

  //右指针向右扩展窗口
  while (r < s.length) {
    const c = s[r];
    if (need.has(c)) {
      // 右指针找到目标字符,需求数减1
      need.set(c, need.get(c) - 1);
      // 目标字符需求数为0 ,就不需要这个字符种类了
      if (need.get(c) === 0) {
        types -= 1;
      }
    }

    // 如果窗口包含所有目标字符,缩小窗口找是否有更小的目标子串
    while (types === 0) {
      const newRes = s.substring(l, r + 1);
      // 更新目标子串
      if (!res || newRes.length < res.length) {
        res = newRes;
      }
      const c = s[l];
      if (need.has(c)) {
        //左指针找到目标字符,但由于缩小窗口,目标字符数增加1
        need.set(c, need.get(c) + 1);
        // 目标字符需求数大于0,目标字符种类数增加1
        if (need.get(c) === 1) {
          types += 1;
        }
      }
      l += 1;
    }
    r += 1;
  }
  return res;
};

其他

189. 轮转数组

var rotate = function (nums, k) {
  function reverse(l, r) {
    while (l < r) {
      [nums[l], nums[r]] = [nums[r], nums[l]];
      l++;
      r--;
    }
  }
  // k如果大于size
  k %= nums.length;
  if (k) {
    reverse(0, nums.length - 1);
    reverse(0, k - 1);
    reverse(k, nums.length - 1);
  }
};

724. 寻找数组的中心下标

var pivotIndex = function (nums) {
  const sum = nums.reduce((a, b) => a + b); 
  // 中心索引左半和 中心索引右半和
  let leftSum = 0;
  let rightSum = 0;

  for (let i = 0; i < nums.length; i++) {
    leftSum += nums[i];
    // leftSum 里面已经有 nums[i]
    rightSum = sum - leftSum + nums[i];
    if (leftSum === rightSum) {
      return i;
    }
  }
  return -1;
};

922. 按奇偶排序数组 II

var sortArrayByParityII = function (nums) {
  const res = [];
  let even = 0;
  let odd = 1;

  for (let i = 0; i < nums.length; i++) {
    if (!(nums[i] % 2)) {
      res[even] = nums[i];
      even += 2;
    } else {
      res[odd] = nums[i];
      odd += 2;
    }
  }
  return res;
};

59. 螺旋矩阵 II

var generateMatrix = function (n) {
  const matrix = Array(n)
    .fill(0)
    .map(() => []);

  let t = (l = 0);
  let b = (r = n - 1);
  let count = 1;

  while (true) {
    //从左到右
    for (let i = l; i <= r; i++) {
      matrix[t][i] = count++;
    }
    if (++t > b) {
      // 第一层遍历完了上边界应自增1后与下边界比较若大于下边界则说明遍历完毕
      break;
    }

    //从上到下
    for (let i = t; i <= b; i++) {
      matrix[i][r] = count++;
    }
    if (--r < l) {
      // 最后一列遍历完了右边界应自减1后与左边界比较若小于左边界则说明遍历完毕
      break;
    }

    //从右到左
    for (let i = r; i >= l; i--) {
      matrix[b][i] = count++;
    }
    if (--b < t) {
      //最后一层遍历完了下边界应自减1后与上边界比较若小于上边界则说明遍历完毕
      break;
    }

    //从下到上
    for (let i = b; i >= t; i--) {
      matrix[i][l] = count++;
    }
    if (++l > r) {
      // 第一行遍历完了左边界应自增1后与右边界比较若大于右边界则说明遍历完毕
      break;
    }
  }

  return matrix;
};

54. 螺旋矩阵

var spiralOrder = function (matrix) {
  if (matrix.length === 0) return [];
  const res = [];
  
  //上边
  let t = 0;
  //右列
  let r = matrix[0].length - 1;
  //下边
  let b = matrix.length - 1;
  //左列
  let l = 0;

  while (t <= b && l <= r) {
    // 从左到右
    for (let i = l; i <= r; i++) {
      res.push(matrix[t][i]);
    }
    t++;
    // 从上到下
    for (let i = t; i <= b; i++) {
      res.push(matrix[i][r]);
    }
    r--;

    // 不是n*n的情况下
    if (t > b || l > r) {
      break;
    }

    // 从右到左
    for (let i = r; i >= l; i--) {
      res.push(matrix[b][i]);
    }
    b--;

    // 从下到上
    for (let i = b; i >= t; i--) {
      res.push(matrix[i][l]);
    }
    l++;
  }

  return res;
};

LCR 146. 螺旋遍历二维数组

var spiralArray = function (matrix) {
  if (matrix.length === 0) return [];
  const res = [];

  //上边
  let t = 0;
  //右列
  let r = matrix[0].length - 1;
  //下边
  let b = matrix.length - 1;
  //左列
  let l = 0;

  while (t <= b && l <= r) {
    // 从左到右
    for (let i = l; i <= r; i++) {
      res.push(matrix[t][i]);
    }
    t++;
    // 从上到下
    for (let i = t; i <= b; i++) {
      res.push(matrix[i][r]);
    }
    r--;

    // 不是n*n的情况下
    if (t > b || l > r) {
      break;
    }

    // 从右到左
    for (let i = r; i >= l; i--) {
      res.push(matrix[b][i]);
    }
    b--;

    // 从下到上
    for (let i = b; i >= t; i--) {
      res.push(matrix[i][l]);
    }
    l++;
  }

  return res;
};

参考

《Hello 算法》

代码随想录