剑指Offer-剩余部分

机器人的运动范围

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

示例 1:
输入:m = 2, n = 3, k = 1
输出:3
示例 2:
输入:m = 3, n = 1, k = 0
输出:1
提示:
1 <= n,m <= 100
0 <= k <= 20

解题思路1

广度优先遍历(BFS)
和普通 BFS 相比,有两点不同:
需要调用 bitSum 来检查数位之和
因为从左上角开始遍历,因此只需要遍历「右」和「下」这两个方向

var movingCount = function(m, n, k) {
    function bitSum(n){
        let res = 0;
        while(n){
            res += n%10;
            n = Math.floor(n/10)
        }
        return res
    }
    let res = 0;
    let directions = [
        [1,0],
        [0,1]
    ]
    let queue = [[0,0]];
    let visited = {
        "0-0":true
    }
    while(queue.length){
        let [x,y] = queue.shift();
        if(bitSum(x)+bitSum(y)>k){
            continue
        }
        res++;
        for(let direction of directions){
            let newX = direction[0] + x;
            let newY = direction[1] + y;
            if(!visited[`${newX}-${newY}`] && newX>=0 && newY>=0 && newX < m && newY < n){
                visited[`${newX}-${newY}`] = true;
                queue.push([newX,newY])
            }
        }
    }
    return res
};

解题思路2

深度优先遍历(DFS)

var movingCount = function(m, n, k) {
    function bitSum(n){
        let res = 0;
        while(n){
            res += n%10;
            n = Math.floor(n/10)
        }
        return res
    }
    let res = 0;
    let directions = [
        [1,0],
        [0,1]
    ]
    let visited = {};
    dfs(0,0)
    return res
    function dfs(x,y){
        visited[`${x}-${y}`] = true
        if(bitSum(x)+bitSum(y)>k){
            return
        }
        res++;
        for(let direction of directions){
            let newX = direction[0] + x;
            let newY = direction[1] + y;
            if(!visited[`${newX}-${newY}`] && newX>=0 && newY>=0 && newX < m && newY < n){
                dfs(newX,newY)
            }
        }
    }
};

调整数组顺序使奇数位于偶数前面

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。

示例:
输入:nums = [1,2,3,4]
输出:[1,3,2,4] 
注:[3,1,2,4] 也是正确的答案之一。
提示:
0 <= nums.length <= 50000
1 <= nums[i] <= 10000

解题思路1

开辟新数组

var exchange = function(nums) {
    let num = []
    for(let i=0;i<nums.length;i++){
        if(nums[i]%2 == 1){
            num.unshift(nums[i])
        }else{
            num.push(nums[i])
        }
    }
    return num
};

时间复杂度O(n),空间复杂度O(n)

解题思路2

可以利用“双指针”,分别是指向数组头部的指针 i,与指向数组尾部的指针 j。过程如下:
i 向右移动,直到遇到偶数;j 向左移动,直到遇到奇数
检查 i 是否小于 j,若小于,交换 i 和 j 的元素,回到上一步骤继续移动;否则结束循环
时间复杂度是 O(N),空间复杂度是 O(1)。

var exchange = function(nums) {
    let i = 0;
    let j = nums.length-1;
    while(i<j){
        while(i<nums.length-1 && nums[i]%2 == 1){
            i++
        }
        while(j>0 && nums[j]%2 == 0){
            j--
        }
        if(i<j){
            [nums[i],nums[j]] = [nums[j],nums[i]];
            i++;
            j--
        }
    }
    return nums
};

栈的压入、弹出序列

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。

示例 1:
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
示例 2:
输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false
解释:1 不能在 2 之前弹出。
提示:
0 <= pushed.length == popped.length <= 1000
0 <= pushed[i], popped[i] < 1000
pushed 是 popped 的排列。

解题思路

利用辅助栈,根据释放辅助栈来判断popped序列是否为弹出序列,如果释放不了,则辅助栈的长度不为0,则不为弹出序列

