题一(21.03.30)
剑指 Offer 03. 数组中重复的数字
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例 1:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
限制:
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域指向第一个结点的节点? 前者的递归过程如下:
题五(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] 是最小值右侧的元素,因此我们可以忽略二分查找区间的右半部分。
第二种情况:numbers[pivot] > numbers[high]。说明 numbers[pivot] 是最小值左侧的元素,因此我们可以忽略二分查找区间的左半部分。
第三种情况:numbers[pivot] === numbers[high]。由于重复元素的存在,我们并不能确定 numbers[pivot] 究竟在最小值的左侧还是右侧,因此我们不能莽撞地忽略某一部分的元素。我们唯一可以知道的是,由于它们的值相同,所以无论 numbers[high] 是不是最小值,都有一个它的“替代品” numbers[pivot],因此我们可以忽略二分查找区间的右端点。
当二分查找结束时,我们就得到了最小值所在的位置。
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"(单词中的字母已标出)。
示例 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 的 二进制串。