# 算法与数据结构

# 整数

# 整数除法

题目: 给定两个整数 ab ,求它们的除法的商 a/b ,要求不得使用乘号 '*'、除号 '/' 以及求余符号 '%'

注意: 整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2 假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−2^31, 2^31−1]。本题中,如果除法结果溢出,则返回 2^31 − 1

const divide = (a, b) => {
  // 当被除数为- 2^31,除数为 -1 时   才会除法溢出
  if (a === -Math.pow(2, 31) && b === -1) { return Math.pow(2, 31) - 1 }

  let p = 0 // 0-正,1-负
  if (a > 0 && b > 0) { p = 0 }
  else if (a < 0 && b < 0) { p = 0 }
  else { p = 1 }

  // 将两个数都变为正数
  if (a < 0) { a = -a }
  if (b < 0) { b = -b }

  let res = 0
  // 用减法来代替除法,如15/2就是循环15减去7个2
  while (a >= b) {
    a -= b
    res++
  }

  return p ? -res : res
}

# 二进制加法

题目: 给定两个 01 字符串 ab ,请计算它们的和,并以二进制字符串的形式输出。输入为 非空 字符串且只包含数字 10

解法一:
/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
const addBinary = (a, b) => {
  // 0b 表示二进制,0o表示八进制,0x表示十六进制
  return (BigInt('0b' + a) + BigInt('0b' + b)).toString(2);
}

解法二:
/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
const addBinary = (a, b) => {
  // 补齐两个字符串的长度
  const len = Math.max(a.length, b.length)
  if (a.length < len) {
    while (a.length < len) {
      a = '0' + a
    }
  }
  if (b.length < len) {
    while (b.length < len) {
      b = '0' + b
    }
  }
  // 定义一个变量来存储下次运算是否需要+1
  let isPlusOne = 0, ans = ''
  for (let i = len - 1; i >= 0; i--) {
    let tempA = a[i]
    let tempB = b[i]
    let sum = Number(tempA) + Number(tempB) + isPlusOne
    isPlusOne = sum > 1 ? 1 : 0
    ans = sum % 2 + ans 
  }
  if (isPlusOne) { ans = isPlusOne + ans }
  return ans
}

# 比特位计数

题目: 给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。

示例: 输入5,输出[0,1,1,2,1,2]

解法一:
/**
 * @param {number} n
 * @return {number[]}
 */
const countBits = (n) => {
  const bits = [0]
  for (let i = 1; i <= n; i++) {
    bits[i] = i.toString(2).split('').reduce((pre, cur) => {
      return cur === '1' ? parseInt(pre) + 1 : parseInt(pre)
    }, 0)
  }
  return bits
}

解法二:
位运算+动态规划
&    与      两个位都为1时,结果才为1
|    或      两个位都为0时,结果才为0
^    异或    两个位相同为0,相异为1
~    取反    0110
<<   左移    各二进位全部左移若干位,高位丢弃,低位补0
const countBits = (n) => {
  const dp = [0]
  for (let i = 1; i <= n; i++) {
    // 1、对于任一数 i,它二进制数中 1 的个数 = i 右移一位 的 1 的个数 + i 末位 1 的个数
    // 2、判断奇偶用且运算:如果 i 是偶数,i & 1 === 0;如果是奇数,i & 1 === 1;
    dp[i] = dp[i >> 1] + (i & 1)
  }
  return dp
}

# 只出现一次的数字 (opens new window)

题目: 给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 **三次 。**请你找出并返回那个只出现了一次的元素。

示例: nums = [2,2,3,2]

/**
 * @param {number[]} nums
 * @return {number}
 */
const singleNumber = function(nums) {
  const map = new Map()
  for (const num of nums) {
    map.set(num, (map.get(num) || 0) + 1)
  }
  for (const [key, val] of map) {
    if (val === 1) {return key}
  }
};

# 最大单词长度乘积 (opens new window)

题目: 给定一个字符串数组 words,找到 length(word[i]) * length(word[j]) 的最大值,并且这两个单词不含有公共字母。你可以认为每个单词只包含小写字母。如果不存在这样的两个单词,返回 0。

示例1:

输入: ["abcw","baz","foo","bar","xtfn","abcdef"]
输出: 16 
解释: 这两个单词为 "abcw", "xtfn"。

示例2:

输入: ["a","aa","aaa","aaaa"]
输出: 0 
解释: 不存在这样的两个单词。
// 方法一:
const maxProduct = function(words) {
  const bits = [], len = words.length
  // 预排序,字符串长的排在前面
  words.sort((a, b) => b.length - a.length)
  for (let i = 0; i < words.length; i++) {
    let word = words[i]
    // 生成掩码,出现 a 对应掩码第一位为 1,以此类推
    for (const letter of word) {
      bits[i] |= 1 << (letter.charCodeAt(0) - 97)
    }
  }
  let ans = 0;
  for(let i=0; i<len-1; i++){
    // 如果循环一开始的两字符串乘积小于 ans 则后面不可能出现更大的乘积了
    if(words[i].length * words[i+1].length <= ans) break;
    for(let j=i+1; j<len; j++){
      if(words[i].length * words[j].length <= ans) break;
      // 通过与运算判断来两个字符串是否包含相同字母,与运算为 true 代表具有相同字母,因此这里取非
      if(!(bits[i] & bits[j])) {
        ans = words[i].length * words[j].length;
        break;
      }
    }
  }
  return ans
};


// 方法二:
const maxProduct = (words) => {
  // 构建字符二位数组
  const stringArray = new Array(words.length).fill(0).map(x => new Array(26).fill(0))
  // 记录每个字符在二维数组中的位置
  for (let i = 0; i < words.length; i++) {
    for (const char of words[i]) {
      stringArray[i][char.charCodeAt(0) - 'a'.charCodeAt(0)] = true
    }
  }
  let ans = 0
  // 对words里面的字符串两两比较
  for (let i = 0; i < words.length; i++) {
    for (let j = i + 1; j < words.length; j++) {
      let k = 0 // k单独定义是因为要判断没有重复的情况
      for (; k < 26; k++) {
        if (stringArray[i][k] && stringArray[j][k]) { break }
      }
      // 如果k=26就说明,没有字符是重复的
      if (k === 26) { ans = Math.max(ans, words[i].length * words[j].length) }
    }
  }
  return ans
}

# 排序数组中两个数字之和

题目: 给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。

函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 0 开始计数 ,所以答案数组应当满足 0 <= answer[0] < answer[1] < numbers.length 。

假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。

示例:

输入:numbers = [1,2,4,6,10], target = 8
输出:[1,3]
解释:2 与 6 之和等于目标数 8 。因此 index1 = 1, index2 = 3

输入:numbers = [2,3,4], target = 6
输出:[0,2]

输入:numbers = [-1,0], target = -1
输出:[0,1]

解法: 双指针

/**
 * @param {number[]} numbers
 * @param {number} target
 * @return {number[]}
 */
const twoSum = (numbers, target) => {
  let [left, right] = [0, numbers.length - 1]
  while (left < right) {
    if (numbers[left] + numbers[right] > target) { right-- }
    else if (numbers[left] + numbers[right] < target) { left++ }
    else {
      return [left, right]
    }
  }
}

# 数组

# 三数之和 (opens new window)

题目: 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。答案中不可以包含重复的三元组。

示例:

示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

示例 2:
输入:nums = []
输出:[]

示例 3:
输入:nums = [0]
输出:[]

解法:

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
const threeSum = (nums) => {
  if (nums.length >= 3) {
    nums = nums.sort((a, b) => a - b) // 先对数组排序
    let [len, res] = [nums.length, []]
    for (let i = 0; i < len; i++) {
      if (nums[i] > 0) { break }  // 如果第一个数大于0,那么三个数之和肯定大于0
      if (i > 0 && nums[i] === nums[i - 1]) { continue }  // 去重
      let leftIndex = i + 1, rightIndex = len - 1
      while (leftIndex < rightIndex) {
        const sum = nums[leftIndex] + nums[i] + nums[rightIndex]
        if (sum > 0) {
          rightIndex--
        } else if (sum < 0) {
          leftIndex++
        } else {
          res.push([nums[i], nums[leftIndex], nums[rightIndex]])
          while (leftIndex<rightIndex && nums[leftIndex] === nums[leftIndex + 1]) { leftIndex++ } // 去重
          while (leftIndex<rightIndex && nums[rightIndex] === nums[rightIndex + 1]) { rightIndex++ }  // 去重
          leftIndex++
          rightIndex--
        }
      }
    }
    return res
  } else {
    return []
  }
}

# 长度最小的子数组 (opens new window)

题目: 给定一个含有 n 个正整数的数组和一个正整数 target 。找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例:

示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:
输入:target = 4, nums = [1,4,4]
输出:1

示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

**解法:**滑动窗口

minSubArrayLen

示例:

/**
 * @description 长度最小的子数组
 * @param {number} target
 * @param {number[]} nums
 * @return {number}
 */
const minSubArrayLen = (target, nums) => {
  const len = nums.length
  let [ans, L, R, sum] = [len + 1, 0, 0, 0]
  while (R < len) {
    sum += nums[R]
    while (sum >= target) {
      ans = Math.min(ans, R - L + 1)
      sum -= nums[L]
      L++
    }
    R++
  }
  return ans > len ? 0 : ans
}

# 乘积小于 K 的子数组 (opens new window)