var validateStackSequences = function(pushed, popped) {
    let stack = [];
    let index = 0;
    for(let i=0;i<pushed.length;i++){
        stack.push(pushed[i]);
        while(stack.length != 0 && stack[stack.length-1] == popped[index]){
            stack.pop();
            index++
        }
    }
    return !stack.length
};

二叉搜索树的后序遍历序列

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。

参考以下这颗二叉搜索树:
     5
    / \
   2   6
  / \
 1   3
示例 1:
输入: [1,6,3,2,5]
输出: false
示例 2:
输入: [1,3,2,6,5]
输出: true
提示:
数组长度 <= 1000

解题思路

采用递归,二叉搜索树的左子树均小于根节点,右子树均大于根节点。且划分时根节点为最后一位数。

var verifyPostorder = function(postorder) {
    let len = postorder.length;
    if(len<2) return true;
    let i = 0;
    let root = postorder[len-1]
    for(;i<len-1;i++){
        if(postorder[i]>root) break
    }
    let right = postorder.slice(i,len-1).every(x=>x>root);
    if(right){
        return verifyPostorder(postorder.slice(0,i)) && verifyPostorder(postorder.slice(i,len-1))
    }else{
        return false
    }
};

字符串的排列

输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]
限制:
1 <= s 的长度 <= 8

解题思路

回溯法 (回溯利用递归实现,但有自我判断过程)
在递归之前缓存当前循环已经使用过的字符串的索引, 避免在递归循环中再次使用相同字符串拼接
这里用递归的方式再次循环字符串, 将没有使用过的字符串拼接上, 并打上已经使用的标识
已经使用的标识去除的时机就在递归的方法执行完毕的时候, 这个时候需要将当前字符串已经使用过的标识去除, 用于下一次当前循环需要使用字符串, 这样的话就可以按照上图执行的顺序, 依次拼接 dfs(path + s[i]);

var permutation = function(s) {
    let arr = new Set();
    let visit = {};
    let dfs = function(path){
        if(path.length == s.length) return arr.add(path)
        for(let i=0;i<s.length;i++){
            if(visit[i]) continue;
            visit[i] = true;
            dfs(path+s[i]);
            visit[i] = false
        }
    }
    dfs('')
    return [...arr] 
};

把数字翻译成字符串

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

示例 1:
输入: 12258
输出: 5
解释: 122585种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi""mzi"
提示:
0 <= num < 231

解题思路参考:
作者:xiao_ben_zhu
链接:https://leetcode-cn.com/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/solution/shou-hui-tu-jie-dfsdi-gui-ji-yi-hua-di-gui-dong-ta/
来源:力扣(LeetCode)

解题思路1

递归
翻译 2163 可以分解成:翻译 2 和剩下的 163 、翻译 21 和剩下的 63
每次都有2种选择:翻译1个数、或2个数,但有的两位数没有对应字母,翻译2个数这个选项就被砍掉。
指针从左往右扫描,画出下图,2 种选择对应 2 个分支,1 种选择对应 1 个分支。
当指针到达边界和正好越界,都返回 1。这使得下图 △ 分支返回 0。成为死支。
在这里插入图片描述
定义递归函数
dfs 函数求:「当前指针位置到末尾的数字」的翻译方法数。
节点的状态用指针表示,dfs 入口传 0。
如果 指针 和 指针+1 对应的两位数处于[10,25]范围,则可以直译,有两种选择:
翻译 1 个数,指针走一步,递归调用 dfs,返回出剩余数字的翻译方法数。
翻译 2 个数,指针走两步,递归调用 dfs,返回出剩余数字的翻译方法数。
二者相加,就是当前数字串的翻译方法数。
如果 指针 和 指针+1 对应的两位数不在[10, 25]内,则无法直译,只有一个选择:
翻译 1 个数,指针走一步,递归调用 dfs,返回出剩余子串的翻译方法数。

