剑指Offer-数学部分

剪绳子

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

2 <= n <= 58

解题思路1

动态规划
状态数组dp[i]表示:数字 i 拆分为至少两个正整数之和的最大乘积。为了方便计算,dp 的长度是 n + 1,值初始化为 1。
显然dp[2]等于 1,外层循环从 3 开始遍历,一直到 n 停止。内层循环 j 从 1 开始遍历,一直到 i 之前停止,它代表着数字 i 可以拆分成 j + (i - j)。但 j * (i - j)不一定是最大乘积,因为i-j不一定大于dp[i - j](数字i-j拆分成整数之和的最大乘积),这里要选择最大的值作为 dp[i] 的结果。
空间复杂度是 O(N),时间复杂度是 O(N^2)。
状态数组i从1开始,建立数组从0开始,所以应该建立长度n+1的数组

var cuttingRope = function(n) {
    const dp = new Array(n + 1).fill(1);
    for (let i = 3; i <= n; ++i) {
        for (let j = 1; j < i; ++j) {
            dp[i] = Math.max(dp[i], j * (i - j), j * dp[i - j]);
        }
    }
    return dp[n];
};

解题思路2

贪心算法
前面提到:8 拆分为 3+3+2,此时乘积是最大的。然后就推测出来一个整数,要拆成多个 2 和 3 的和,保证乘积最大。原理很容易理解,因为 2 和 3 可以合成任何数字,例如5=2+3,但是5 < 23;例如6=3+3,但是6<33。所以根据贪心算法,就尽量将原数拆成更多的 3,然后再拆成更多的 2,保证拆出来的整数的乘积结果最大。
但上面的解法还有不足。如果整数 n 的形式是 3k+1,例如 7。按照上面规则,会拆分成“3 + 3 + 1”。但是在乘法操作中,1 是没作用的。此时,应该将 1 和 3 变成 4,也就是“3 + 3 + 1”变成“3 + 4”。此时乘积最大。
综上所述,算法的整体思路是:
n 除 3 的结果为 a,余数是 b
当 b 为 0,直接将 a 个 3 相乘
当 b 为 1,将(a-1)个 3 相乘,再乘以 4
当 b 为 2,将 a 个 3 相乘,再乘以 2
空间复杂度是 O(1),时间复杂度是 O(1)。

var cuttingRope = function(n) {
    if(n==2) return 1;
    if(n==3) return 2;
    const a = Math.floor(n / 3);
    if(n % 3 == 0) return Math.pow(3,a);
    if(n % 3 == 1) return Math.pow(3,a-1)*4;
    if(n % 3 == 2) return Math.pow(3,a)*2
};

剪绳子 II

题目与上题相同,但2 <= n <= 1000
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

var cuttingRope = function(n) {
  let arr=[0,0,1,2,4];
  if(n<5) return arr[n];
  const max=1e9+7;
  let res=1;
  while(n>=5){
      res=res%max*3;
      n=n-3;
  }
  return  res*n%max;
};

打印从1到最大的n位数

输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。

示例 1:
输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]
说明:
用返回一个整数列表来代替打印
n 为正整数

解题思路1

直接调用内置函数

var printNumbers = function(n) {
    let m = Math.pow(10,n);
    let res = [];
    for(let i=1;i<m;i++){
        res.push(i)
    }
    return res
};

解题思路2

位运算

var printNumbers = function(n) {
    let max = 1;
    let x = 10;
    while (n) {
        if (n & 1) {
            max = max * x;
        }
        x = x * x;
        n = n >> 1;
    }

    const res = [];
    for (let i = 1; i < max; ++i) {
        res.push(i);
    }
    return res;
};

利用位运算比直接调用内置函数用时低很多

表示数值的字符串

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。
①数值(按顺序)可以分成以下几个部分:
1.若干空格
2.一个 小数 或者 整数
3.可选)一个 ‘e’ 或 ‘E’ ,后面跟着一个 整数
4.若干空格
②小数(按顺序)可以分成以下几个部分:
1.(可选)一个符号字符(’+’ 或 ‘-’)
2.下述格式之一:
至少一位数字,后面跟着一个点 ‘.’
至少一位数字,后面跟着一个点 ‘.’ ,后面再跟着至少一位数字
一个点 ‘.’ ,后面跟着至少一位数字
③整数(按顺序)可以分成以下几个部分:
1.(可选)一个符号字符(’+’ 或 ‘-’)
2.至少一位数字
部分数值列举如下:
["+100", “5e2”, “-123”, “3.1416”, “-1E-16”, “0123”]
部分非数值列举如下:
[“12e”, “1a3.14”, “1.2.3”, “±5”, “12e+5.4”]