题目: 给定一个正整数数组 nums和整数 k 。请找出该数组内乘积小于 k 的连续的子数组的个数。

示例:

示例 1:
输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8个乘积小于100的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。


示例 2:
输入: nums = [1,2,3], k = 0
输出: 0

**解法: ** 滑动窗口


/**
 * @description 乘积小于K的子数组
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
const numSubarrayProductLessThanK = (nums, k) => {
  if (k <= 1) { return 0 }
  const len = nums.length
  let [R, L, ans, accumulate] = [0, 0, 0, 1]
  while (R < len) {
    accumulate *= nums[R]
    while (accumulate >= k) {
      accumulate /= nums[L]
      L++
    }
    ans += R - L + 1
    R++
  }
  return ans
}

# 和为K的子数组 (opens new window)

**题目:**给定一个整数数组和一个整数 **k,**你需要找到该数组中和为 k 的连续的子数组的个数。

示例:

示例 1:
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。

题解:

const subarraySum = (nums, k) => {
  const mp = new Map();
  mp.set(0, 1);
  let count = 0, pre = 0;
  for (const x of nums) {
    pre += x;
    if (mp.has(pre - k)) { count += mp.get(pre - k) }
    if (mp.has(pre)) { mp.set(pre, mp.get(pre) + 1) }
    else { mp.set(pre, 1) }
  }
  return count;
}

# 树形结构扁平化

题目:将一个树形的结构转换成数组。

示例

const tree = [
  {
    name: '小明',
    id: 1,
    pid: 0,
    children: [
      {
        name: '小花',
        id: 11,
        pid: 1,
        children: [
          {
            name: '小华',
            id: 111,
            pid: 11,
          },
          {
            name: '小李',
            id: 112,
            pid: 11,
          }
        ]
      },
      {
        name: '小红',
        id: 12,
        pid: 1,
      }
    ]
  },
  {
    name: '小王',
    id: 2,
    pid: 0,
    children: [
      {
        name: '小林',
        id: 21,
        pid: 2,
      },
      {
        name: '小李',
        id: 22,
        pid: 2,
      }
    ]
  }
]

题解

// 广度优先遍历
const treeToArray = (tree) => {
  const queue = tree, ans = []
  while (queue.length) {
    const currentNode = queue.shift()
    ans.push({
      name: currentNode.name,
      pid: currentNode.pid,
      id: currentNode.id
    })
    if (currentNode.children) {
      for (const child of currentNode.children) {
        queue.push(child)
      }
    }
  }
  return ans
}

# 数组转树形结构

// 递归
const arrayToTree = (arr, pid) => {
  let res = []
  arr.forEach(item => {
    if (item.pid === pid) {
      let itemChildren = arrayToTree(arr, item.id)
      if (itemChildren.length) {
        item.children = itemChildren
      }
      res.push(item)
    }
  })
  return res
}

# 字符串

# 含有所有字符的最短字符串 (opens new window)

题目: 给定两个字符串 s 和 t 。返回 s 中包含 t 的所有字符的最短子字符串。如果 s 中不存在符合条件的子字符串,则返回空字符串 "" 。如果 s 中存在多个符合条件的子字符串,返回任意一个。

示例:

示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC" 
解释:最短子字符串 "BANC" 包含了字符串 t 的所有字符 'A'、'B'、'C'

示例 2:
输入:s = "a", t = "a"
输出:"a"

示例 3:
输入:s = "a", t = "aa"
输出:""
解释:t 中两个字符 'a' 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。

题解:哈希表+双指针维护滑动窗口

/**
 * @param {string} s
 * @param {string} t
 * @return {string}
 */
const minWindow = function(s, t) {
  let [l, r, ans, m] = [0, 0, '', new Map()]
  for (const k of t) {
    m.set(k, m.has(k) ? m.get(k) + 1 : 1)
  }
  let needType = m.size;
  while (r < s.length) {
    // 获取当前字符
    const c = s[r];
    // 如果是需要匹配的字符
    if (m.has(c)) {
      // 更新字典表中的次数 - 1
      m.set(c, m.get(c) - 1);
      //  如果次数为0,证明这个字符种类在当前窗口已经集齐了,needType - 1
      if (m.get(c) === 0) needType -= 1;
    }
    // 当needType为0,证明所有需要匹配的字符都已经在当前滑动窗口中
    while (needType === 0) {
      const c2 = s[l];
      // 记录当前滑动窗口的字符
      let newRes = s.slice(l, r + 1);
      // 当新的窗口中的字符长度小于上次的字符长度时,更新结果
      // !res 是在结果值为空的时候需要更新一下第一次匹配的值
      if (!ans || newRes.length < ans.length) ans = newRes;
      // 如果左指针移动过程中出现,字典中的值证明需要匹配的字符已经脱离了当前窗口
      if (m.has(c2)) {
        // 更新表中需要出现的次数
        m.set(c2, m.get(c2) + 1);
        // 更新needType
        if (m.get(c2) === 1) needType += 1;
      }
      // 移动左指针
      l++;
    }
    // 移动右指针
    r++;
  }
  return ans
}