var translateNum = function(num) {
    let str = num.toString();
    let dfs = function(str,pointer){
        if(pointer >= str.length-1) return 1
        let Num = Number(str[pointer] + str[pointer+1]);
        if(Num>=10 && Num<=25){
            return dfs(str,pointer+1) + dfs(str,pointer+2)
        }else{
            return dfs(str,pointer+1)
        }
    }
    return dfs(str,0)
};

解题思路2

解题思路1存在重复计算
在这里插入图片描述
黄色、蓝色阴影所标注的是重复的子树。
该递归优先遍历左子树,在右侧遇到重复子树时,没有必要重新计算。
计算过的结果用一个备忘录存下来,再遇到就直接拿来用。我们剪去重复的子树:
在这里插入图片描述
记忆化递归
可以用 map 作备忘录,也可以用数组。创建数组 memo,索引是指针位置,元素是对应子树的返回值。
下图例子,数字的长度 n = 4,memo[3] 和 memo[4] 是已知的,值都为 1,是 base case。
在这里插入图片描述

const translateNum = (num) => {
    let str = num.toString();
    let n = str.length;
    let memo = new Array(n);
    memo[n-1] = 1;
    memo[n] = 1;
    let dfs = function(str,pointer,memo){
        if(memo[pointer]) return memo[pointer];
        let Num = Number(str[pointer] + str[pointer+1]);
        if(Num>=10 && Num<=25){
            memo[pointer] = dfs(str,pointer+1,memo) + dfs(str,pointer+2,memo)
        }else{
            memo[pointer] = dfs(str,pointer+1,memo)
        }
      return memo[pointer]
    }
    return dfs(str,0,memo)
}

总结一下记忆化递归
先往 memo 存入两个已知的、处于底部的子树的结果。等 dfs 往下遇到它,就能直接从 memo 中拿出来用。递归的结果从下往上返回的过程中,子树的计算结果不断抄录到 memo 中。本来递归没有记忆计算结果,现在加入了记忆化。
复杂度分析
暴力 DFS
time:O(2^n) 。每个节点都要遍历,栈的深度 n ,n 是数字字符个数
space: O(n) 。栈的深度 n ,n 是数字字符个数
DFS + 备忘录
time:O(n) 。栈的深度 n ,n 是数字字符个数
space: O(n) 。栈的深度 n ,n 是数字字符个数

解题思路3

动态规划
动态规划和递归的区别
前两个方法是递归,不断压栈再不断出栈。是自上而下解决问题,等待下面返回上来的结果。动态规划是自下而上解决问题,从已知的 case 出发,存储前面的状态,迭代出最后的结果。动态规划就是想办法不用递归,利用递推关系用“填表格”的方式顺序计算。每个 dp 项的值其实等于一个递归子调用的结果(递归子问题的解)。
定义 dp[i] 、base case:dp[0] 为何为 1
dp[i]:翻译前 i 个数的方法数。
dp[0]=1 ,是为了让边界情况也能满足通式:比如数字 16 ,dp[2]=2 是肯定的,dp[1]=1 也是肯定的。为了让 dp[2]=dp[0]+dp[1]通式成立,只有让 dp[0]=1
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

const translateNum = (num) => {
  const str = num.toString();
  const n = str.length;
  const dp = new Array(n + 1);
  dp[0] = 1;
  dp[1] = 1;
  for (let i = 2; i < n + 1; i++) {
    const temp = Number(str[i - 2] + str[i - 1]);
    if (temp >= 10 && temp <= 25) {
      dp[i] = dp[i - 1] + dp[i - 2];
    } else {
      dp[i] = dp[i - 1];
    }
  }
  return dp[n]; // 翻译前n个数的方法数,即翻译整个数字
}

降维
刚才的时间复杂度是O(n),n 是状态转移的次数,空间复杂度是 O(n)。
你会发现,当前 dp 项只和它前面两项有关,无需用数组存储所有的 dp 项。用两个变量去存这两个状态值,在迭代中更新就好。空间复杂度可以优化到 O(1)。