示例 1:
输入:s = "0"
输出:true
示例 2:
输入:s = "e"
输出:false
示例 3:
输入:s = "."
输出:false
示例 4:
输入:s = "    .1  "
输出:true

解题思路1

利用内置函数Number转换为数值,若转换不成功为NaN

var isNumber = function(s) {
    if(!s.trim()) return false
    return Number(s).toString() !== 'NaN'
};

解题思路2

正则表达式

var isNumber = function(s) {
    return /^[+-]?(\d+(\.\d*)?|(\.\d+))(e[+-]?\d+)?$/i.test(s.trim())
};

以^开头以 结 尾 加 以 限 制 例 如 , / A / 并 不 会 匹 配 " a n A " 中 的 ′ A ′ , 但 是 会 匹 配 " A n E " 中 的 ′ A ′ 。 例 如 , / t 结尾加以限制 例如,/^A/ 并不会匹配 "an A" 中的 'A',但是会匹配 "An E" 中的 'A'。 例如,/t /A/"anA"A"AnE"A/t/ 并不会匹配 “eater” 中的 ‘t’,但是会匹配 “eat” 中的 ‘t’。
综合举例“0E”会返回false而“0E2”返回true

1~n 整数中 1 出现的次数

输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

示例 1:
输入:n = 12
输出:5
示例 2:
输入:n = 13
输出:6

解题思路

找规律并归纳

   现在有一个函数f(n),代表n位上有多少个1
            f(0) = 0
     0~9    f(1) = 1
     0~99   f(2) = 10 + 10*f(1) = 20  (10+10~19中十位有101)
     0~999  f(3) = 100 + 10*f(2) = 300  (100+100~199中百位有1001)
     10~9999 f(4) = 1000 + 10*f(3) = 4000 (...)
    ...5467 中有多少个1
    1. 0~5000中有 5 * f(3) + 1000 = 25002. 0~400中有 4 * f(2) + 100 = 1803. 0~60中有 6 * f(1) + 10 = 164. 0~7中有 7 * f(0) + 1 = 1个
    所以5467中有26971
var countDigitOne = function(n) {
    let f = [0,1,20,300,4000,50000,600000,7000000,80000000,900000000,10000000000];
    let res = 0;
    let str = n + '';
    const len = str.length;
    let m = Math.pow(10,len - 1);
    let p = len - 1; //解析中的n
    for(let i = 0; i < len; i++) {
        res += str[i] * f[p];
        if(str[i] === '1' && i !== len - 1) {//中间为1时后面的每一个数都要加一个1,再加上第一个1,比如12中10,11,12三个数的十位有3个1,需要加上,也就是2+1个1要加上
        	res += Number(str.slice(i + 1)) + 1;
        } else if(str[i] === '1' && i === len - 1) {//解决末尾为1但未加上的bug
        	res += 1;
        }
        if(str[i] > 1) res += m;
        m /= 10, p -=1;
    }
    return res;
};

数字序列中某一位的数字

数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。
请写一个函数,求任意第n位对应的数字。

示例 1:
输入:n = 3
输出:3
示例 2:
输入:n = 11
输出:0
限制:
0 <= n < 2^31

解题思路

写出这些数据,可以发现:
1位数: 1~9 9个数字 9个字符
2位数: 10~99 9 * 10个数字 9 * 10 * 2个字符
3位数: 100~999 9 * 100个数字 9 * 100 * 3个字符
……
解题步骤分三步:
找到n的区间
找到n在此区间中的哪个数字上面
找到n在这个数字中的位置并返回

所以解决思路就是先通过循环,确定所要查找的数字落在第几位。最后通过计算找出数字即可。例如对于 n=13 来说,过程如下:
n 大于 9,说明不是 1 位数字,n 更新为 n - 9 = 4。继续循环。
n 小于 90,说明是 2 位数字。
计算要找的数字 num:num = 10 + int(4/2) - 1 = 11
计算结果在 num 中的位置:pos = 4 - 2 * (11 - 10) - 1 = 1