# 有效的回文 (opens new window)

题目: 给定一个字符串 s ,验证 s 是否是 回文串 ,只考虑字母和数字字符,可以忽略字母的大小写。本题中,将空字符串定义为有效的 回文串 。

示例 1:
输入: s = "A man, a plan, a canal: Panama"
输出: true
解释:"amanaplanacanalpanama" 是回文串

示例 2:
输入: s = "race a car"
输出: false
解释:"raceacar" 不是回文串

题解: 正则表达+双指针

/**
 * @param {string} s
 * @return {boolean}
 */
const isPalindrome = (s) => {
s = s.toLocaleUpperCase().match(/[0-9A-Za-z]+/g)
  if (!s) return true
  s = s.join('')
  let l = 0, r = s.length - 1
  while (l < r) {
    if (s[l] !== s[r]) { return false }
    l++
    r--
  }
  return true
}

# 无重复字符的最长子串

题目:给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

思想:滑动窗口

题解

const lengthOfLongestSubstring = (s) => {
  let l = 0, maxLength = 0, set = new Set()
  for (let r = 0; r < s.length; r++) {
    if (!set.has(s[r])) {
      set.add(s[r])
      maxLength = Math.max(maxLength, set.size)
    } else {
      while (set.has(s[r])) {
        set.delete(s[l])
        l++
      }
      set.add(s[r])
    }
  }
  return maxLength
}

# 排序

image-20210926193142691

# 冒泡排序

bubbleSort

function bubbleSort(arr) {
  let len = arr.length;
  for (let i = 0; i < len; i++) {
    for (let j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j+1]) {        //相邻元素两两对比
        [arr[j],arr[j+1]] = [arr[j+1],arr[j]]  //通过解构完成元素交换
      }
    }
  }
  return arr;
}

let arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bubbleSort(arr));

# 选择排序

selectionSort

思想:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

function selectionSort(arr) {
  let len = arr.length, minIndex;
  for (let i = 0; i < len - 1; i++) {
    minIndex = i;   //用来保存最小数
    for (let j = i + 1; j < len; j++) {
      if (arr[j] < arr[minIndex]) {     //寻找最小的数
        minIndex = j;                 //将最小数的索引保存
      }
    }
    [arr[minIndex],arr[i]] = [arr[i],arr[minIndex]]  //通过解构完成元素交换
  }
  return arr;
}

let arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(selectionSort(arr));

# 插入排序

insertSort

function insertSort(arr) {
  let len = arr.length;
  for(let i = 1; i < len; i++) {  //外循环从1开始,默认arr[0]是有序段
    for(let j = i; j > 0; j--) {  //j = i,表示此时你起在手上的那张牌,将arr[j]依次比较插入有序段中
      if(arr[j] < arr[j-1]) {
        [arr[j],arr[j-1]] = [arr[j-1],arr[j]];  //其实这里内循环中,只要比它前一个数小就交换,直到没有更小的,就break退出.这和动图表示的插入还是有点区别的,但最后结果其实是一样的.
      }
    }
  }
  return arr;
}

let arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(insertSort(arr));

# 快速排序

quickSort

思路:找到一个数作为参考,比这个数字大的放在数字右边,比它小的放在左边; 然后分别再对左边和右变的序列做相同的操作(递归)。快排是冒泡排序基础上的递归分治法。

function quickSort (arr) {
  let len = arr.length;
  if (len <= 1) {
    return arr;
  }
  let left = [], right = [], pivot = arr[0];
  for (let i = 1; i < len; i++) {
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return quickSort(left).concat(pivot, quickSort(right));
}

let arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(quickSort(arr));

# 归并排序

mergeSort

**原理:**归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。

function mergeSort(arr) {
  let len = arr.length;
  // 递归出口
  if (len<=1) return arr;
  // 将数组分成左右2份
  let middle = Math.floor(len / 2);
  let left = arr.slice(0, middle), right = arr.slice(middle);
  // 排序递归
  return merge(mergeSort(left), mergeSort(right));
  // 排序函数
  function merge(left, right) {
    let res = [];
    // 左右两边比较
    while (left.length && right.length) {
      if (left[0] <= right[0]) res.push(left.shift());
      else res.push(right.shift());
    }
    // 因为左右两边的数目不一定相等,所以跳出上面的递归之后有可能还有的没有排完
    while (left.length) res.push(left.shift());
    while (right.length) res.push(right.shift());
    return res;
  }
}
Last Updated: 11/1/2022, 11:24:21 AM