const translateNum = (num) => {
    let str = num.toString();
    let n = str.length;
    let cur = 1;
    let pre = 1
    for(let i=2;i<n+1;i++){
        let Num = Number(str[i-2] + str[i-1]);
        if(Num>=10 && Num<=25){
            let temp = cur;
            cur = pre + cur;
            pre = temp;
        }else{
            cur = cur;
            pre = cur;
        }
    }
    return cur
}

礼物的最大价值

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

示例 1:
输入: 
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 12
解释: 路径 13521 可以拿到最多价值的礼物
提示:
0 < grid.length <= 200
0 < grid[0].length <= 200

由于只能向下走或向右走,假设 f(i, j) 表示从 [i, j] 位置开始能搜集到的最大礼物,那么:
f(i, j) = grid[i][j] + Math.max(f(i+1, j), f(i, j+1))
参考来源
作者:tank
链接:https://leetcode-cn.com/problems/li-wu-de-zui-da-jie-zhi-lcof/solution/javascript-san-chong-fang-fa-di-gui-er-wei-dp-yi-w/
来源:力扣(LeetCode)

解题思路1

递归(会超时)

var maxValue = function(grid) {
    let rows = grid.length
    if (!rows) return 0
    let cols = grid[0].length
    return recurse(0, 0)

    function recurse(i, j) {
        if (i >= rows || j >= cols) return 0

        return grid[i][j] + Math.max(recurse(i, j+1), recurse(i+1, j))
    }
};

时间复杂度O(2*n)

解题思路2

递归+缓存
缓存相当于剪枝

var maxValue = function(grid) {
    let rows = grid.length
    if (!rows) return 0
    let cols = grid[0].length
    let max = 0
    let map = new Map()
    return recurse(0, 0)
    function recurse(i, j) {
        if (i >= rows || j >= cols) return 0
        if (map.has(i*cols+j)) return map.get(i*cols+j)
        let a = recurse(i+1, j)
        let b = recurse(i, j+1)
        let max = grid[i][j] + Math.max(a, b)
        map.set(i*cols+j, max)
        return max
    }
};

解题思路3

二维动态规划,利用空间换时间
从后往前,以dp[row-1][col-1]为开始,先从最后一行开始循环,也可以从最后一列开始循环,值为dp[row][col-1] + dp[row-1][col] 最后返回dp[0][0]

var maxValue = function(grid) {
    let rows = grid.length
    if (!rows) return 0
    let cols = grid[0].length
    let dp = Array(rows).fill(0).map(i => Array(cols).fill(0))

    for(let i = rows-1; i>=0; i--) {
        for(let j = cols-1; j>=0; j--) {
            let a = i+1 >= rows ? 0 : dp[i+1][j]
            let b = j+1 >= cols ? 0 : dp[i][j+1]

            dp[i][j] = grid[i][j] + Math.max(a, b)
        }
    }

    return dp[0][0]
};

时间复杂度O(nn),空间复杂度O(nn)

解题思路4

一维动态规划
通过分析可以发现,实际上建立联系的为当前循环行或者列与它前一个循环过的行或者列,所以可以用一维数组来代替,一维数组的长度与内循环的长度相同

var maxValue = function(grid) {
    let rows = grid.length
    if (!rows) return 0
    let cols = grid[0].length
    let dp = Array(cols).fill(0)
    for(let i = rows-1; i>=0; i--) {
        for(let j = cols-1; j>=0; j--) {
            let a = i+1 >= rows ? 0 : dp[j]
            let b = j+1 >= cols ? 0 : dp[j+1]
            dp[j] = grid[i][j] + Math.max(a, b)
        }
    }
    return dp[0]
};

时间复杂度O(n*n),空间复杂度O(n)

数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:
输入: [7,5,6,4]
输出: 5