var findNthDigit = function(n) {
    for (let bit = 1; bit < 32; ++bit) {
        const startNum = Math.pow(10, bit - 1);
        const bitSum = 9 * startNum * bit;
        if (n > bitSum) {
            n -= bitSum;
        } else {
            let num = startNum + Math.ceil(n / bit) - 1;
            let pos = n - bit * (num - startNum) - 1;
            return num.toString(10)[pos];
        }
    }
};

丑数

我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。

示例:
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
说明:  
1 是丑数。
n 不超过1690

解题思路1

动态规划
因为丑数只包含质因数 2, 3, 5,所以对于下个丑数来说,一定是前面某个丑数乘 3、乘 4 或者乘 5 所得。
准备三个指针 ptr2、ptr3、ptr5,它们指向的数只能乘 2、3 和 5。在循环过程中,每次选取 2 * res[ptr2]、3 * res[ptr3] 和 5 * res[ptr5]这三个数中结果最小的数,并且将对应的指针向前移动。有效循环是 n 次,当循环结束后,res 数组中就按从小到大的顺序保存了丑数。

var nthUglyNumber = function(n) {
    let res = new Array(n);
    res[0] = 1;
    let pstr2 = 0;
    let pstr3 = 0;
    let pstr5 = 0;
    for(let i=1;i<n;i++){
        res[i] = Math.min(res[pstr2]*2,res[pstr3]*3,res[pstr5]*5);
        if(res[i] == res[pstr2]*2){
            pstr2++;
        }
        if(res[i] == res[pstr3]*3){
            pstr3++;
        }
        if(res[i] == res[pstr5]*5){
            pstr5++;
        }
    }
    return res[n-1]
};

这里的代码if分开写的原因是例如当res[i]为6时,2,3这两个指针都应该向前移动。
时间复杂度是O(N),空间复杂度是O(N)。

解题思路2

最小堆
借助最小堆,可以在 O(LogN)O(LogN) 时间复杂度内找到当前最小的元素。整体算法流程是:
准备最小堆 heap。准备 map,用于记录丑数是否出现过。
将 1 放入堆中
从 0 开始,遍历 n 次:
取出堆顶元素,放入数组 res 中
用堆顶元素依此乘以 2、3、5
检查结果是否出现过。若没有出现过,那么放入堆中,更新 map
返回 res 最后一个数字

//转自作者:xin-tan
//链接:https://leetcode-cn.com/problems/chou-shu-lcof/solution/shuang-jie-fa-dong-tai-gui-hua-zui-xiao-dui-javasc/
const defaultCmp = (x, y) => x > y;
const swap = (arr, i, j) => ([arr[i], arr[j]] = [arr[j], arr[i]]);
class Heap {
    /**
     * 默认是最大堆
     * @param {Function} cmp
     */
    constructor(cmp = defaultCmp) {
        this.container = [];
        this.cmp = cmp;
    }

    insert(data) {
        const { container, cmp } = this;

        container.push(data);
        let index = container.length - 1;
        while (index) {
            let parent = Math.floor((index - 1) / 2);
            if (!cmp(container[index], container[parent])) {
                return;
            }
            swap(container, index, parent);
            index = parent;
        }
    }

    extract() {
        const { container, cmp } = this;
        if (!container.length) {
            return null;
        }

        swap(container, 0, container.length - 1);
        const res = container.pop();
        const length = container.length;
        let index = 0,
            exchange = index * 2 + 1;

        while (exchange < length) {
            // 如果有右节点,并且右节点的值大于左节点的值
            let right = index * 2 + 2;
            if (right < length && cmp(container[right], container[exchange])) {
                exchange = right;
            }
            if (!cmp(container[exchange], container[index])) {
                break;
            }
            swap(container, exchange, index);
            index = exchange;
            exchange = index * 2 + 1;
        }

        return res;
    }

    top() {
        if (this.container.length) return this.container[0];
        return null;
    }
}

/**
 * @param {number} n
 * @return {number}
 */
var nthUglyNumber = function(n) {
    const heap = new Heap((x, y) => x < y);
    const res = new Array(n);
    const map = {};
    const primes = [2, 3, 5];

    heap.insert(1);
    map[1] = true;
    for (let i = 0; i < n; ++i) {
        res[i] = heap.extract();

        for (const prime of primes) {
            let tmp = res[i] * prime;
            if (!map[tmp]) {
                heap.insert(tmp);
                map[tmp] = true;
            }
        }
    }
    return res[n - 1];
};

时间复杂度是O(NlogN), 空间复杂度是O(N)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值