字符串
剑指 Offer 58 - I. 翻转单词顺序
Difficulty: 简单
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. ",则输出"student. a am I"。
示例 1:
输入: "the sky is blue"
输出: "blue is sky the"
示例 2:
输入: " hello world! "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
示例 3:
输入: "a good example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
说明:
- 无空格字符构成一个单词。
- 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
- 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
注意:本题与主站 151 题相同:
注意:此题对比原题有改动
Solution
比较简单的一道题,合理运用好库函数就可解出来。
split()
以空格为分隔,将单词放入数组中filter(Boolean)
由于以空格分隔,可能会有空字符串放入,所以使用过滤将空字符串过滤reverse()
将数组翻转join(' ')
用空格将数组拼接成字符串
Language: 全部题目
var reverseWords = function(s) {
return s
.split(" ")
.filter(Boolean)
.reverse()
.join(" ");
};
剑指 Offer 58 - II. 左旋转字符串
Difficulty: 简单
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
示例 1:
输入: s = "abcdefg", k = 2
输出: "cdefgab"
示例 2:
输入: s = "lrloseumgh", k = 6
输出: "umghlrlose"
限制:
1 <= k < s.length <= 10000
Solution
Language: 全部题目
方案一
var reverseLeftWords = function(s, n) {
let res = ''
for(let i=n;i<n+s.length;i++ ){
res += s.charAt(i%s.length)
}
return res
};
方案二
var reverseLeftWords = function(s, n) {
return s.substring(n) + s.substring(0,n)
剑指 Offer 67. 把字符串转换成整数
Difficulty: 中等
写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。
首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。
当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。
该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。
注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。
在任何情况下,若函数不能进行有效的转换时,请返回 0。
说明:
假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,请返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。
示例 1:
输入: "42"
输出: 42
示例 2:
输入: " -42"
输出: -42
解释: 第一个非空白字符为 '-', 它是一个负号。
我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。
示例 3:
输入: "4193 with words"
输出: 4193
解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。
示例 4:
输入: "words and 987"
输出: 0
解释: 第一个非空字符是 'w', 但它不是数字或正、负号。
因此无法执行有效的转换。
示例 5:
输入: "-91283472332"
输出: -2147483648
解释: 数字 "-91283472332" 超过 32 位有符号整数范围。
因此返回 INT_MIN (−231) 。
注意:本题与主站 8 题相同:
Solution
方案一
利用显式类型转换,将字符串转换为number
- 首部空格使用
trim()
进行去除 - 去除掉首部空格后,如果第一个字符为
-
,则将符号变量signal
置为-1
,否则就时1
- 接下来进入循环赋值给
newStr
,使用显式类型转换,如果遇到字母或者为空则推出循环 - 接下来就对返回值进行判断
Language: 全部题目
var strToInt = function(str) {
str = str.trim();
if (!str.length) return 0;
// 使用变量存储 数值范围
var max = Math.pow(2, 31) - 1,
//新的数字类型默认为0
newNum = 0,
//默认从字符串的第二位开始
i = 1,
//默认符号位为1
signal = 1;
//判断符号位是不是 -
if (str[0] === '-') signal = -1;
//如果符号位是不是 + 如果不是则从第一位开始
else if (str[0] != '+') i = 0;
//循环判断之后的符号是否为数字
while (i < str.length) {
//如果遇到数字就退出循环
if ((+str[i]).toString() === 'NaN' || str[i] === ' ') break;
newNum = newNum * 10 + +str[i];
i++;
}
return newNum > max
? signal === 1
? signal * max
: signal * max - 1
: signal * newNum;
};
方案二
使用正则表达式更轻松的取出值进行判断
- 首部空格使用
trim()
进行去除 - 构建正则表达式
- 正负号使用
^(+|-)?
进行判断并且,?
判断1次或0次 - 数值使用
[0-9]+
,多次判断数值
- 正负号使用
var strToInt = function(str) {
str = str.trim()
let max = Math.pow(2,31)-1
let pattern = /^(+|-)?[0-9]+/
let tmp = pattern.exec(str)
if(tmp){
let newNum = Number(tmp[0])
if(newNum > max) return max
else if(newNum < -max-1) return -max-1
return newNum
}
return 0
};
数组
剑指 Offer 04. 二维数组中的查找
Difficulty: 简单
在一个 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
注意:本题与主站 240 题相同:
Solution
- 将左下角的数作为标记
- 如果
target
小于当前左下角的数,那么寻找区域就会在当前行的上方,所以行数-1
- 如果
target
大于当前左下角的数,那么寻找区域就会在当前行的右方,所以列数+1
- 如果超过了边界,则返回false
Language: 全部题目
var findNumberIn2DArray = function(matrix, target) {
let row = matrix.length -1 ,col=0
while(row>=0 && col < matrix[0].length){
if(matrix[row][col] > target) row--
else if(matrix[row][col] < target) col++
else return true
}
return false
};
剑指 Offer 53 - I. 在排序数组中查找数字 I
Difficulty: 简单
统计一个数字在排序数组中出现的次数。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: 0
限制:
0 <= 数组长度 <= 50000
注意:本题与主站 34 题相同(仅返回值不同):
Solution
方案一
- 初始化: 左边界 i = 0i=0 ,右边界 j = len(nums) - 1 。
- 循环二分: 当闭区间
[i, j][i,j]
无元素时跳出;- 计算中点 m = (i + j) / 2 (向下取整);
- 若 nums[m] < target ,则 target 在闭区间
[m + 1, j]
中,因此执行 i = m + 1; - 若 nums[m] > target,则 target 在闭区间
[i, m - 1]
中,因此执行 j = m - 1; - 若 nums[m] = target ,则右边界 right 在闭区间
[m+1, j]
中;左边界 left 在闭区间[i, m-1]
中。因此分为以下两种情况:- 若查找 右边界 right,则执行 i = m + 1 ;(跳出时 i指向右边界)
- 若查找 左边界 left ,则执行 j = m - 1 ;(跳出时 j 指向左边界)
- 返回值: 应用两次二分,分别查找 right 和 left,最终返回 right - left - 1 即可。
Language: 全部题目
var search = function(nums, target) {
let i = 0 , j = nums.length-1
//查找右边界
while(i<=j){
let mid = Math.floor((i+j)/2)
if(nums[mid] <= target ) i=mid+1
else j=mid - 1
}
let right = i
i=0,j=nums.length-1
while(i<=j){
let mid = Math.floor((i+j)/2)
if(nums[mid] < target) i=mid+1
else j=mid-1
}
let left = j
return right-left-1
};
方案二
优化方案一版本
var search = function(nums, target) {
return helper(nums,target) - helper(nums,target-1)
};
var helper = function(nums,tar){
let i =0,j=nums.length-1
while(i<=j){
let mid = Math.floor((i+j)/2)
if(nums[mid] <= tar) i=mid+1
else j=mid-1
}
return i
}
剑指 Offer 03. 数组中重复的数字
Difficulty: 简单
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例 1:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
限制:
2 <= n <= 100000
Solution
使用原地hash方法
在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内 。 此说明含义:数组元素的 索引 和 值 是 一对多 的关系。
因此,可遍历数组并通过交换操作,使元素的 索引 与 值 一一对应(即 nums[i] = inums[i]=i )。因而,就能通过索引映射对应的值,起到与字典等价的作用。
Language: 全部题目
var findRepeatNumber = function(nums) {
const length = nums.length
for(let i = 0 ; i<length ; ++i){
//循环使得当前值和 索引 一一对应
while((num = nums[i]) !== i){
//判断 当前值 和 当前值所需要在的索引里的值是否相等
if(num === nums[num])
return num;
//不相同则将其互换
[nums[num],nums[i]] = [nums[i],nums[num]]
}
}
};
剑指 Offer 53 - II. 0~n-1中缺失的数字
Difficulty: 简单
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
示例 1:
输入: [0,1,3]
输出: 2
示例 2:
输入: [0,1,2,3,4,5,6,7,9]
输出: 8
限制:
1 <= 数组长度 <= 10000
Solution
使用二分查找
Language: 全部题目
var missingNumber = function(nums) {
let left =0,right = nums.length-1
while(left<=right){
let mid = left+Math.floor((right-left)/2)
if(nums[mid] > mid ) right=mid-1
else left =mid+1
}
return left
};
剑指 Offer 53 - II. 0~n-1中缺失的数字
Difficulty: 简单
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
示例 1:
输入: [0,1,3]
输出: 2
示例 2:
输入: [0,1,2,3,4,5,6,7,9]
输出: 8
限制:
1 <= 数组长度 <= 10000
Solution
- 如果一条边从头遍历到底,则下一条边遍历的起点随之变化
- 选择不遍历到底,可以减小横向、竖向遍历之间的影响
- 一轮迭代遍历一个“圈”,然后 4 条边的两端同时收缩 1
- 一层层向内处理,按顺时针依次遍历:上、右、下、左层
- 不再形成“环”了,就会剩下一行或一列,然后单独判断
Language: 全部题目
var spiralOrder = function(matrix) {
if(matrix.length===0) return []
let left = 0, right = matrix[0].length-1, top = 0, bottom = matrix.length-1
let res = []
while(left<right && top <bottom){
for(let i=left;i<right;i++) res.push(matrix[top][i]) //上层
for(let i=top;i<bottom;i++) res.push(matrix[i][right]) //右侧
for(let i=right;i>left;i--) res.push(matrix[bottom][i]) //下层
for(let i=bottom;i>top;i--) res.push(matrix[i][left]) //左侧
left++
right--
top++
bottom--
}
if(top==bottom)
for(let i =left;i<=right;i++) res.push(matrix[top][i])
else if(right==left)
for(let i=top;i<=bottom;i++) res.push(matrix[i][left])
return res
};
树
剑指 Offer 34. 二叉树中和为某一值的路径
Difficulty: 中等
输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。
示例:
给定如下二叉树,以及目标和 sum = 22
,
5
/
4 8
/ /
11 13 4
/ /
7 2 5 1
返回:
[
[5,4,11,2],
[5,8,4,5]
]
提示:
节点总数 <= 10000
注意:本题与主站 113 题相同:
Solution
通过DFS,先序遍历树,将root.val
放入path
中,通过回溯方法,最后将符合条件的path
放入res
中
需要注意一点的是:
如果在放入res的时候直接放入path
,这是将引用直接放入,之后修改path
的话,res
里的数据也会被修改。
使用[...path]
结构赋值,将内容复制一遍。
Language: 全部题目
var pathSum = function(root, sum) {
let res = [],path = [];
let dfs = (root,sum) => {
if(!root ) return ;
path.push(root.val)
sum = sum-root.val;
if(sum==0 && !root.left && !root.right){
//使用解构赋值 将内容浅复制一遍放入结果中。
res.push([...path])
}
dfs(root.left,sum)
dfs(root.right,sum)
path.pop()
}
dfs(root,sum)
return res
};
剑指 Offer 07. 重建二叉树
Difficulty: 中等
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/
9 20
/
15 7
限制:
0 <= 节点个数 <= 5000
注意:本题与主站 105 题重复:
Solution
前序遍历特点: 节点按照 [ 根节点 | 左子树 | 右子树 ]
排序,以题目示例为例:[ 3 | 9 | 20 15 7 ]
中序遍历特点: 节点按照[ 左子树 | 根节点 | 右子树 ]
排序,以题目示例为例:[ 9 | 3 | 15 20 7 ]
根据题目描述输入的前序遍历和中序遍历的结果中都不含重复的数字,其表明树中每个节点值都是唯一的。
根据以上特点,我们得知:
- 前序遍历首个元素为第一个
root
元素 - 根据前序遍历,我们可以知道
根节点
、左子树的根节点
以及左子树之下的节点
- 根据前序遍历,我们可以在中序遍历中找到
左子树下的节点的数目
以及右子树的下的节点数目
同理可得其子树,这种将大问题转为相同的小问题我们可以采用递归
的方式进行计算。
Language: 全部题目
var buildTree = function(preorder, inorder) {
//使用map存储中序遍历,以便根据前序遍历找到根节点
const map = new Map();
//循环存入map中
for(let i=0;i<inorder.length;i++){
map.set(inorder[i],i)
}
//进行递归
return helper(preorder,map,0,0,inorder.length-1)
};
// 传入参数: 前序遍历,map,前序中根节点索引,中序遍历左边界,中序遍历右边界
var helper= (preorder,map,pre_idx,in_left,in_right) =>{
if(in_left>in_right) return null;
// 将当前节点作为根节点进行递归
const root = new TreeNode(preorder[pre_idx])
// 从前序遍历中获得根节点的值 匹配中序遍历的索引
const i = map.get(preorder[pre_idx])
//左子树的节点就是 前序遍历索引+1 进行推进
root.left = helper(preorder,map,pre_idx+1,in_left,i-1)
//右子树需要计算 前序遍历根节点的位置 = 当前前序遍历节点+(左子树的长度)
root.right = helper(preorder,map,pre_idx+(i-in_left)+1,i+1,in_right)
return root;
}
剑指 Offer 34. 二叉树中和为某一值的路径
Difficulty: 中等
输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。
示例:
给定如下二叉树,以及目标和 sum = 22
,
5
/
4 8
/ /
11 13 4
/ /
7 2 5 1
返回:
[
[5,4,11,2],
[5,8,4,5]
]
提示:
节点总数 <= 10000
注意:本题与主站 113 题相同:
Solution
回溯进行计算即可
参数解读:
path
:存入路径res
:将符合结果的path
放入res
Language: 全部题目
var pathSum = function(root, sum) {
let res = [],path = [];
// 进行DFS 将根节点以及总数传入
let dfs = (root,sum) => {
if(!root ) return ;
// 先把 节点值存入path中
path.push(root.val)
sum = sum-root.val;
if(sum==0 && !root.left && !root.right){
res.push([...path])
}
dfs(root.left,sum)
dfs(root.right,sum)
path.pop()
}
dfs(root,sum)
return res
};
剑指 Offer 27. 二叉树的镜像
Difficulty: 简单
请完成一个函数,输入一个二叉树,该函数输出它的镜像。
例如输入:
4 / 2 7 / / 1 3 6 9
镜像输出:
4 / 7 2 / / 9 6 3 1
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
限制:
0 <= 节点个数 <= 1000
注意:本题与主站 226 题相同:
Solution
方法一:递归法
根据二叉树镜像的定义,考虑递归遍历(dfs)二叉树,交换每个节点的左 / 右子节点,即可生成二叉树的镜像。
递归解析:
终止条件: 当节点 rootroot 为空时(即越过叶节点),则返回 nullnull ;
递推工作:
初始化节点 tmptmp ,用于暂存 rootroot 的左子节点;
开启递归 右子节点 mirrorTree(root.right)mirrorTree(root.right) ,并将返回值作为 rootroot 的 左子节点 。
开启递归 左子节点 mirrorTree(tmp)mirrorTree(tmp) ,并将返回值作为 rootroot 的 右子节点 。
返回值: 返回当前节点 rootroot ;
方法二:辅助栈
- 使用栈,将节点放入栈中
- 左右节点交换
- 将当前节点的左右子节点放入栈中
Language: 全部题目
var mirrorTree = function(root) {
if(root){
let stack = [];
stack.push(root)
while(stack.length > 0 ){
let node = stack.pop();
let temp = node.left;
node.left = node.right;
node.right=temp;
if(node.left){
stack.push(node.left)
}
if(node.right){
stack.push(node.right)
}
}
}
return root;
};
236. 二叉树的最近公共祖先
Difficulty: 中等
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
说明:
- 所有节点的值都是唯一的。
- p、q 为不同节点且均存在于给定的二叉树中。
Solution
我们假定 root
节点是p、q
节点的公共祖先节点。通过递归遍历根节点的左子树、右子树,返回其搜寻到的节点结果,共有三种结果:
left
和right
都返回空,则p、q
节点并不存在于树中。left
和right
都不为空,p、q
都在根节点的异侧,则直接返回根节点。left
和right
其中之一为空:p、q
不在同一子树中,其中之一指向一个子树中,此时right
、left
指向其p、q
p、q
都在同一方向子树中,那么right
、left
指向其公共祖先。
Language: 全部题目
var lowestCommonAncestor = function(root, p, q) {
if(root == null || root == p || root == q ) return root;
let left = lowestCommonAncestor(root.left,p,q);
let right = lowestCommonAncestor(root.right,p,q);
if(left == null ) return right;
if(right == null) return left;
return root;
};
剑指 Offer 54. 二叉搜索树的第k大节点
Difficulty: 简单
给定一棵二叉搜索树,请找出其中第k大的节点。
示例 1:
输入: root = [3,1,4,null,2], k = 1
3
/
1 4
2
输出: 4
示例 2:
输入: root = [5,3,6,2,4,null,null,1], k = 3
5
/
3 6
/
2 4
/
1
输出: 4
限制:
1 ≤ k ≤ 二叉搜索树元素个数
Solution
二叉搜索树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
第K大节点:树中值从大到小排列后,第K大的值
由于 二叉搜索树的特性我们 中序遍历 ** 得到的其从小到大的排列,我们可以给他反转一下,就可以让其从大到小**,然后输出指定值。
Language: 全部题目
var kthLargest = function(root, k) {
res=helper(root)
return res.reverse()[k-1];
};
let res = [];
let helper = (root) =>{
if(!root) return ;
helper(root.left)
res.push(root.val);
helper(root.right)
return res;
}
剑指 Offer 26. 树的子结构
Difficulty: 中等
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
例如:
给定的树 A:
3 / 4 5 / 1 2
给定的树 B:
4 / 1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。
示例 1:
输入:A = [1,2,3], B = [3,1]
输出:false
示例 2:
输入:A = [3,4,5,1,2], B = [4,1]
输出:true
限制:
0 <= 节点个数 <= 10000
Solution
- 首先判断其
主结构
与子结构
是否相同,不相同则直接返回false
- 对
主结构
与子结构
进行递归判断:- 如果
子结构
为空,则可以返回true
- 如果
主结构
为空或者主结构
与子结构
值不相同,则传回false
- 如果
- 继续向下递归判断。
Language: 全部题目
var isSubStructure = function(A, B) {
return (A!==null && B!==null) && (helper(A,B) || isSubStructure(A.left , B) || isSubStructure(A.right , B)) ;
};
var helper = (a , b) => {
if(!b) return true;
if(!a || a.val !== b.val) return false;
return helper(a.left,b.left) && helper(a.right ,b.right)
}
剑指 Offer 32 - I. 从上到下打印二叉树
Difficulty: 中等
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
例如:
给定二叉树: [3,9,20,null,null,15,7]
,
3
/
9 20
/
15 7
返回:
[3,9,20,15,7]
提示:
节点总数 <= 1000
Solution
通过队列进行层序遍历按照顺序进行放入打印。
shift
:删除第一个字符,并返回字符
push
:增加字符到最后一个字符
Language: 全部题目
var levelOrder = function(root) {
if(!root) return [];
const data = [];
const queue = [root];
while(queue.length){
let temp = queue.shift();
data.push(temp.val);
if(temp.left) queue.push(temp.left);
if(temp.right) queue.push(temp.right);
}
return data;
};
剑指 Offer 32 - II. 从上到下打印二叉树 II
Difficulty: 简单
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
例如:
给定二叉树: [3,9,20,null,null,15,7]
,
3
/
9 20
/
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
提示:
节点总数 <= 1000
注意:本题与主站 102 题相同:
Solution
与1⃣️
不同的是增加了一个临时数组,最后向结果数组中加临时数组。
Language: 全部题目
var levelOrder = function(root) {
if(!root) return [];
const res = [];
const queue = [root];
while(queue.length){
const data = []
for(let i = queue.length;i>0;i--){
let temp = queue.shift();
data.push(temp.val)
temp.left && queue.push(temp.left);
temp.right && queue.push(temp.right);
}
res.push([...data])
}
return res;
};
剑指 Offer 32 - III. 从上到下打印二叉树 III
Difficulty: 中等
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
例如:
给定二叉树: [3,9,20,null,null,15,7]
,
3
/
9 20
/
15 7
返回其层次遍历结果:
[
[3],
[20,9],
[15,7]
]
提示:
节点总数 <= 1000
Solution
通过队列进行层序遍历,然后根据奇偶数
的不同选择从临时队列的从前进入海时从后进入队列。
Language: 全部题目
var levelOrder = function(root) {
if(!root) return [];
const res = [];
const queue = [root];
while(queue.length){
let data =[];
for(let i=queue.length;i>0;i--){
let temp = queue.shift();
if(res.length % 2 == 0) data.push(temp.val);
else{
data.unshift(temp.val);
}
temp.left && queue.push(temp.left)
temp.right && queue.push(temp.right)
}
res.push([...data])
}
return res;
};
剑指 Offer 28. 对称的二叉树
Difficulty: 简单
请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1 / 2 2 / / 3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1 / 2 2 3 3
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false
限制:
0 <= 节点个数 <= 1000
注意:本题与主站 101 题相同:
Solution
对左右子树同时进行递归,一旦左右子树不同或者值不相同则直接返回false。
Language: 全部题目
var isSymmetric = function(root) {
return root == null ? true : helper(root.left,root.right)
};
var helper= (left, right) =>{
if(!left && !right ) return true;
if(!left || !right ||left.val !== right.val) return false;
return helper(left.left , right.right) && helper(left.right , right.left)
}
栈
剑指 Offer 59 - II. 队列的最大值
Difficulty: 中等
请定义一个队列并实现函数 max_value
得到队列里的最大值,要求函数max_value
、push_back
和 pop_front
的均摊时间复杂度都是O(1)。
若队列为空,pop_front
和 max_value
需要返回 -1
示例 1:
输入:
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
示例 2:
输入:
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]
限制:
1 <= push_back,pop_front,max_value的总操作数 <= 10000
1 <= value <= 10^5
Solution
使用单调递减队列进行最大值进行管理。
具体方法是使用一个双端队列 dequedeque,在每次入队时,如果 dequedeque 队尾元素小于即将入队的元素 valuevalue,则将小于 valuevalue 的元素全部出队后,再将 valuevalue 入队;否则直接入队。
双端队列队首就是当前队列最大值
Language: 全部题目
var MaxQueue = function() {
this.queue1 = [];
this.queue2 = [];
};
/**
* @return {number}
*/
MaxQueue.prototype.max_value = function() {
if (this.queue2.length) {
return this.queue2[0];
}
return -1;
};
/**
* @param {number} value
* @return {void}
*/
MaxQueue.prototype.push_back = function(value) {
this.queue1.push(value);
while (this.queue2.length && this.queue2[this.queue2.length - 1] < value) {
this.queue2.pop();
}
this.queue2.push(value);
};
/**
* @return {number}
*/
MaxQueue.prototype.pop_front = function() {
if (!this.queue1.length) {
return -1;
}
const value = this.queue1.shift();
if (value === this.queue2[0]) {
this.queue2.shift();
}
return value;
};
/**
* Your MaxQueue object will be instantiated and called as such:
* var obj = new MaxQueue()
* var param_1 = obj.max_value()
* obj.push_back(value)
* var param_3 = obj.pop_front()
*/
剑指 Offer 30. 包含min函数的栈
Difficulty: 简单
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
示例:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.min(); --> 返回 -2.
提示:
- 各函数的调用总次数不超过 20000 次
注意:本题与主站 155 题相同:
Solution
我们使用两个栈进行模拟能够取到栈中最小值的函数,一个栈是正常栈,一个辅助栈是存最小值的栈
当我们进栈时,会分两种情况:
- 当前辅助栈为空,则将数同时压进两个栈
- 如果当前压入的数比当前辅助栈顶小,则压入辅助栈中,否则就不压入辅助栈中。
Language: 全部题目
// ac地址:https://leetcode-cn.com/problems/bao-han-minhan-shu-de-zhan-lcof/
// 原文地址:https://xxoo521.com/2020-01-31-stack-min/
/**
* initialize your data structure here.
*/
var MinStack = function() {
this.dataStack = [];
this.minStack = []; // 辅助栈
};
/**
* @param {number} x
* @return {void}
*/
MinStack.prototype.push = function(x) {
this.dataStack.push(x);
const length = this.minStack.length;
if (!length) {
this.minStack.push(x);
} else if (x <= this.minStack[length - 1]) {
this.minStack.push(x);
}
};
/**
* @return {void}
*/
MinStack.prototype.pop = function() {
const { minStack, dataStack } = this;
if (minStack[minStack.length - 1] === dataStack[dataStack.length - 1]) {
minStack.pop();
}
dataStack.pop();
};
/**
* @return {number}
*/
MinStack.prototype.top = function() {
const length = this.dataStack.length;
if (length) {
return this.dataStack[length - 1];
} else {
return null;
}
};
/**
* @return {number}
*/
MinStack.prototype.min = function() {
const length = this.minStack.length;
if (!length) return null;
return this.minStack[length - 1];
};
剑指 Offer 09. 用两个栈实现队列
Difficulty: 简单
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 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 次调用
Solution
我们可以使用两个栈进行模拟实现队列:
- 如果要实现入队操作,将要入队的元素压入
stack1
- 如果要实现出队操作,有两种情况:
- 如果出队时,
stack2
为空,则将stack1
所有元素压入stack2
中 - 如果出队时,
stack2
不为空,则直接取出栈顶元素即可。
- 如果出队时,
Language: 全部题目
var CQueue = function() {
// 输入时进入的栈
this.stack1 = []
// 输出时出去的栈
this.stack2 = []
};
/**
* @param {number} value
* @return {void}
*/
CQueue.prototype.appendTail = function(value) {
this.stack1.push(value)
};
/**
* @return {number}
*/
CQueue.prototype.deleteHead = function() {
if (!this.stack2.length) {
while (this.stack1.length) {
this.stack2.push(this.stack1.pop());
}
}
return this.stack2.pop() || -1
};
深度优先遍历
剑指 Offer 55 - I. 二叉树的深度
Difficulty: 简单
输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。
例如:
给定二叉树 [3,9,20,null,null,15,7]
,
3
/
9 20
/
15 7
返回它的最大深度 3 。
提示:
节点总数 <= 10000
注意:本题与主站 104 题相同:
Solution
方案一:
通过后序遍历/深度优先遍历的方法,用递归的方式进行遍历
树的深度和左右子树的深度之间的关系就是,左右子树的最大深度 + 1 == 此二叉树的最大深度
Language: 全部题目
var maxDepth = function(root) {
if(!root) return null;
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
};
方案二:
通过层序遍历的方式进行
- 第一层循环用于对二叉树层进行遍历
- 每层循环初始化一个临时列表用于记录下一层所有节点
- 第二层循环将上一层节点全部遍历出来,并将他们
left
、right
进行存储在临时列表中 - 第二层循环完毕后,将临时列表赋给队列进行新一轮的循环
- 最后,层数进行
+1
var maxDepth = function(root) {
let queue = [];
let res =0;
queue.push(root);
while(queue.length){
let temp =[];
while(queue.length){
let f = queue.shift();
f.left && temp.push(f.left);
f.right && temp.push(f.right);
}
queue = temp;
res++;
}
return res;
};
剑指 Offer 12. 矩阵中的路径
Difficulty: 中等
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。
[["a","b","c","e"],
["s","f","c","s"],
["a","d","e","e"]]
但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。
示例 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
注意:本题与主站 79 题相同:
Solution
递归参数: 当前元素在矩阵 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 k 。
终止条件:
- 返回 false : ① 行或列索引越界 或 ② 当前矩阵元素与目标字符不同 或 ③ 当前矩阵元素已访问过 (③ 可合并至 ② ) 。
- 返回 true: 字符串 word 已全部匹配,即 k = len(word) - 1 。
递推工作:
- 标记当前矩阵元素: 将 board[i][j]值暂存于变量 tmp ,并修改为字符 '/' ,代表此元素已访问过,防止之后搜索时重复访问。
- 搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 或 连接 (代表只需一条可行路径) ,并记录结果至 res 。
- 还原当前矩阵元素: 将 tmp 暂存值还原至 board[i][j] 元素。
- 回溯返回值: 返回 res ,代表是否搜索到目标字符串。
Language: 全部题目
var exist = function(board, word) {
var row = board.length;
var 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; // word遍历完了
var tmp = board[i][j]; // 记录到board的值
board[i][j] = '-' // 锁上,因为后续的递归是4个方向上的,无法保证上一个方向的值
var 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] = tmp; // 恢复现场
return res;
}
// 遍历整个board,找到初始位置点
for(var i = 0;i < row; i++){
for(var j = 0; j < col; j++){
if(dfs(i,j,board,word,0)) return true;
}
}
// 没找到
return false
};
递归
剑指 Offer 10- I. 斐波那契数列
Difficulty: 简单
写一个函数,输入 n
,求斐波那契(Fibonacci)数列的第 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
注意:本题与主站 509 题相同:
Solution
斐波那契数列的定义是 f(n + 1) = f(n) + f(n - 1)
,生成第 n 项的做法有以下几种:
-
直接递归
- 原理: 把
f(n)
问题的计算拆分成f(n-1) 和 f(n−2)
两个子问题的计算,并递归,以f(0)
和f(1)
为终止条件。 - 缺点: 大量重复的递归计算,例如
f(n)
和f(n - 1)
两者向下递归需要 各自计算f(n - 2)
的值。
- 原理: 把
-
记忆化
- 原理:可以使用一个长度为
n
的数组进行记忆,记一下f(n-1) 和 f(n−2)
的值,之后遇到后直接调用,可以使复杂度降低为O(n)
- 缺点:需要另外一个数组进行记忆化,空间复杂度提高
- 原理:可以使用一个长度为
Language: 全部题目
var fib = function(n) {
if(n==0) return 0;
let dp=[];
dp[0]=0;
dp[1]=1;
for(let i = 2;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
dp[i]%=1000000007;
}
return dp[n];
};
剑指 Offer 10- I. 斐波那契数列
Difficulty: 简单
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n
级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入:n = 2
输出:2
示例 2:
输入:n = 7
输出:21
示例 3:
输入:n = 0
输出:1
提示:
0 <= n <= 100
注意:本题与主站 70 题相同:
Solution
与上一题很类似,只是起始的数据不相同
这一题我们可以采用三个变量
进行记忆化,从而更加节省空间。
Language: 全部题目
var numWays = function(n) {
if(n==1 || n==0) return 1;
let a=1;b=1;
let sum=0;
for(let i =2;i<=n;i++){
sum = (a +b)%1000000007;
a=b;
b=sum;
}
return sum ;
};
Hash
剑指 Offer 50. 第一个只出现一次的字符
Difficulty: 简单
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
示例:
s = "abaccdeff"
返回 "b"
s = ""
返回 " "
限制:
0 <= s 的长度 <= 50000
Solution
通过使用 Hash 进行存储
- 创建
map
,根据value
来判断是否出现多次。 - 循环将字符串放入
map
中,key
=字符串中字符,value
=该字符之前是否出现过
Language: 全部题目
var firstUniqChar = function(s) {
let map = new Map();
for(s of s){
map.set(s,!map.has(s))
}
for(m of map.keys()){
if(map.get(m)){
return m;
}
}
return ' '
};
剑指 Offer 48. 最长不含重复字符的子字符串
Difficulty: 中等
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
s.length <= 40000
注意:本题与主站 3 题相同:
Solution
主要思路:我们可以通过滑动窗口的方式进行一个滑动(其实就是两个指针划定一个区域)
方案一:
- 定义一个
arr
数组对窗口中的值进行储存,数组中存储的都是连续且不重复的值 - 当遍历字符串时,遇到重复的字符,就在数组中删除前一个重复字符串以及之前所有的字符。
- 更新
max
使得,当前arr
数组与之前的max
进行比较,选大的赋给max
多出来的看图:
Language: 全部题目
var lengthOfLongestSubstring = function(s) {
let arr = [], max = 0
for(let i = 0; i < s.length; i++) {
let index = arr.indexOf(s[i])
if(index !== -1) {
arr.splice(0, index+1);
}
arr.push(s.charAt(i))
max = Math.max(arr.length, max)
}
return max
};
方案二:
利用map
更加节省空间
var lengthOfLongestSubstring = function(s) {
let max = 0
let map = new Map();
for(let start = 0,end =0; end < s.length; end++) {
if(map.has(s[end])){
//这里会比较一下上一个重复字符所在的索引和当前所在索引,是因为当重复字符是在字符串的前部如:‘abba’,b已经更改了sart的位置,当轮到a时会start会指到前面的位置
start = Math.max(map.get(s[end])+1,start)
}
//因为是记录长度,所以 end-start 需要 +1
max = Math.max(end-start+1,max)
//在map中放入遍历的字符与其索引
map.set(s[end],end)
}
return max
};
链表
剑指 Offer 22. 链表中倒数第k个节点
Difficulty: 简单
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
Solution
这道题,我们通常会直接想到遍历到倒数k个位置就可,但是如果要遍历的话首先就需要知道链表的总长度,这样的话就需要遍历两次。
我们可以使用双指针的方式对其进行优化:
- 使用
first
与last
两个指针进行移动,先让first
移动k
个位置 - 直到
first
移动到位置后,两指针同时移动,直到first
为空,那么此时last
位置就是指向倒数第k
个位置
Language: 全部题目
var getKthFromEnd = function(head, k) {
let first = head,later = head;
for(let i = 0;i<k;i++){
first=first.next;
}
while(first!==null){
first=first.next;
later=later.next;
}
return later;
};
剑指 Offer 06. 从尾到头打印链表
Difficulty: 简单
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
示例 1:
输入:head = [1,3,2]
输出:[2,3,1]
限制:
0 <= 链表长度 <= 10000
Solution
还是挺简单的题目,我们可以直接使用Array.reverse()
反转
考虑到性能问题,我们也可以使用自己构造的reverse
方法
- 定义两个指针,分别指向两端,依次交换两边的值。
Language: 全部题目
var reversePrint = function(head) {
let arr = [];
let temp=head;
while(temp){
arr.push(temp.val)
temp=temp.next;
}
let reverse=(arr)=>{
let left,right,length = arr.length;
for(left=0;left<length/2;left++){
right= length-left-1;
let temp = arr[left]
arr[left]=arr[right]
arr[right]=temp
}
return arr
}
return reverse(arr);
};
剑指 Offer 24. 反转链表
Difficulty: 简单
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
限制:
0 <= 节点个数 <= 5000
注意:本题与主站 206 题相同:
Solution
可以使用 双指针和递归进行操作。
Language: 全部题目
var reverseList = function(head) {
let cur=null,pre = head;
while(pre){
let node = pre.next;
pre.next=cur;
cur=pre;
pre=node;
}
return cur
};
剑指 Offer 52. 两个链表的第一个公共节点
Difficulty: 简单
输入两个链表,找出它们的第一个公共节点。
如下面的两个链表:
在节点 c1 开始相交。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。
注意:
- 如果两个链表没有交点,返回
null
. - 在返回结果后,两个链表仍须保持原有的结构。
- 可假定整个链表结构中没有循环。
- 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
- 本题与主站 160 题相同:
Solution
-
我们使用两个指针分别指向两个链表,然后依次遍历到当前链表的结尾。
-
我们遍历到当前链表的结尾后,将他们再分别指向对方的链表,进行遍历
-
当它们相等时即退出遍历,会有两种情况
- 如果两链表有共同节点,那么他们此时就共同指向共同节点
- 如果两链表没有共同节点,那么他们都指向null
Language: 全部题目
var getIntersectionNode = function(headA, headB) {
if(headA== null || headB==null){
return null;
}
let lA = headA;
let lB = headB;
while(lA !== lB){
lA = lA==null ? headB : lA.next;
lB = lB==null ?headA : lB.next;
}
return lA;
};