每日一题剑指OFFER(JS版)

289 阅读7分钟

题一(21.03.30)

剑指 Offer 03. 数组中重复的数字
找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:

输入:
[2, 3, 1, 0, 2, 5, 3]  
输出:23 

限制:
2 <= n <= 100000

/**
 * @param {number[]} nums
 * @return {number}
 */
var findRepeatNumber = function(nums) {

};

解答一

利用哈希表

1.遍历数组,若当前数字不在哈希表,则添加到哈希表;
2.若当前数字在哈希表中存在,则返回结果。

var findRepeatNumber = function(nums) {
    let map = new Map();
    for(let i of nums){
        if(map.has(i)) return i;
        map.set(i, 1);
    }
    return null;
};

由于使用了哈希表,所以空间复杂度是 O(N)。需要遍历一次,时间复杂度是 O(1)。
Map.prototype.has()
Map.prototype.set()

解法二

使用set

set自动忽略重复元素,遍历数组中元素,若长度未增加,则输出当前元素

var findRepeatNumber = function(nums) {
    const s = new Set()
    for(let i of nums){
        let curLenth = s.size
        s.add(i)
        if(s.size === curLenth) return i
    }
};

时间复杂度最大为O(n),空间复杂度为O(1)。
Set.prototype.size
Set.prototype.add()

解法三

先排序,相邻相同就返回

var findRepeatNumber = function(nums) {
    nums.sort()
    for(let i=0; i<nums.length;){
        if(nums[i++] === nums[i]) return nums[i]
    }
    return null
};

解法四

无脑双for

var findRepeatNumber = function(nums) {
    for(let i=0;i<nums.length-1;i++){
        for(let j=i+1;j<nums.length;j++){
            if(nums[i]==nums[j])
            return nums[i];
        }
    }
    return null;
};

解法五

看书上的,置换,还不太懂

题二(21.03.31)

剑指 Offer 04. 二维数组中的查找
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

示例:

现有矩阵 matrix 如下:

[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]

给定 target = 5,返回 true。

给定 target = 20,返回 false。

限制:
0 <= n <= 1000
0 <= m <= 1000

/**
 * @param {number[][]} matrix
 * @param {number} target
 * @return {boolean}
 */
var findNumberIn2DArray = function(matrix, target) {
    
};

解法一

循环+二分

每一行是递增的,所以可用二分法查找每行

var findNumberIn2DArray = function(matrix, target) {
    let m = matrix.length
    if(!m) return false
    for(let i=0; i<m; i++){
        let item = matrix[i]
        if(target >= item[0]){
            if(bs(item, target)) return true
        } else {
            return false
        }
    }
    return false
};

function bs(arr, t){
    let i = 0, j = arr.length - 1
    while(i <= j){
        let m = Math.floor((i+j)/2)
        if(arr[m] === t) return true
        else if(arr[m] > t) j = m - 1
        else i = m + 1
    }
    return false
}

二分法
时间复杂度O(mlogn)
这样没有充分利用题目的纵向也是递增的, 所以有更加快捷的方法

解法二

从右上角 往左下找

1.从右上角开始遍历
2.当前元素小于目标元素,进入下一行
3.当前元素大于目标元素,向前一列查找

var findNumberIn2DArray = function(matrix, target) {
    const rowNum = matrix.length
    if(!rowNum) return false
    const colNum = matrix[0].length
    if(!colNum) return false
    let row = 0, col = colNum - 1
    while(row < rowNum && col >= 0){
        if(matrix[row][col] === target) return true
        else if(matrix[row][col] > target) --col
        else ++row
    }
    return false
};

时间复杂度是 O(M+N),空间复杂度是 O(1)。其中 M 和 N 分别代表行数和列数。

题三(21.04.01)

剑指 Offer 05. 替换空格
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

示例 1:

输入:s = "We are happy."
输出:"We%20are%20happy."

限制:
0 <= s 的长度 <= 10000

/**
 * @param {string} s
 * @return {string}
 */
var replaceSpace = function(s) {

};

解法一

正则表达式

var replaceSpace = function (s) {
    return s.replace(/\s/g,"%20")
};