解题思路

归并排序
参考
作者:tang_chao_li_zi
地址
就以arr = [7,5,6,4]这个例子来讲解为什么一遍归并排序就看可以解决逆序对的问题。
按照归并排序的思路,会将arr分解为arrL = [7,5],arrR = [6,4];
继续分解为arrLL = [7], arrLR = [5]; arrRL = [6], arrRR = [4];
自此分解完成。
接下来合并:
假设i为arrLL的数组下标,j为arrLR的数组下标, index为新数组res的下标,初始值都为0
首先arrLL与arrLR合并,因为arrLL[i] > arrLRj,
所以可以说明arrLL中7及其之后的所有数字都大于arrLR中的5,
也就是说7及其之后的所有元素都可以与5组成逆序对,
所以此时7及其之后的所有元素个数(leftLen - i)即我们要的逆序对数,需要添加到结果sum中。即sum += leftLen - 1
(这也就是此算法高效的地方,一次可以查找到好多次的逆序对数,而且不会重复)
合并之后为arrL=[5,7].
根据上述方法将arrRL和arrRR合并为arrR=[4,6];
现在将arrL和arrR合并为arr:
5 > 4,说明5及其之后的所有元素都能与4组成逆序对;所以sum += (leftLen - 1);
5 < 6,正常排序,不做处理
7 > 6,说明7及其之后的所有元素都能与6组成逆序对;所以sum += (leftLen - 1);
7,正常排序,不作处理
最后sum就是所有逆序对的总个数!

var reversePairs = function(nums) {
    let mergeDivided = function(nums){
        if(nums.length < 2) return nums;
        let mid = parseInt(nums.length / 2);
        let left = nums.slice(0,mid);
        let right = nums.slice(mid);
        return mergeStatistics(mergeDivided(left),mergeDivided(right))
    }
    let mergeStatistics = function(left,right){
        let res = []
        let leftLen = left.length;
        let rightLen = right.length;
        let len = leftLen + rightLen;
        for(let index=0,i=0,j=0;index<len;index++){
            if(i>=leftLen) res[index] = right[j++];
            else if(j>=rightLen || left[i]<=right[j]) res[index] = left[i++];
            else if(left[i]>right[j]){
                res[index] = right[j++];
                sum += leftLen-i;
            }
        }
        return res
    }
    let sum = 0;
    mergeDivided(nums);
    return sum;
};

数组中数字出现的次数

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

示例 1:
输入:nums = [4,1,4,6]
输出:[1,6][6,1]
示例 2:
输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10][10,2]

解题思路1

利用数组中的indexOf和lastIndexOf函数

var singleNumbers = function(nums) {
    let res = [];
    for(let i=0;i<nums.length;i++){
        if(nums.indexOf(nums[i]) == nums.lastIndexOf(nums[i])){
            res.push(nums[i])
        }
    }
    return res
};

解题思路2

哈希表

var singleNumber = function(nums) {
    const map = new Map();
    for (let num of nums) {
        if (map.has(num)) map.set(num, map.get(num) + 1);
        else map.set(num, 1);
    }

    for (let [num, times] of map.entries()) {
        if (times === 1) return num;
    }
};

解题思路3

位运算
遍历数组,将所有的数据做异或运算,最后的结果记为num1,由于相同的数异或运算是0,所以最后的结果一定是那两个不同的数据异或出来的结果,而且一定不是0,不为0意味着转化为二进制一定有一个位置上为1。(核心))
通过与(&)运算,选定一个位为1的位置i,再次遍历数组,当前数据二进制位i上为0的数据放在a组,当前数据二进制位i上为1的数据放在b组【思考:相同的数据,一定会被分在同一组,不同的两个数一定会在不同的组(因为异或计算为1表明当前位上的数据一个为0,一个为1)】
最后将a组和b组分别做异或运算就会得到这两个不同的数

