《剑指Offer:专项突破版》 - 整数部分 JavaScript 题解

Posted 前端GoGoGo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《剑指Offer:专项突破版》 - 整数部分 JavaScript 题解相关的知识,希望对你有一定的参考价值。

《剑指Offer:专项突破版》是一个算法题集。该题单包含了程序员在准备面试过程中必备的数据结构与算法知识。具体包含:

  • 数据结构:整数、数组、字符串、链表、哈希表、栈、队列、树、堆和前缀树。
  • 算法:二分查找、排序、回溯法、动态规划和图搜索。
  • 最近,把整数部分的题刷完了。本文来分享下这些题的解法~

    题1 - 整数除法[1]

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

    题解

    最容易想到的算法是用减法来模拟除法。被除数减除数,直到被除数小于除数为止。核心代码如下:

    const times = 0;
    while (left >= right) 
        left -= right;
        times++;

    该算法存在性能问题。当被除数很大但除数很小时,减法操作执行的次数会很多。

    改进算法如下:探测除数是否大于除数的2倍。如果是,则继续判断被除数是否大于除数的4倍、8倍...2n倍,直到被除数大于被除数。此时,将被除数减去除数的 2n 倍,余数重复前面的步骤。被除数是15,除数是2时,用该算法来解如下图所示:

    代码实现如下:

    let times = 0;
    let rest = left;
    while (rest >= right) 
    let res = getMaxTimes(rest, right);
    rest = res.rest;
    times += res.times;


    function getMaxTimes(left, right
      let times = 1;
      let prevRight = right;
      right += right;
      
      while (left >= right) 
        prevRight = right;
        right += right;
        times += times;
      
      return 
        times,
        rest: left - prevRight,
      ;

    题2 - 二进制加法[2]

    给定两个 01 字符串 a 和 b ,请计算它们的和,并以二进制字符串的形式输出。输入为 非空 字符串且只包含数字 1 和 0。

    解决方案

    最容易想到的算法是将字符串转化出十进制的整数,求和后转化成二进制的字符串。实现如下:

    const addBinary = function (left, right
      const leftNum = parseInt(left, 2);
      const rightNum = parseInt(right, 2);
      const resNum = leftNum + rightNum;
      const resStr = resNum.toString(2);
      return resStr;
    ;

    该算法存在的问题是:当求和的字符串足够大,转化成整数时会出现溢出。

    要规避这种情况,可以将求和字符串右对齐,将每位相加。我这边将字符串转化成数组来处理。代码如下:

    const addBinary = function (left, right
      const leftArr = toNumberArr(left);
      const leftLen = leftArr.length;
      const rightArr = toNumberArr(right);
      const rightLen = rightArr.length;
      const maxLen = Math.max(leftLen, rightLen);

      const sumArr = [];

      // 是否进位
      let isCarry = false;
      for (let i = 1; i <= maxLen; i++) 
        const res = add(leftArr[leftLen - i], rightArr[rightLen - i], isCarry);
        sumArr[maxLen - i] = res.sum;
        isCarry = res.isCarry;
      
      const tempSum = sumArr.join(\'\');
      const sum = isCarry ? `1$tempSum` : tempSum;
      return sum;
    ;

    /*
     * 一位的运算
     */

    function add(left, right, isCarry
      const sum = (left || 0) + (right || 0) + (isCarry ? 1 : 0);
      if (sum > 1
        return 
          sum: sum - 2,
          isCarrytrue,
        ;
      
      return 
        sum,
        isCarryfalse,
      ;


    function toNumberArr(str
      return str.split(\'\').map((item) => parseInt(item, 10));

    题3 - 前 n 个数字二进制中 1 的个数[3]

    给定一个非负整数 n ,请计算 0 到 n 之间的每个数字的二进制表示中 1 的个数,并输出一个数组。

    解决方案

    暴力解法是遍历所有数字,一个个算。但这样有效率问题。

    可以根据偶数奇数的二进制的特点来高效的解决该问题。n 左移动一位,就变成了偶数 2n。因此,偶数 2n 和 n 的二进制中的 1 的个数相同。n 左移动一位再加1,就变成了奇数 2n + 1。因此,奇数 2n + 1 比 n 的二进制中的 1 的个数多1。如下图所示:

    综上,知道 1 的二进制中 1 的个数,就能知道其他数字二进制中 1 的个数。实现:

    const countBits = function (n
      if (n === 0
        return [0];
      
      if (n === 1
        return [01];
      
      const res = [01];
      for (let i = 2; i <= n; i++) 
        if (i % 2 === 0
          res[i] = res[i / 2];
         else 
          res[i] = res[(i - 1) / 2] + 1;
        
      
      return res;
    ;
    题4 - 只出现一次的数字[4]

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

    解决方案

    最容易想到的算法是计算数组中所有数字出现的次数。然后,筛选出只出现一次的。

    也可以用二进制的方法解决该问题。具体算法:

    1. 将数组中所有数字转化成二进制。
    2. 每位求和,除以3。
    3. 结果就是只出现一次的数字的二进制。

    要注意的是,可能会存在负数。我这边是把负数全部转化成正数。最后找到的正数去原数组里找,如果只存在一次,则为正数,否则返回负数。

    实现如下:

    const singleNumber = function (nums
      const positiveNum = nums.map((num) => Math.abs(num));
      const maxBitLen = Math.max(...positiveNum).toString(2).length;
      const binaryStrArr = positiveNum.map((num) => num.toString(2));
      const binarySumArr = [];
      for (let i = 1; i <= maxBitLen; i++) 
        binarySumArr[maxBitLen - i] = 0;
        binaryStrArr.forEach((numStr) => 
          binarySumArr[maxBitLen - i] += parseInt(
            numStr.charAt(numStr.length - i) || 0,
            10
          );
        );
      
      const resBitArr = binarySumArr.map((num) => parseInt(num, 10) % 3);
      const res = parseInt(resBitArr.join(\'\'), 2);
      const isResPositive = nums.filter((num) => num === res).length === 1;

      return isResPositive ? res : -res;
    ;
    题5 - 单词长度的最大乘积[5]

    给定一个字符串数组 words,请计算当两个字符串 words[i] 和 words[j] 不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 0。

    解决方案

    比较两个字符串是否相同,最先想到的是将第一个字符串中所有的字母在第二个字符串中找一遍。找不到则表示两个字符串相同。

    另一种方式是将字符的查找问题转化成数学问题。具体算法:

    1. 用一位是1其他位数是0的二进制来表示一个不同的字母。比如 a 是 1, b 是 10, c 是 100。
    2. 去掉单词内相同的字母。
    3. 将单词包含的字母表示的二进制数求和。
    4. 单词之间做 & 操作,结果为0,则表示没有相同的字母。

    实现如下:

    const maxProduct = function (words
      const lettersArr = \'abcdefghijklmnopqrstuvwxyz\'.split(\'\');
      const lettersValue = ;
      for (let i = 0; i < 26; i++) 
        lettersValue[lettersArr[i]] = 1 << i;
      

      // 单词内的字母去重
      const uniqCharWords = words.map((word) => uniq(word));
      const wordsSum = uniqCharWords.map((word) => 
        return getSum(word, lettersValue);
      );

      let max = 0;
      for (i = 0; i < words.length; i++) 
        for (let j = i + 1; j < words.length; j++) 
          if ((wordsSum[i] & wordsSum[j]) === 0
            // 两个单词没有相同的字符
            const value = words[i].length * words[j].length;
            if (value > max) 
              max = value;
            
          
        
      
      return max;
    ;

    function uniq(word
      const res = [];
      const cache = ;
      word.split(\'\').forEach((c) => 
        if (cache[c]) 
          return;
        
        cache[c] = true;
        res.push(c);
      );
      return res.join(\'\');


    function getSum(word, lettersValue
      return word.split(\'\').reduce((prev, curr) => prev + lettersValue[curr], 0);

    最后

    本文所有代码在:这里[6]

    最近建了个免费的知识星球,目的有两个:

    1. 用来沉淀和分享有价值的前端信息。
    2. 做群活动的管理。目前正在进行中的主题读书会:《高效阅读技术书籍》。用知识星球来做每日的心得分享和打卡。

    感兴趣的,扫码加入:

    参考资料
    [1]

    整数除法: https://leetcode-cn.com/problems/xoh6Oh/

    [2]

    二进制加法: https://leetcode-cn.com/problems/JFETK5/

    [3]

    前 n 个数字二进制中 1 的个数: https://leetcode-cn.com/problems/w3tCBm/

    [4]

    只出现一次的数字: https://leetcode-cn.com/problems/WGki4K/

    [5]

    单词长度的最大乘积: https://leetcode-cn.com/problems/aseY1I/

    [6]

    这里: https://github.com/iamjoel/get-offer-high-efficiency/tree/master

    《剑指Offer:专项突破版》 - 数组部分 JavaScript 题解

    《剑指Offer:专项突破版》是一个算法题集。该题单包含了程序员在准备面试过程中必备的数据结构与算法知识。具体包含:

  • 数据结构:整数、数组、字符串、链表、哈希表、栈、队列、树、堆和前缀树。
  • 算法:二分查找、排序、回溯法、动态规划和图搜索。
  • 本文来分享下数组部分题的解法~

    的两个数。此时,用上一题的方法求解。最后,将结果做个去重。

    核心代码实现如下:

    const threeSum = function (nums
      const sortNums = [...nums]
      sortNums.sort((a, b) => a - b);

      const res = []
        for (let i = 0; i < sortNums.length - 2; i++) 
          const currNum = sortNums[i];
          const target = 0 - currNum;
          const twoSumRes = twoSum(sortNums.slice(i + 1), target);
          if (twoSumRes.length > 0
            res.push(...twoSumRes.map((item) => [currNum, ...item]));
          
      
      return uniq(res);


    function twoSum(numbers, target
      const res = [];
      let leftIndex = 0;
      let rightIndex = numbers.length - 1;

      for (left < right) 
        let left = numbers[leftIndex];
        let right = numbers[rightIndex];
        if (left + right === target) 
          res.push([left, right]);
          leftIndex++;
          continue;
        
        if (left + right > target) 
          rightIndex--;
         else 
          leftIndex++;
        
      
      return res;

    力扣地址[2]

    题3 - 剑指 Offer II 008. 和大于等于 target 的最短子数组

    输入一个正整数组成的数组和一个正整数k,请问数组中和大于或等于k的连续子数组的最短长度是多少?如果不存在所有数字之和大于或等于k的子数组,则返回0。例如,输入数组[5,1,4,3],k的值为7,和大于或等于7的最短连续子数组是[4,3],因此输出它的长度2。

    该题本质就是找出所有满足条件的数组,挑最短的那个。如果用暴力的方式出找出所有可能性,时间复杂度太高了。

    一种高效的解决方案是用双指针。用两个下标的移动来找出所有满足条件的情况。具体算法如下:

    1. 开始位置的下标和结束位置下标都为0。
    2. 如果开始下标和结束下标之间(包含开始和结束)的数字之和小于k,则结束下标右移一位(和会变大),直到和不小于k或结束下标为数组结束。
    3. 开始下标右移一位(和会变小),直到和小于k或开始下标等于结束下标。
    4. 重复第二和第三步,直到结束下标为数组结束。

    代码实现如下:

    const minSubArrayLen = function (target, nums
      let startIndex = 0;
      let sum = 0;
      let count = Number.MAX_VALUE;

      for (let endIndex = 0; endIndex < nums.length; endIndex++) 
        sum += nums[endIndex];
        while (sum >= target && startIndex <= endIndex) 
          count = Math.min(count, endIndex - startIndex + 1);
          sum -= nums[startIndex];
          startIndex++; // 尝试少一个数字
        
      

      return count === Number.MAX_VALUE ? 0 : count;
    ;

    力扣地址[3]

    题4 - 剑指 Offer II 009. 乘积小于 K 的子数组

    输入一个由正整数组成的数组和一个正整数k,请问数组中有多少个数字乘积小于k的连续子数组?例如,输入数组[10,5,2,6],k的值为100,有8个子数组的所有数字的乘积小于100,它们分别是[10]、[5]、[2]、[6]、[10,5]、[5,2]、[2,6]和[5,2,6]。

    这题可以用上题一样的算法来解决。只是在细节上稍有不同。这边就不做赘述了。

    代码实现如下:

    const numSubarrayProductLessThanK = function (nums, target
      let startIndex = 0;
      let res = 1;
      let count = 0;

      for (let endIndex = 0; endIndex < nums.length; endIndex++) 
        res *= nums[endIndex];
        while (res >= target && startIndex <= endIndex) 
          res /= nums[startIndex];
          startIndex++; // 尝试少一个数字
        
        count += endIndex - startIndex + 1// 1 到 n 的组合
      

      return count;
    ;

    力扣地址[4]

    题5 - 剑指 Offer II 010. 和为 k 的子数组

    输入一个整数数组和一个整数k,请问数组中有多少个数字之和等于k的连续子数组?例如,输入数组[1,1,1],k的值为2,有2个连续子数组之和等于2。

    假设数组长度是n,找到结束下标范围是 0 到 n-1 满足条件的子数组,就找到了所有的情况。

    假设当前结束下标是 m,存在若干开始下标 x,满足和是 k。其中 x < m。有等式1:

    arr[x + 1] + arr[x + 2] + ... + ...arr[m] = k

    假设,前 m 个数的和为 sum。即,有等式2:

    arr[0] + arr[1] + ... + arr[m] = sum

    等式2减等式1,可得:

    arr[0] + arr[1] + ... + arr[x] = sum - k

    有多少个这样下标x,就有多少可能的子数组。可以通过缓存数组从开始到某个数的和来减少重复计算。

    代码实现如下:

    const subarraySum = function (nums, k
      let count = 0;
      let sum = 0;
      const sum =  01 ; // 和是0的值,初始化有一个。

      nums.forEach((num) => 
        sum += num;
        count += sum[sum - k] || 0;
        sumCache[sum] = (sumCache[sum] || 0) + 1;
      );

      return count;
    ;

    力扣地址[5]

    题6 - 剑指 Offer II 011. 0 和 1 个数相同的子数组

    输入一个只包含0和1的数组,请问如何求0和1的个数相同的最长连续子数组的长度?例如,在数组[0,1,0]中有两个子数组包含相同个数的0和1,分别是[0,1]和[1,0],它们的长度都是2,因此输出2。

    将数组中的 0 都替换成 -1,则 0 和 1 的个数相同数组的和为0。本题就转化成了:找出最长的和是0的子数组的长度。可以用上题的算法来解本题。

    代码实现如下:

    const findMaxLength = function (nums
      let count = 0;
      let sum = 0;
      const sumIndexCache =  0-1 ; // key 是 和, value 是下标

      nums.forEach((num, i) => 
        sum += num === 0 ? -1 : 1;
        if (sumIndexCache[sum] !== undefined
          count = Math.max(count, i - sumIndexCache[sum]);
         else 
          sumIndexCache[sum] = i;
        
      );

      return count;
    ;

    力扣地址[6]

    题7 - 剑指 Offer II 012. 左右两边子数组的和相等

    输入一个整数数组,如果一个数字左边的子数组的数字之和等于右边的子数组的数字之和,那么返回该数字的下标。如果存在多个这样的数字,则返回最左边一个数字的下标。如果不存在这样的数字,则返回-1。例如,在数组[1,7,3,6,2,9]中,下标为3的数字(值为6)的左边3个数字1、7、3的和与右边两个数字2和9的和相等,都是11,因此正确的输出值是3。

    前 n 个数的和等于前 n -1 个数的和加上第 n 的数的和。缓存之前求的和,可以避免重复计算,从而降低算法的时间复杂度。

    代码实现如下:

    const pivotIndex = function (nums
      const sumCache = []; // 前 n 个,不包含 n
      let sumTotal = 0;
      nums.forEach((num) => 
        sumCache.push(sumTotal);
        sumTotal += num;
      );

      for (let i = 0; i <= nums.length - 2; i++) 
        if (sumCache[i] === sumTotal - sumCache[i + 1]) 
          return i;
        
      
      if (sumCache[nums.length - 1] === 0
        return nums.length - 1;
      
      return -1;
    ;

    力扣地址[7]

    题8 - 剑指 Offer II 013. 二维子矩阵的和

    输入一个二维矩阵,如何计算给定左上角坐标和右下角坐标的子矩阵的数字之和?对于同一个二维矩阵,计算子矩阵的数字之和的函数可能由于输入不同的坐标而被反复调用多次。例如,输入图2.1中的二维矩阵,以及左上角坐标为(2,1)和右下角坐标为(4,3)的子矩阵,该函数输出8。

    通过观察,任意的 左上角坐标 和 右下角坐标 的子矩阵的数字之和 可以转化为左上角坐标是(0,0)的几个矩阵运算:

    res = sum[row2][col2] -
        (row1 > 0 ? sum[row1 - 1][col2] : 0) -
        (col1 > 0 ? sum[row2][col1 - 1] : 0) +
        (row1 > 0 && col1 > 0 ? sum[row1 - 1][col1 - 1] : 0)

    其中:

  • row1,col1 为 左上角坐标。row2,col2 为 右下角坐标。
  • sum[x][y] 为 左上角坐标是 (0,0)右下角坐标为 x,y的矩阵的和。
  • 因此,缓存所有的左上角坐标是(0,0)的子矩阵的和,就可以快速的算出任意子矩阵的和。

    代码实现如下:

    const NumMatrix = function (matrix
      const sum = [];
      for (let row = 0; row < matrix.length; row++) 
        sum[row] = [];
        if (row === 0
          let rowSum = 0;
          for (let col = 0; col < matrix[0].length; col++) 
            rowSum += matrix[0][col]
            sum[0][col] = rowSum;
          
          continue;
        
        let rowSum = 0;
        for (let col = 0; col < matrix[0].length; col++) 
          rowSum += matrix[row][col];
          sum[row][col] = sum[row - 1][col] + rowSum;
        
      
      this.sum = sum;
    ;

    NumMatrix.prototype.sumRegion = function (row1, col1, row2, col2
      const sum = this.sum;
      return (
        sum[row2][col2] -
        (row1 > 0 ? sum[row1 - 1][col2] : 0) -
        (col1 > 0 ? sum[row2][col1 - 1] : 0) +
        (row1 > 0 && col1 > 0 ? sum[row1 - 1][col1 - 1] : 0)
      );
    ;

    力扣地址[8]

    总结

    解数组的算法题,在某些场景下,用双指针和缓存的技巧可以降低算法的时间复杂度。

    用双指针,可以减少遍历数组的次数。难点在确定开始,结束指针的位置,移动规则,做到不遗漏情况。

    用缓存可以减少重复计算。多用在求和,求乘等。关键是发现任意输入和缓存值的映射关系。

    相关推荐
  • 《剑指Offer:专项突破版》 - 整数部分 JavaScript 题解
  • 参考资料
    [1]

    力扣地址: https://leetcode-cn.com/problems/kLl5u1/

    [2]

    力扣地址: https://leetcode-cn.com/problems/1fGaJU/

    [3]

    力扣地址: https://leetcode-cn.com/problems/2VG8Kg/

    [4]

    力扣地址: https://leetcode-cn.com/problems/ZVAVXX/

    [5]

    力扣地址: https://leetcode-cn.com/problems/QTMn0o/

    [6]

    力扣地址: https://leetcode-cn.com/problems/A1NYOS/

    [7]

    力扣地址: https://leetcode-cn.com/problems/tvdfij/

    [8]

    力扣地址: https://leetcode-cn.com/problems/O4NDxx/

    以上是关于《剑指Offer:专项突破版》 - 整数部分 JavaScript 题解的主要内容,如果未能解决你的问题,请参考以下文章

    《剑指Offer:专项突破版》 - 哈希表部分 JavaScript 题解

    《剑指Offer:专项突破版》 - 栈部分 JavaScript 题解

    《剑指Offer:专项突破版》 - 链表部分 JavaScript 题解

    剑指offer第二版和专项突击版有啥区别

    剑指offer第二版和专项突击版有啥区别

    剑指Offer整数整数除法 - 两数相除 - JavaScript