String.prototype.replace()
正则表达式30分钟入门教程

解法二

字符串 split 和数组 join 方法

var replaceSpace = function(s) {
      if (typeof s == "string" && s.length >= 0 && s.length <= 10000) {
        return s.split(' ').join('%20');
      }
      return '';
}

String.prototype.split()
Array.prototype.join()

题四(21.04.02)

剑指 Offer 06. 从尾到头打印链表
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

示例 1:

输入:head = [1,3,2]
输出:[2,3,1]

限制:

0 <= 链表长度 <= 10000

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @return {number[]}
 */
var reversePrint = function(head) {

};

解法一

unshift 方法

var reversePrint = function(head) {
    let arr = []
    while(head){
        arr.unshift(head.val)
        head = head.next
    }
    return arr
};

Array.prototype.unshift()
unshift 方法的复杂度可能有点高,而且不常见。

解法三

递归

var reversePrint = function(head) {
    if(!head) return []
    let p = head
    let arr = reversePrint(head.next)
    arr.push(p.val)
    return arr
};

问题:题目中的头节点到底指的是第一个节点,还是分配的一个数据域为null,next域指向第一个结点的节点? 前者的递归过程如下:

image.png

题五(21.04.03)

剑指 Offer 07. 重建二叉树
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

例如,给出

前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]

返回如下的二叉树:

    3
   / \
  9  20
    /  \
   15   7

限制:

0 <= 节点个数 <= 5000

理解二叉树的三种遍历

题六(21.04.04)

剑指 Offer 09. 用两个栈实现队列
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

示例 1:

输入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
输出:[null,null,3,-1]

示例 2:

输入:
["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]

提示:

  • 1 <= values <= 10000
  • 最多会对 appendTail、deleteHead 进行 10000 次调用
var CQueue = function() {

};

/** 
 * @param {number} value
 * @return {void}
 */
CQueue.prototype.appendTail = function(value) {

};

/**
 * @return {number}
 */
CQueue.prototype.deleteHead = function() {

};

/**
 * Your CQueue object will be instantiated and called as such:
 * var obj = new CQueue()
 * obj.appendTail(value)
 * var param_2 = obj.deleteHead()
 */

解题思路

队列是先入先出,栈是后入先出。一个为入队栈,一个为出队栈,各自负责入队和出队。 入队操作,直接压入入队栈即可,出队操作需要优先检查出队栈是否有数据,若无,需要从入队栈倒入后再操作。

var CQueue = function() {
   this.stackA = []
   this.stackB = []
};

/** 
* @param {number} value
* @return {void}
*/
CQueue.prototype.appendTail = function(value) {
   this.stackA.push(value)
};

/**
* @return {number}
*/
CQueue.prototype.deleteHead = function() {
   if(this.stackB.length){
       return this.stackB.pop()
   } else {
       while(this.stackA.length){
           this.stackB.push(this.stackA.pop())
       }
       if(!this.stackB.length){
           return -1
       } else {
           return this.stackB.pop()
       }
   }
};

代码流程图:

题七(21.04.05)

剑指 Offer 10- I. 斐波那契数列
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:

F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.

斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入:n = 2
输出:1

示例 2:

输入:n = 5
输出:5

提示:

  • 0 <= n <= 100
/**
 * @param {number} n
 * @return {number}
 */
var fib = function(n) {

};

解法一(错误解法)

直接递归(爆栈,超时)

var fib = function(n) {
    if(n<=1) return n;
    return ( fib(n-1) + fib(n-2) ) % 1000000007;
};

时间复杂度 O(2n)

解法二

动态规划 + 备忘录

将已经计算过的值存起来,用空间换取时间。

let arr = [0,1]
var fib = function(n) {
    if(n<1) return 0;
    if(n===1 || n===2) return 1;
    if(arr[n]) return arr[n];
    arr[n] = fib(n-1) + fib(n-2);
    return arr[n] % (1e9 +7);
};

时间复杂度 O(n),但空间复杂度略高。

解法三

动态规划 + 自底向下循环迭代

我们可以使用动态规划,将每次前两数之和存起来,便于下次直接使用,这样子,我们就把一个栈溢出的问题,变为了单纯的数学加法,大大减少了内存的压力。

var fib = function(n) {
    let n1 = 0, n2 = 1, sum
    for(let i=0; i<n; i++){
        sum = (n1 + n2) % 1000000007
        n1 = n2
        n2 = sum
    }
    return n1
};

自下而上 for 循环
时间复杂度 O(n),空间复杂度 O(1),最优解法
动态规划专项练习
动态规划-知乎专栏
公众号:labuladong
迭代与循环的区别

题八(21.04.06)

剑指 Offer 10- II. 青蛙跳台阶问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入:n = 2
输出:2

示例 2:

输入:n = 7
输出:21

示例 3:

输入:n = 0
输出:1

提示:

  • 0 <= n <= 100
/**
 * @param {number} n
 * @return {number}
 */
var numWays = function(n) {

};

解答

题九(21.04.07)

剑指 Offer 11. 旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。

示例 1:

输入:[3,4,5,1,2]
输出:1

示例 2:

输入:[2,2,2,0,1]
输出:0
/**
 * @param {number[]} numbers
 * @return {number}
 */
var minArray = function(numbers) {

};

解法一

看规律(暴力解法)

遍历数组,判断数组元素是否递增,如果遍历完一直是递增则返回首个元素,如果不是则返回首个不是递增的元素,也就是numbers[i]<numbers[i-1]不成立,返回numbers[i]。

var minArray = function(numbers) {
    for(let i=0; i<numbers.length; i++){
        if(numbers[i] > numbers[i+1]) return numbers[i+1]
    }
    return numbers[0]
};

但该方法有缺陷,如果旋转次数过多,有多个递增区间,无法记录最小值,需要一个变量存储,进行Min的比较。 时间复杂度:O(n)

解法二

二分查找

我们考虑数组中的最后一个元素 x:在最小值右侧的元素,它们的值一定都小于等于 x;而在最小值左侧的元素,它们的值一定都大于等于 x。因此,我们可以根据这一条性质,通过二分查找的方法找出最小值。
第一种情况:numbers[pivot] < numbers[high]。说明 numbers[pivot] 是最小值右侧的元素,因此我们可以忽略二分查找区间的右半部分。

image.png 第二种情况:numbers[pivot] > numbers[high]。说明 numbers[pivot] 是最小值左侧的元素,因此我们可以忽略二分查找区间的左半部分。

image.png 第三种情况:numbers[pivot] === numbers[high]。由于重复元素的存在,我们并不能确定 numbers[pivot] 究竟在最小值的左侧还是右侧,因此我们不能莽撞地忽略某一部分的元素。我们唯一可以知道的是,由于它们的值相同,所以无论 numbers[high] 是不是最小值,都有一个它的“替代品” numbers[pivot],因此我们可以忽略二分查找区间的右端点

image.png 当二分查找结束时,我们就得到了最小值所在的位置。

var minArray = function(numbers) {
    let low = 0
    let high = numbers.length - 1
    while(low < high){
        const pivot = low + Math.floor((high - low)/2)
        if(numbers[pivot] < numbers[high]) high = pivot
        else if(numbers[pivot] > numbers[high]) low = pivot + 1
        else high--
    }
    return numbers[low]
};
  • 时间复杂度:平均时间复杂度为 O(logn),其中 n 是数组 numbers 的长度。如果数组是随机生成的,那么数组中包含相同元素的概率很低,在二分查找的过程中,大部分情况都会忽略一半的区间。而在最坏情况下,如果数组中的元素完全相同,那么 while 循环就需要执行 n 次,每次忽略区间的右端点,时间复杂度为 O(n)。

  • 空间复杂度:O(1)O(1)。 疑问:为什么第一种情况 numbers[pivot] < numbers[high] 时,缩小区间到 [low, pivot] ;而第二种情况 numbers[pivot] > numbers[high] 时,缩小区间到 [pivot+1, high] , 而不是 [pivot, high] ?为什么第二种情况要加 1 呢?
    解答:因为第二种情况下pivot不可能为最小值,而第一种情况下pivot可能为最小值。1.不加1可能死循环 2.第二种情况答案只会在[pivot+1,high]里,numbers[pivot]不可能是答案。试想low=2,high=3,则pivot=2,若numbers[pivot]>numbers[high],则陷入死循环。

题十(21.04.08)

剑指 Offer 12. 矩阵中的路径
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

例如,在下面的 3×4 的矩阵中包含单词 "ABCCED"(单词中的字母已标出)。

image.png

示例 1:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true

示例 2:

输入:board = [["a","b"],["c","d"]], word = "abcd"
输出:false

提示:

  • 1 <= board.length <= 200
  • 1 <= board[i].length <= 200
  • board 和 word 仅由大小写英文字母组成
/**
 * @param {character[][]} board
 * @param {string} word
 * @return {boolean}
 */
var exist = function(board, word) {

};

解题思路

DFS(回溯算法)

/**
 * @param {character[][]} board
 * @param {string} word
 * @return {boolean}
 */
var exist = function(board, word) {
    let row = board.length
    let col = board[0].length

    var dfs = function(i, j, board, word, index){
        if(i<0 || i>=row || j<0 || j>=col || board[i][j] !== word[index]) return false // 判断不符合条件
        if(index === word.length-1) return true //遍历完了
        let temp = board[i][j] //记录到board的值
        board[i][j] = '-' //锁上,因为后续的递归是4个方向上的,无法保证上一个方向的值
        let res =  dfs(i - 1,j,board,word,index + 1) || dfs(i + 1,j,board,word,index + 1) || dfs(i,j - 1,board,word,index + 1) || dfs(i,j + 1,board,word,index + 1) 
        board[i][j] = temp //恢复现场
        return res
    }

    for(let i=0; i<row; i++){
        for(let j=0; j<col; j++){
            if(dfs(i, j, board, word, 0)) return true
        }
    }
    return false
}

疑问:这个题如果不加这行恢复现场的代码,对这题结果有影响吗?(不加的话是不是会改变题目给出的board?)
PS:原答案代码中是 j>col。作者回答:判断i,j在矩阵范围内,j也应该要有j>=col,但是后面 board[i][j] !== word[index]把j=col卡掉了,此时的board[i][j]='-'。
回溯算法详解

题十一(21.04.09)

剑指 Offer 13. 机器人的运动范围
地上有一个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
/**
 * @param {number} m
 * @param {number} n
 * @param {number} k
 * @return {number}
 */
var movingCount = function(m, n, k) {

};

解法一

bfs

var movingCount = function(m, n, k) {
    //位数和
    function bitSum(num){
        let res = 0
        while(num){
            res = res + (num % 10)
            num = Math.floor(num / 10)
        }
        return res
    }

    //方向数组
    const directionAry = [
        [0, 1], //右
        [1, 0] //下
    ]

    //已经走过的坐标
    let set = new Set(['0,0'])

    //将遍历的坐标队列,题意要求从[0, 0]开始走
    let queue = [[0, 0]]

    //遍历队列中的坐标
    while(queue.length){
        //移除队首坐标
        let [x, y] = queue.shift()

        //遍历方向
        for(let i=0; i<2; i++){
            let offsetX = x + directionAry[i][0]
            let offsetY = y + directionAry[i][1]

            //临界值判断
            if(offsetX<0 || offsetX>=m || offsetY<0 || offsetY>=n || bitSum(offsetX)+bitSum(offsetY) > k || set.has(`${offsetX},${offsetY}`)) continue

            //走过的格子就不再纳入统计
            set.add(`${offsetX},${offsetY}`)

            //将该坐标加入队列(因为这个坐标的四周没有走过,需要纳入下次的遍历)
            queue.push([offsetX, offsetY])
        }
    }
    //走过坐标的个数就是
    return set.size
};

这题的 Set 运用的很巧妙,同时要注意只需要遍历右和下两个方向就行
BFS 算法框架套路详解
ES6模板字符串

解法二

DFS

题十二(21.04.10)

剑指 Offer 15. 二进制中1的个数
请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。

示例 1:

输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'

示例 2:

输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'

示例 3:

输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'

提示:

  • 输入必须是长度为 32 的 二进制串。