const singleNumbers = (nums) => {
  // 计算异或值
  let num1 = 0
  for (let i = 0; i < nums.length; i++) {
    num1 = num1 ^ nums[i];
  }
  // 通过与(&)选定1的位置
  let count = 1
  while((num1&count) === 0) {
    count = count * 2
  }
  // 分组
  let num2 = 0
  let num3 = 0
  for (let i = 0; i < nums.length; i++) {
    const num = nums[i]
    if ((num & count) === 0) {
      num2 = num2^num
    } else {
      num3 = num3^num
    }
  }
  return [num2, num3]
}

数组中数字出现的次数 II

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例 1:
输入:nums = [3,4,3,3]
输出:4
示例 2:
输入:nums = [9,1,7,9,7,9,7]
输出:1

解题思路1

跟上题解题思路1相同

var singleNumber = function(nums) {
    let res = [];
    for(let i=0;i<nums.length;i++){
        if(nums.indexOf(nums[i]) == nums.lastIndexOf(nums[i])){
            res.push(nums[i])
        }
    }
    return res
};

解题思路2

哈希表

var singleNumber = function(nums) {
    const map = new Map();
    for (let num of nums) {
        if (map.has(num)) map.set(num, map.get(num) + 1);
        else map.set(num, 1);
    }

    for (let [num, times] of map.entries()) {
        if (times === 1) return num;
    }
};

解题思路3

位运算
按照位数(最高 32 位)去考虑,这种方法的关键就是找到对于只出现一次的数字,它的哪些二进制位是 1。
整体算法流程如下:
从第 1 位开始
创建掩码(当前位为 1,其他为 0),count 设置为 0
将每个数字和掩码进行&运算,如果结果不为 0,count 加 1
如果 count 整除 3,说明出现一次的数字这一位不是 1;否则,说明出现一次的数字这一位是 1!自己推推便知道了
继续检查第 2 位,一直到 32 位,结束

var singleNumber = function(nums) {
    let res = 0;
    for (let bit = 0; bit < 32; ++bit) {
        let mask = 1 << bit;
        let count = 0;
        for (let num of nums) {
            if (num & mask) ++count;
        }
        if (count % 3) {
            res = res | mask;
        }
    }
    return res;
};

数组中数字出现的次数 III

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了两次。请找出那个只出现一次的数字。

示例 1:
输入:nums = [3,4,,3]
输出:4
示例 2:
输入:nums = [9,1,7,9,7]
输出:1

解题思路

位运算
题目提到了某个元素出现 1 次,其他都是 2 次。这题利用“异或”运算的性质:二进制位相同为 0,不同为 1。所以出现两次的元素,自己和自己异或运算后,就变成了 0.
注意,0 和任意数异或结果都是那个任意数。所以初始值设为 0.

var singleNumber = function(nums) {
    let res = 0;
    for (let num of nums) {
        res = res ^ num;
    }
    return res;
};

和为s的两个数字

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
示例 2:
输入:nums = [10,26,30,31,47,60], target = 40
输出:[10,30] 或者 [30,10]
限制:
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^6

解题思路

双指针
由于是递增数组,将指针 ii 指向数组首位数字,指针 jj 指向数组末位数字。
若两数字之和大于了 target,则指针 jj 往左移一位。
若两数字之和小于了 target,则指针 ii 往右移一位。
若两数字之和等于了 target,返回结果 [i, j][i,j] 即可。

var twoSum = function(nums, target) {
    let left = 0,right = nums.length-1;
    while(left < right){
        if(nums[left] + nums[right] == target) return [nums[left],nums[right]];
        if(nums[left] + nums[right] > target) right--;
        else left++
    }
    return null
};

和为s的连续正数序列

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

示例 1:
输入:target = 9
输出:[[2,3,4],[4,5]]
示例 2:
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]
限制:
1 <= target <= 10^5

解题思路

动态窗口
先把数分解9=1+8=2+7=3+6=4+5,按这种,找到可能组成正确结果的数组,根据数的结构,易知结果可能存在[1,2,3,4,5]中,不难发现数组最后一个数,如果target是偶数就是target/2,如果是奇数就是target/2取整加一,再对找到的数组采用滑动窗口模型,找出答案。

var findContinuousSequence = function (target) {
  let index = target % 2 === 0 ? target / 2 : Math.ceil(target / 2 ) 
  let res = []
  let temp = []
  let sum = 0
  for (let i = 1; i <= index; i++) {
    temp.push(i)
    sum = sum + i
    while (sum > target) {
      sum -= temp[0]
      temp.shift()
    }
    if (sum === target) {
      res.push([...temp]) //注意这里不能res.push([temp])  如果push temp这个变量,会出现覆盖的情况,所以不能push变量
    }
  }
  return res;
};

n个骰子的点数

把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。

示例 1:
输入: 1
输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
示例 2:
输入: 2
输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]
限制:
1 <= n <= 11

解题思路

动态规划
核心计算公式为dp[sum] = dp[sum] + dp[k] * 1/6
相当于前几个骰子的概率加上最后一个骰子的概率
这里的dp[k]不是代表具体的点数概率,而是n个骰子的最小点数到最大点数

var dicesProbability = function(n) {
    let dp = [1/6,1/6,1/6,1/6,1/6,1/6];
    for(let i=2;i<=n;i++){
        let temp = [];
        for(let k=0;k<6;k++){
            for(let j=0;j<dp.length;j++){
                let sum = k + j;
                temp[sum] = (temp[sum] || 0) + dp[j] * 1/6;   //如果temp[sum]存在,则进行累加,若不存在,初始化为0
            }
        }
        dp = temp
    }
    return dp
};

扑克牌中的顺子

从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。

示例 1:
输入: [1,2,3,4,5]
输出: True
示例 2:
输入: [0,0,1,2,5]
输出: True
限制:
数组长度为 5 
数组的数取值为 [0, 13] .

解题思路1

依据题意数组需要满足:
除 0 以外,数组最大数与最小数之差不能超过 4 ;因为长度为 5 的数组里数字若是连续的必定满足这一条件(不理解的同学可以自己试试枚举连续的 5 个数字出来)
除 0 以外,数组中不能有相同的数字
故解题思路为:
要直接获取数组 最大数 与 最小数 ,就必须将数组进行排序
以上条件都需要除去 0 ,故使用了数组的 filter方法 将数组中的 0 过滤掉
获取到 最大数 与 最小数 之后,判断它们的差是否大于 4 ,大于 4 则不符题意,返回 false
遍历一次新数组判断是否有重复的数字,有重复则不符题意,返回 false
以上都判断都通过,说明符合题意,返回 true

var isStraight = function(nums) {
    nums = nums.sort((a,b)=>a-b).filter(item=>item!==0);
    if(nums[nums.length-1] - nums[0] > 4) return false;
    for(let i=0;i<nums.length-1;i++){
        if(nums[i] == nums[i+1]) return false
    };
    return true
};

解题思路2

利用分治的思想
分治思想 五张牌构成顺子的充分条件需要满足

  1. 不重复 使用Set去重
  2. max - min < 5 最大牌值 减去 最小牌值 小于5 且跳过大小王
var isStraight = function(nums) {
    let max = 0,min = 14;
    let set = new Set();
    for(let num of nums){
        if(!num) continue;
        if(set.has(num)) return false;
        set.add(num);
        max = Math.max(max,num);
        min = Math.min(min,num)
    };
    return max - min < 5
};

圆圈中最后剩下的数字

0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

示例 1:
输入: n = 5, m = 3
输出: 3
示例 2:
输入: n = 10, m = 17
输出: 2
限制:
1 <= n <= 10^5
1 <= m <= 10^6

约瑟夫环参考
本质是约瑟夫环问题,核心在于
f(N,M)=(f(N−1,M)+M)%N
f(N,M)表示,N个人报数,每报到M时杀掉那个人,最终胜利者的编号
f(N−1,M)表示,N-1个人报数,每报到M时杀掉那个人,最终胜利者的编号
按照下标来定义,最后的胜利者一定在0处,而上一轮最后的胜利者在0+M处,由于可能会越界,因此需要%N来进行环的拼接

解题思路

var lastRemaining = function(n, m) {
    let pos = 0;
    for(let i=2;i<=n;i++){
        pos = (m+pos)%i
    }
    return pos
};

股票的最大利润

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
限制:
0 <= 数组长度 <= 10^5

解题思路1

切分比较法

var maxProfit = function(prices) {
	if(!prices || !prices.length) return 0
    let max = 0;
    let temp = 0;
    for(let i=0;i<prices.length-1;i++){
        temp = Math.max(...prices.slice(i+1))
        max = Math.max(max,temp-prices[i])
    }
    return max
};

解题思路2

动态规划
核心思路:当前股票价格 - 前i项股票最低值

var maxProfit = function(prices) {
    let max = 0,min = Number.MAX_SAFE_INTEGER;
    if(!prices || !prices.length) return 0;
    for(let i=0;i<prices.length;i++){
        min = Math.min(prices[i],min);
        max = Math.max(max,prices[i]-min)
    }
    return max
};

求1+2+…+n

求 1+2+…+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

示例 1:
输入: n = 3
输出: 6
示例 2:
输入: n = 9
输出: 45
限制:
1 <= n <= 10000

思路参考
和公式位n(n+1)/2

解题思路1

运用对数,将乘法变为加法,除法变成减法

var sumNums = function(n) {
    return Math.round(Math.exp(Math.log(n)+Math.log(n+1)-Math.log(2)))
};

解题思路2

递归,&&逻辑符短路
n && sumNums(n-1) 若n不为0,返回sumNums(n-1),否则返回n,利用这个原理进行和的累加

var sumNums = function(n) {
    return n && sumNums(n-1) + n
};

解题思路3

幂运算加移位
(n ** 2 + n)一定为偶数,右移一位即除2操作

var sumNums = function (n) {
    return (n ** 2 + n) >> 1;
};

不用加减乘除做加法

写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

示例:
输入: a = 1, b = 1
输出: 2
提示:
a, b 均可能是负数或 0
结果不会溢出 32 位整数

位运算实现加减乘除

解题思路

可将和分为非进位和n与进位c两部分 可推导出s(和) = n(非进位和)+ c(进位)
直到没有进位后,非进位和便是和

var add = function(a, b) {
    while (b) {
        let c = (a & b) << 1 // 进位
        a ^= b               // 非进位和
        b = c                // 进位
    }
    return a
};

构建乘积数组

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

示例:
输入: [1,2,3,4,5]
输出: [120,60,40,30,24]
提示:
所有元素乘积之和不会溢出 32 位整数
a.length <= 100000

解题思路

双向遍历
left从左往右遍历,逐个求出从索引0对应值一直乘到 到当前索引的乘积结果;
right从右往左遍历,逐个求出从最后一个值乘到当前值得乘积结果。
在观察left 和 right 数组进行分析生成所需乘积数组b[i]
然后 结果就是b[i] = left[i-1] *right[i+1];

var constructArr = function(a) {
    let left=[];
    let right=[];
    let len = a.length;
    for(let i = 0;i < len ; i++){
        let j=len-i-1;
        if(i == 0){
            left[i] = a[i];
            right[j] = a[j];
        }else{
            left[i] = left[i-1] * a[i];
            right[j] = right[j+1] * a[j]; 
        }
       
    }
    let b=[];
    for(let i=0;i<len;i++){
        if(i==0)b[i]=right[i+1];
        else if(i==len-1)b[i]=left[i-1];
        else b[i] = left[i-1] *right[i+1];
    }
    return b;
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值