持续分类别记录算法题总结
二叉树
236. 二叉树的最近公共祖先
难度中等
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
思路一:设置map,层序保存每个节点的父节点,然后设置set存下p的所有父节点(包括自己),遍历q和祖先元素,如果有直接retrun
var lowestCommonAncestor = function(root, p, q) {
if (!root||p==root||q==root)
return root;
let map = new Map();
let set = new Set();
let queue = [root];
while (queue.length){
let size =queue.length;
while(size--){
let node = queue.shift();
if (node.left){
map.set(node.left,node);
queue.push(node.left);
}
if (node.right){
map.set(node.right,node);
queue.push(node.right);
}
}
}
while (q){
set.add(q);
q = map.get(q);
}
while (p){
if (set.has(p))
return p;
p = map.get(p);
}
};
思路二:递归,核心在递归左右子树,两种情况:
- 如果左右子树都有最近p,q公共祖先,说明当前的root是p,q的最近公共祖先
- 左右子树单独有结果,将这个左右子树往外层吐(满足最近的公共祖先)
function lowestCommonAncestor(root: TreeNode | null, p: TreeNode | null, q: TreeNode | null): TreeNode | null {
if (!root || p === root || q === root) {
return root
}
const leftChildTreeLowestCommonAncestor = lowestCommonAncestor(root.left, p, q)
const rightChildTreeLowestCommonAncestor = lowestCommonAncestor(root.right, p, q)
// 如果左右子树都有最近p,q公共祖先,说明当前的root是p,q的最近公共祖先
if (leftChildTreeLowestCommonAncestor && rightChildTreeLowestCommonAncestor) {
return root
}
return leftChildTreeLowestCommonAncestor || rightChildTreeLowestCommonAncestor
};
剑指 Offer 55 - II. 平衡二叉树
难度简单
输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
思路: 外部设置flag,内部求深度,判断绝对值差大于1 falg设置false
var isBalanced = function(root) {
let falg = true;
let helper = (root)=>{
if (!root)
return 0;
let left = helper(root.left);
let right = helper(root.right);
if (Math.abs(left-right)>1)
falg = false;
return Math.max(left,right)+1;
}
helper(root);
return falg;
};
思路二: 每次都直接计算左右子树的高度,进行判断(效率低)
var isBalanced = function(root) {
let getDepth = (root)=>{
if (!root)
return 0;
return Math.max(getDepth(root.left),getDepth(root.right))+1;
}
if (!root)
return true;
return Math.abs(getDepth(root.left)-getDepth(root.right))<=1&&isBalanced(root.left)&&isBalanced(root.right);
};
114. 二叉树展开为链表
难度中等
给你二叉树的根结点 root ,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用
TreeNode,其中right子指针指向链表中下一个结点,而左子指针始终为null。 - 展开后的单链表应该与二叉树 先序遍历 顺序相同。
思路一: 记录前序遍历的节点,直接pre,cur从1开始迭代,因为是破坏了结构,直接赋值即可
let predfs = (root)=>{
if (!root)
return;
list.push(root);
predfs(root.left);
predfs(root.right);
}
var flatten = function(root) {
if (!root)
return root;
let list = [];
predfs(root);
let pre,cur;
for (let i=1;i<list.length;i++){
pre = list[i-1];
cur = list[i];
pre.left = null;
pre.right = cur;
}
};
思路二:后序遍历,保存每个节点的左右,将左接上右并进行遍历到尽头,右接上原本的右子树。
var flatten = function(root) {
if (!root)
return null;
flatten(root.left);
flatten(root.right);
let left = root.left;
let right = root.right;
root.left =null;
root.right = left;
while (root.right){
root = root.right;
}
root.right = right;
return root;
};
思路三:递归,先存下左右子树,接左边 => 接右边
function flatten(root: TreeNode | null): void {
if (!root) {
return
}
const {left, right} = root
// 左子树置为空
root.left = null
// 递归左
flatten(left)
// 递归右边
flatten(right)
// 先接左边,符合先序
root.right = left
// 遍历到最右边
let p = root
while (p.right) {
p = p.right
}
// 接上右边
p.right = right
};
二叉树路径 || 深度
剑指 Offer 55 - I. 二叉树的深度
难度简单
输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。
var maxDepth = function(root) {
if (!root)
return 0;
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
};
112. 路径总和
难度简单
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
思路: 深度遍历,每次把节点的值累加sum传入递归,判断路径和是否为target,每次递归返回 左 || 右
var hasPathSum = function(root, targetSum) {
if (!root)
return false;
let dfs = (root,sum)=>{
if (!root)
return false;
if (!root.left&&!root.right&&targetSum==sum+root.val)
return true;
return dfs(root.left,sum+root.val)||dfs(root.right,sum+root.val);
}
return dfs(root,0);
};
思路二:递归,当root为叶子节点命中targetSum,每次左右递归传入减入值,最终即可检测路径和
function hasPathSum(root: TreeNode | null, targetSum: number): boolean {
if (!root) {
return false
}
const isLeaf = !root.left && !root.right
if (isLeaf && targetSum === root.val) {
return true
}
return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val)
};
124. 二叉树中的最大路径和
难度困难
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和
思路: 定义一个helper(root),返回从下到root节点的最大路径,内部为左递归和右递归,如果递归结果小于0则不走,同时解题(更新最大路径和)。
var maxPathSum = function(root) {
if (!root)
return 0;
let sum = -Infinity;
let helper = (root)=>{
if (!root)
return 0;
let left = helper(root.left);
let right = helper(root.right);
left = left>0?left:0;
right = right>0?right:0;
sum = Math.max(sum,left+right+root.val);
return Math.max(left,right)+root.val;
}
helper(root);
return sum;
};
257. 二叉树的所有路径
难度简单
给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
思路: dfs深度遍历,定义一个path,每次遍历加入path,判断是否为叶子节点,如果为把路径加入结果数组,重点: 每次走完本节点记得pop回溯路径!
var binaryTreePaths = function(root) {
let path = [];
let result = [];
let dfs = function(root){
if (!root)
return;
path.push(root.val);
dfs(root.left);
dfs(root.right);
if (!root.left&&!root.right){
result.push(path.join('->'));
}
path.pop();
}
dfs(root);
return result;
};
思路二:队列,存储[TreeNode, stinrg]的队列
function binaryTreePaths(root: TreeNode | null): string[] {
if (!root) return [];
const result: string[] = [];
const queue: [TreeNode, string][] = [[root, root.val.toString()]];
while (queue.length) {
const [node, path] = queue.shift()!;
if (!node.left && !node.right) {
result.push(path);
continue;
}
if (node.left) {
queue.push([node.left, `${path}->${node.left.val}`]);
}
if (node.right) {
queue.push([node.right, `${path}->${node.right.val}`]);
}
}
return result;
}
543. 二叉树的直径
难度简单
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
思路:某节点的直径为左右子树的高度和,题目即求所有节点中左右子树的最大高度和。
- 定义求深度函数,每次对左右子树调用,更新最大值
- 或者 进行深度遍历,遍历过程中拿到左右子树的深度,然后更新最大值,每次递归返回本节点的最高深度。
TIP:每次递归返回本节点最大深度= 左右的大值 + 本层
var diameterOfBinaryTree = function(root) {
let max = -Infinity;
let dfs = (root)=>{
if (!root)
return 0;
let left = dfs(root.left);
let right = dfs(root.right);
max = Math.max(max,left+right);
return Math.max(left,right)+1;
}
dfs(root);
return max;
};
113. 路径总和 II
难度中等
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
思路:深度遍历,每次path记录路径,判断是否为根节点和和为target,是则加入result,tip:加入浅拷贝! 每次递归完返回上层执行上下文pop
var pathSum = function(root, targetSum) {
let result = [];
let path = [];
let getSum = (arr)=>{
return arr.reduce((acc,cur)=>acc+cur);
}
if (!root)
return result;
let helper = (root)=>{
if (!root)
return;
path.push(root.val);
helper(root.left);
helper(root.right);
if (!root.left&&!root.right&&getSum(path)==targetSum){
result.push(path.slice());
}
path.pop();
}
helper(root);
return result;
};
129. 求根节点到叶节点数字之和
难度中等
给你一个二叉树的根节点
每条从根节点到叶节点的路径都代表一个数字:
- 例如,从根节点到叶节点的路径
1 -> 2 -> 3表示数字123。
计算从根节点到叶节点生成的 所有数字之和 。
思路: 前序遍历,每次sum*10加上本身的val,左右子树递归。如果是叶子节点直接返回sum,不是则返回左右和。
var sumNumbers = function(root) {
let dfs = (root,sum)=>{
if (!root)
return 0;
sum = sum*10+root.val;
let left = dfs(root.left,sum);
let right = dfs(root.right,sum);
if (!root.left&&!root.right)
return sum;
return left+right;
};
return dfs(root,0);
};
111. 二叉树的最小深度
难度简单
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
思路: 根据左右子树的情况进行处理: 都有返回左右小的最小深度+1(自身),一边有直接递归+1,叶子节点返回1(递归的出口) tip: 这里需要判断左右是因为,左右子树为空意味着没有叶子节点的路径返回0不能参与最小值比较
var minDepth = function(root) {
if (!root)
return 0;
if (root.left&&root.right){
return Math.min(minDepth(root.left),minDepth(root.right))+1;
}
else if (root.left){
return minDepth(root.left)+1;
}
else if (root.right){
return minDepth(root.right)+1;
}
else{
return 1;
}
};
104. 二叉树的最大深度
难度简单
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
思路: 比最小深度简单一点,直接叶子返回1,返回左右子树最大的+1即可。
var maxDepth = function(root) {
if (!root)
return 0;
if (!root.left&&!root.right)
return 1;
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
};
翻转二叉树 || 对称二叉树
226. 翻转二叉树
难度简单
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
1 递归: 左右翻转,深度遍历
var invertTree = function(root) {
let reverse = (root)=>{
if (!root)
return;
[root.left,root.right] = [root.right,root.left];
reverse(root.left);
reverse(root.right);
}
reverse(root);
return root;
};
2 层序遍历: 每一层对每个节点的左右子树进行翻转
let reverse = (root)=>{
let temp = root.left;
root.left = root.right;
root.right = temp;
}
var invertTree = function(root) {
let queue = [];
if (!root)
return root;
queue.push(root);
while(queue.length){
let size = queue.length;
while(size--){
let node = queue.shift();
reverse(node);
let left = node.left;
let right =node.right;
if (left)
queue.push(left);
if (right)
queue.push(right);
}
}
return root;
};
101. 对称二叉树
难度简单
给你一个二叉树的根节点 root , 检查它是否轴对称。
思路,构造一个左右子树的helper,判断三种情况:都没有true,一边有false,都有但值不相同flase,递归左右。
var isSymmetric = function(root) {
if (!root)
return true;
let helper = (root1,root2)=>{
if (!root1&&!root2)
return true;
if (!root1||!root2)
return false;
if (root1.val!==root2.val)
return false;
return helper(root1.left,root2.right)&&helper(root1.right,root2.left);
}
return helper(root.left,root.right);
};
100. 相同的树
难度简单
给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
思路:和判断对称二叉树有相似的地方,构造两个参数的helper,都没有true,一个有false,都有判断值 ,然后进行左右子树的递归判断
var isSameTree = function(p, q) {
if(!p&&!q)
return true;
if (!q||!p)
return false;
if (q.val!==p.val)
return false;
return isSameTree(p.left,q.left)&&isSameTree(p.right,q.right);
};
前中后序 | | 层序遍历
144. 二叉树的前序遍历
难度简单731收藏分享切换为英文接收动态反馈
给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
非递归思路: 构建stack和指针p,循环条件为p或者stack,前序为根左右,每次通过p去存储节点到stack和result,然后到了右就把右节点赋值给p。
//递归
var preorderTraversal = function(root) {
if (!root)
return [];
let result = [];
let dfs =(root)=>{
if (!root)
return;
result.push(root.val);
dfs(root.left);
dfs(root.right);
}
dfs(root);
return result;
};
// 也可以
function preorderTraversal(root: TreeNode | null): number[] {
if (!root) {
return []
}
return [
root.val,
...preorderTraversal(root.left),
...preorderTraversal(root.right)
]
};
//迭代
var preorderTraversal = function(root) {
if (!root)
return [];
let result= [];
let stack = [];
let p =root;
while(p||stack.length){
while(p){
result.push(p.val);
stack.push(p);
p = p.left;
}
let node = stack.pop();
p = node.right;
}
return result;
};
// 也可以
function preorderTraversal(root: TreeNode | null): number[] {
if (!root) {
return []
}
const stack = [root]
const result = []
while(stack.length) {
const node = stack.pop()
result.push(node.val)
if (node.right) {
stack.push(node.right)
}
if (node.left) {
stack.push(node.left)
}
}
return result
};
94. 二叉树的中序遍历
难度简单、
给定一个二叉树的根节点 root ,返回它的 中序 遍历。
迭代思路: 构建p和stack,优先左并记录到stack,当弹出时进行根的存储,并把右树赋予p
//递归
var inorderTraversal = function(root) {
if (!root)
return [];
let result = [];
let dfs = (root)=>{
if (!root)
return;
dfs(root.left);
result.push(root.val);
dfs(root.right);
}
dfs(root);
return result;
};
//迭代
var inorderTraversal = function(root) {
if (!root)
return [];
let stack = [];
let result = [];
let p = root;
while(p||stack.length){
while(p){
stack.push(p);
p = p.left;
}
let node = stack.pop();
result.push(node.val);
p = node.right;
}
return result;
};
145. 二叉树的后序遍历
难度简单
给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 。
迭代思路: 按照前序的思路做,后序为左右根,可以用根右左再翻转。
//递归
var postorderTraversal = function(root) {
let result = [];
let traversal = (root)=>{
if (!root)
return;
traversal(root.left);
traversal(root.right);
result.push(root.val);
}
traversal(root);
return result;
}
//迭代
var postorderTraversal = function(root) {
let result = [];
let stack = [];
if (!root)
return result;
let p = root;
while (p||stack.length){
while (p){
result.push(p.val);
stack.push(p);
p = p.right;
}
let node = stack.pop();
p = node.left;
}
return result.reverse();
};
102. 二叉树的层序遍历
难度中等
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
思路: 两层循环,利用queue队列来实现顺序,从左到右,size--很经典
// 层序
var levelOrder = function(root) {
let queue = [];
let result = [];
if (!root)
return result;
queue.push(root);
while (queue.length){
let size = queue.length;
let res = [];
while(size--){
let node = queue.shift();
if (node.left){
queue.push(node.left);
}
if (node.right){
queue.push(node.right);
}
res.push(node.val);
}
result.push(res)
}
return result;
};
// 递归写法
function levelOrder(root: TreeNode | null): number[][] {
const result: number[][] = []
const traverse = (node: TreeNode, level: number) => {
if (!node) {
return
}
if (result.length === level) {
result.push([])
}
result[level].push(node.val)
traverse(node.left, level + 1)
traverse(node.right, level + 1)
}
traverse(root, 0)
return result
};
429. N 叉树的层序遍历
难度中等
给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。
树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。
思路:和二叉树几乎没有区别,遍历children即可
var levelOrder = function(root) {
let queue = [];
let result = [];
let level = 0;
if (!root)
return result;
queue.push(root);
while(queue.length){
let size = queue.length;
let res = [];
while (size--){
let node = queue.shift();
for (let i =0;i<node.children.length;i++){
queue.push(node.children[i]);
}
res.push(node.val);
}
level++;
result.push(res);
}
return result;
};
199. 二叉树的右视图
难度中等
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
思路:利用层序,就很简单,每次push最右边的即可。
var rightSideView = function(root) {
let queue = [];
let result = [];
if (!root)
return result;
queue.push(root);
while (queue.length){
let size = queue.length;
while (size--){
let node = queue.shift();
if (node.left)
queue.push(node.left)
if (node.right)
queue.push(node.right)
if (size==0)
result.push(node.val);
}
}
return result;
};
思路二: 递归,设置递归参数level,每次往右边递归(保证每一层遍历到的是最右边的)
function rightSideView(root: TreeNode | null): number[] {
const result = []
const traverse = (node: TreeNode, level: number): void => {
if (!node) {
return
}
// 每一层第一个记录
if (result.length === level) {
result.push(node.val)
}
// 右视图,所以先递归右边的
traverse(node.right, level + 1)
traverse(node.left, level + 1)
}
traverse(root, 0)
return result
};
103. 二叉树的锯齿形层序遍历
难度中等
给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
思路:锯齿根据level翻转即可。
var zigzagLevelOrder = function(root) {
let queue = [];
let result = [];
if (!root)
return result;
queue.push(root);
let level = 0;
while (queue.length){
let size = queue.length;
let res = [];
while (size--){
let node = queue.shift();
res.push(node.val);
if (node.left)
queue.push(node.left);
if (node.right)
queue.push(node.right);
}
level++;
if (level%2==0)
res.reverse();
result.push(res);
}
return result;
};
// 递归
function zigzagLevelOrder(root: TreeNode | null): number[][] {
const result = []
const dfs = (node: TreeNode, level: number): void => {
if (!node) {
return
}
if (result.length === level) {
result.push([])
}
if (level % 2 === 0) {
result[level].push(node.val)
} else {
result[level].unshift(node.val)
}
dfs(node.left, level + 1)
dfs(node.right, level + 1)
}
dfs(root, 0)
return result
};
105. 从前序与中序遍历序列构造二叉树
难度中等
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
思路: 前序的第一个为当前前序构造的头节点,然后根据这个前序节点找到中序的位置index,根据中序划分前序的左右子树
var buildTree = function(preorder, inorder) {
if (!preorder.length)
return null;
let node = preorder.shift();
let index = inorder.indexOf(node);
let head = new TreeNode(node);
head.left = buildTree(preorder.slice(0,index),inorder.slice(0,index));
head.right = buildTree(preorder.slice(index),inorder.slice(index+1));
return head;
};
106. 从中序与后序遍历序列构造二叉树
难度中等675收藏分享切换为英文接收动态反馈
给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
思路与105一致 : 后序遍历的最后一个节点为根节点...
var buildTree = function(inorder, postorder) {
if (!inorder.length)
return null;
let val = postorder.pop()
let head = new TreeNode(val);
let index = inorder.indexOf(val);
head.left = buildTree(inorder.slice(0,index),postorder.slice(0,index));
head.right = buildTree(inorder.slice(index+1),postorder.slice(index));
return head;
};
二叉搜索树
98. 验证二叉搜索树
难度中等
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
思路: 规定上下界,每个节点进行判断是否符合上下界,左子树规定最大值,右子树规定最小值。
var isValidBST = function(root) {
let max = Infinity;
let min = -Infinity;
let helper = (root,min,max)=>{
if (!root)
return true;
if (root.val<=min||root.val>=max)
return false;
return helper(root.left,min,root.val)&&helper(root.right,root.val,max);
}
return helper(root,min,max);
};
思路二(奇特) : 利用中序升序,并去重 ,如果一样则true
var isValidBST = function(root) {
let result = [];
let inorder = (root)=>{
if (!root)
return;
inorder(root.left);
result.push(root.val);
inorder(root.right);
}
inorder(root);
return result.join('') === [...new Set(result.sort((a,b)=>a-b))].join('');
};
108. 将有序数组转换为二叉搜索树
难度简单
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
思路: 类似二分查找,升序数组的中值为根,递归链接左右子树
var sortedArrayToBST = function(nums) {
let creatBST = (left,right)=>{
if (left>right)
return null;
let mid = Math.floor((left+right)/2);
let node = new TreeNode(nums[mid]);
node.left = creatBST(left,mid-1);
node.right = creatBST(mid+1,right);
return node;
}
return creatBST(0,nums.length-1);
};
230. 二叉搜索树中第K小的元素
难度中等
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
思路:中序返回
var kthSmallest = function(root, k) {
let result = [];
let inorder = (root)=>{
if (!root)
return;
inorder(root.left);
result.push(root.val);
inorder(root.right);
}
inorder(root);
return result[k-1];
};
链表
146. LRU 缓存
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity)以 正整数 作为容量capacity初始化 LRU 缓存int get(int key)如果关键字key存在于缓存中,则返回关键字的值,否则返回-1。void put(int key, int value)如果关键字key已经存在,则变更其数据值value;如果不存在,则向缓存中插入该组key-value。如果插入操作导致关键字数量超过capacity,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
思路:
- 双向链表引入虚拟头尾节点,以链表的顺序表示使用的顺序,头为最近使用
- 构建双向链表服务来管理链表的操作
- 哈希表 (map)存储缓存
- 每次使用(get,put)都移动节点到最前面,注意边界条件处理即可
// 双向链表节点
class DLinkedNode {
prev: DLinkedNode
next: DLinkedNode
key: number
value: number
constructor(key = 0, value = 0) {
this.key = key
this.value = value
this.prev = null
this.next = null
}
}
// 抽象链表的操作服务
class DLinkedNodeService {
head: DLinkedNode
tail: DLinkedNode
constructor() {
this.head = new DLinkedNode()
this.tail = new DLinkedNode()
this.head.next = this.tail
this.tail.prev = this.head
}
removedNode(node: DLinkedNode) {
node.next.prev = node.prev
node.prev.next = node.next
}
addToHead(node: DLinkedNode) {
node.prev = this.head
node.next = this.head.next
this.head.next.prev = node
this.head.next = node
}
moveToHead(node: DLinkedNode) {
// 先移除节点,保持链接关系
this.removedNode(node)
// 单独添加到头节点
this.addToHead(node)
}
removeTail(): DLinkedNode {
const tail = this.tail.prev
this.removedNode(tail)
return tail
}
}
class LRUCache {
private dLinkedNodeService: DLinkedNodeService
private capacity: number
private cache: Map<number, DLinkedNode>
private size: number
constructor(capacity: number) {
this.dLinkedNodeService = new DLinkedNodeService()
this.capacity = capacity
this.cache = new Map()
this.size = 0
}
get(key: number): number {
const node = this.cache.get(key)
if (!node) {
return -1
}
// 移动到最前端
this.dLinkedNodeService.moveToHead(node)
return node.value
}
put(key: number, value: number): void {
const node = this.cache.get(key)
// 新建
if (!node) {
const newNode = new DLinkedNode(key, value)
this.cache.set(key, newNode)
this.size += 1
this.dLinkedNodeService.addToHead(newNode)
if (this.size > this.capacity) {
// 去掉末端
const removed = this.dLinkedNodeService.removeTail()
this.cache.delete(removed.key)
this.size -= 1
}
} else {
// 更新
node.value = value
this.dLinkedNodeService.moveToHead(node)
}
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* var obj = new LRUCache(capacity)
* var param_1 = obj.get(key)
* obj.put(key,value)
*/
160. 相交链表
难度简单
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
思路一: 利用set记录headA路径,遍历headB,有返回,没有返回null
var getIntersectionNode = function(headA, headB) {
let set = new Set();
while (headA){
set.add(headA);
headA = headA.next;
}
while (headB){
if (set.has(headB))
return headB;
headB = headB.next;
}
return null;
};
思路二: 巧妙的方法,构建两个指针分别遍历两个链表,当一个走完走另一端,这样就能达到一个效果,两个指针最终走的路程是lenA+lenB,如果走到最后为null退出循环,之前退出即为交点。
var getIntersectionNode = function(headA, headB) {
let pA = headA;
let pB = headB;
while(pA!==pB){
pA = pA ==null ?headB:pA.next;
pB = pB == null ?headA:pB.next;
}
return pA;
};
19. 删除链表的倒数第 N 个结点
难度中等
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
思路: 快慢指针,快走n+1步,这样慢指针就能走到第n-1个,直接将next指向删除节点的下一个即可
var removeNthFromEnd = function(head, n) {
let newHead = new ListNode(-1,head);
let slow = fast = newHead;
for (let i=0;i<=n;i++){
fast = fast.next;
}
while(fast){
fast =fast.next;
slow= slow.next;
}
slow.next = slow.next.next;
return newHead.next;
};
思路二:递归连接next,用count记录倒数第几个,匹配中了返回next即可
function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
let count = 0
const recursion = (node: ListNode): ListNode => {
if (!node) {
return null
}
node.next = recursion(node.next)
count++
if (n === count) {
return node.next
}
return node
}
return recursion(head)
};
21. 合并两个有序链表
难度简单
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:每次进行链表头的比较处理,一方没有返回另一头,每次把小的节点拿出来返回并递归连上后序链表。
var mergeTwoLists = function(list1, list2) {
if (!list1)
return list2;
if (!list2)
return list1;
if (list1.val<list2.val){
list1.next = mergeTwoLists(list1.next,list2);
return list1;
}else{
list2.next = mergeTwoLists(list1,list2.next);
return list2;
}
};
翻转
206. 反转链表
难度简单
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
思路: 递归和迭代 ,递归是递归到末尾,然后每次对当前节点翻转,return tail,迭代是pre,cur,进行cur.next存储,交换。
//递归
var reverseList = function(head) {
if (!head||!head.next){
return head;
}
let tail = reverseList(head.next);
head.next.next = head;
head.next = null;
return tail;
};
var reverseList = function(head){
let pre = null;
let cur = head;
if (!head)
return head;
while(cur){
let temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
24. 两两交换链表中的节点
难度中等
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
思路:用递归进行两两分组,先保存下一个节点,head连接下一组,进行翻转,每次返回分组的头节点
var swapPairs = function(head) {
if (!head||!head.next){
return head;
}
let p = head.next;
head.next = swapPairs(p.next);
p.next = head;
return p;
};
92. 反转链表 II
难度中等
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
var reverseBetween = function(head, left, right) {
let newHead = new ListNode(-1,head);
let front = newHead;
for (let i=0;i<left-1;i++){
front = front.next;
}
let pre =tail= front.next;
let cur = pre.next;
for (let i =left;i<right;i++){
let temp = cur.next;
cur.next = pre;
pre =cur;
cur = temp;
}
front.next = pre;
tail.next = cur;
return newHead.next;
};
递归:递归出口分为head head.next,两个一组,返回第二个,并递归连接第一个
function swapPairs(head: ListNode | null): ListNode | null {
if (!head || !head.next) {
return head
}
const newHead = head.next
const next = newHead.next
newHead.next = head
head.next = swapPairs(next)
return newHead
};
25. K 个一组翻转链表
难度困难
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
思路: 构造递归,每次递归分k个节点一组,如果不满足直接返回head,每个递归函数需要对这k个进行翻转,并且最后进行head与下一组的连接,每次递归返回反转后每组的头节点。
var reverseKGroup = function(head, k) {
let p = head;
for (let i=0;i<k;i++){
if (!p)
return head;
p = p.next;
}
let nextGroup = reverseKGroup(p,k);
let pre= null;
let cur = head;
for (let i=0;i<k;i++){
let temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
head.next = nextGroup;
return pre;
};
堆
最大(小)堆
class MaxHeap {
public heap: number[] = [];
private swap(i: number, j: number) {
[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
}
private up(index: number) {
if (index <= 0) {
return;
}
const parent = Math.floor((index - 1) / 2);
if (this.heap[index] > this.heap[parent]) {
this.swap(index, parent);
this.up(parent);
}
}
public add(value: number) {
this.heap.push(value);
this.up(this.heap.length - 1);
}
private shiftDown(index: number) {
const left = 2 * index + 1;
const right = 2 * index + 2;
let largestIndex = index;
const size = this.getSize();
if (left < size && this.heap[left] > this.heap[largestIndex]) {
largestIndex = left;
}
if (right < size && this.heap[right] > this.heap[largestIndex]) {
largestIndex = right;
}
if (largestIndex !== index) {
this.swap(largestIndex, index);
this.shiftDown(largestIndex);
}
}
public getSize(): number {
return this.heap.length;
}
public pop(): number {
const max = this.heap[0];
this.heap[0] = this.heap.pop()!;
if (this.getSize() > 0) {
this.shiftDown(0);
}
return max;
}
}
class MinHeap {
public heap: number[] = [];
private swap(i: number, j: number) {
[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
}
private up(index: number) {
if (index <= 0) {
return;
}
const parent = Math.floor((index - 1) / 2);
if (this.heap[index] < this.heap[parent]) {
this.swap(index, parent);
this.up(parent);
}
}
public add(value: number) {
this.heap.push(value);
this.up(this.heap.length - 1);
}
public getSize(): number {
return this.heap.length;
}
public pop(): number {
if (this.getSize() === 0) {
throw new Error("Heap is empty");
}
const min = this.heap[0];
const last = this.heap.pop()!;
if (this.getSize() > 0) {
this.heap[0] = last;
this.shiftDown(0);
}
return min;
}
private shiftDown(index: number) {
const left = index * 2 + 1;
const right = index * 2 + 2;
const size = this.getSize();
let minIndex = index;
if (left < size && this.heap[left] < this.heap[minIndex]) {
minIndex = left;
}
if (right < size && this.heap[right] < this.heap[minIndex]) {
minIndex = right;
}
if (minIndex !== index) {
this.swap(minIndex, index);
this.shiftDown(minIndex);
}
}
public peek(): number {
if (this.getSize() === 0) {
throw new Error("Heap is empty");
}
return this.heap[0];
}
}
剑指 Offer 40. 最小的k个数
难度简单
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
思路: 最小k个数,构造容量k的最大堆,每次加入堆,如果满了则把堆顶推出,这样就把length-k个大值推出,最大堆中存放最小的k个数,其中堆顶是第k小的数
var getLeastNumbers = function(arr, k) {
let heap = new MaxHeap();
if (!k)
return [];
for (let num of arr){
heap.add(num);
if (heap.size()>k){
heap.pop();
}
}
return heap.maxheap;
};
215. 数组中的第K个最大元素
难度中等1485
给定整数数组 nums 和整数 k,请返回数组中第 **k** 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素
思路:最小堆,与40相反
function findKthLargest(nums: number[], k: number): number {
const minHeap = new MinHeap()
for (const num of nums) {
minHeap.add(num)
if (minHeap.getSize() > k) {
minHeap.pop()
}
}
return minHeap.peek()
};
347. 前 K 个高频元素
难度中等
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
思路:
- 记录次序是map的思路,没有赋值1,有加1。前k高频是最小堆
- 实现带data泛型的最小堆,传入自定义比较函数
/**
* 通用最小堆实现,支持自定义比较函数
*/
export class CustomMinHeap<T> {
private heap: T[] = [];
private compare: (a: T, b: T) => number;
/**
* 创建一个新的最小堆
* @param compareFunction 比较函数,返回负数表示a<b,0表示a=b,正数表示a>b
*/
constructor(compareFunction: (a: T, b: T) => number = (a: any, b: any) => a - b) {
this.compare = compareFunction;
}
/**
* 交换堆中两个元素的位置
*/
private swap(i: number, j: number): void {
[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
}
/**
* 将元素上移到正确位置
*/
private siftUp(index: number): void {
if (index <= 0) return;
const parentIndex = Math.floor((index - 1) / 2);
if (this.compare(this.heap[index], this.heap[parentIndex]) < 0) {
this.swap(index, parentIndex);
this.siftUp(parentIndex);
}
}
/**
* 将元素下移到正确位置
*/
private siftDown(index: number): void {
const leftChildIndex = 2 * index + 1;
const rightChildIndex = 2 * index + 2;
const size = this.heap.length;
let smallestIndex = index;
if (leftChildIndex < size && this.compare(this.heap[leftChildIndex], this.heap[smallestIndex]) < 0) {
smallestIndex = leftChildIndex;
}
if (rightChildIndex < size && this.compare(this.heap[rightChildIndex], this.heap[smallestIndex]) < 0) {
smallestIndex = rightChildIndex;
}
if (smallestIndex !== index) {
this.swap(index, smallestIndex);
this.siftDown(smallestIndex);
}
}
/**
* 添加元素到堆中
*/
public add(value: T): void {
this.heap.push(value);
this.siftUp(this.heap.length - 1);
}
/**
* 获取并移除堆顶元素
*/
public poll(): T | undefined {
if (this.isEmpty()) {
return undefined;
}
const min = this.heap[0];
const last = this.heap.pop()!;
if (this.heap.length > 0) {
this.heap[0] = last;
this.siftDown(0);
}
return min;
}
/**
* 查看堆顶元素但不移除
*/
public peek(): T | undefined {
return this.isEmpty() ? undefined : this.heap[0];
}
/**
* 检查堆是否为空
*/
public isEmpty(): boolean {
return this.heap.length === 0;
}
/**
* 获取堆的大小
*/
public size(): number {
return this.heap.length;
}
/**
* 清空堆
*/
public clear(): void {
this.heap = [];
}
/**
* 获取堆中的所有元素
*/
public toArray(): T[] {
return [...this.heap];
}
}
function topKFrequent(nums: number[], k: number): number[] {
// 统计每个元素出现的频率
const frequencyMap = new Map<number, number>();
for (const num of nums) {
frequencyMap.set(num, (frequencyMap.get(num) || 0) + 1);
}
// 创建一个最小堆,按照元素的频率进行比较
const minHeap = new CustomMinHeap<number>((a, b) => {
return frequencyMap.get(a)! - frequencyMap.get(b)!;
});
// 遍历频率Map
for (const [num, freq] of frequencyMap) {
// 如果堆的大小小于k,直接添加
if (minHeap.size() < k) {
minHeap.add(num);
}
// 如果当前元素的频率大于堆顶元素的频率,替换堆顶元素
else if (freq > frequencyMap.get(minHeap.peek()!)!) {
minHeap.poll();
minHeap.add(num);
}
}
// 从堆中提取结果
const result: number[] = [];
while (!minHeap.isEmpty()) {
result.unshift(minHeap.poll()!);
}
return result;
}
栈 || 队列
394. 字符串解码
function decodeString(s: string): string {
const repeatStack = []
const prevStack = []
let currentNum = 0
let currentStr = ''
for (let i = 0; i < s.length; i++) {
if (s[i] >= '0' && s[i] <= '9') {
currentNum = currentNum * 10 + parseInt(s[i])
} else if (s[i] === '[') {
repeatStack.push(currentNum)
prevStack.push(currentStr)
currentNum = 0
currentStr = ''
} else if (s[i] === ']') {
const repeat = repeatStack.pop()
const prev = prevStack.pop()
currentStr = prev + currentStr.repeat(repeat)
} else {
currentStr += s[i]
}
}
return currentStr
};
155. 最小栈
难度简单
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
MinStack()初始化堆栈对象。void push(int val)将元素val推入堆栈。void pop()删除堆栈顶部的元素。int top()获取堆栈顶部的元素。int getMin()获取堆栈中的最小元素。
思路: 一个正常栈,一个最小栈,每次push的val如果小于等于最小栈的栈顶(最小值)则push更新,每次pop当最小栈值与正常栈要pop的值相等则一起pop。
var MinStack = function() {
this.datastack = [];
this.minstack = [];
};
/**
* @param {number} val
* @return {void}
*/
MinStack.prototype.push = function(val) {
this.datastack.push(val);
if (!this.minstack.length||this.minstack[this.minstack.length-1]>=val){
this.minstack.push(val);
}
};
/**
* @return {void}
*/
MinStack.prototype.pop = function() {
if (this.datastack[this.datastack.length-1]==this.minstack[this.minstack.length-1]){
this.minstack.pop();
}
return this.datastack.pop();
};
/**
* @return {number}
*/
MinStack.prototype.top = function() {
return this.datastack[this.datastack.length-1];
};
/**
* @return {number}
*/
MinStack.prototype.getMin = function() {
return this.minstack[this.minstack.length-1];
};
20. 有效的括号
难度简单2989收藏分享切换为英文接收动态反馈
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
思路:利用栈,开入栈,闭压栈,判断,最后判断栈是否为空
function isValid(s: string): boolean {
const stack = []
const openTags = ['(', '{', '[']
const closeToOpenTagMap = {
'}': '{',
')': '(',
']': '['
}
for (const char of s) {
if (openTags.includes(char)) {
stack.push(char)
continue
}
const openTag = stack.pop()
if (openTag !== closeToOpenTagMap[char]) {
return false
}
}
return stack.length === 0
};
剑指 Offer 09. 用两个栈实现队列
难度简单
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
思路:两个栈,一个正常push,另一个用于取栈顶元素,通过翻转(pop第一个栈push到第二个栈即可)
class CQueue {
private stack1
private stack2
constructor() {
this.stack1 = []
this.stack2 = []
}
appendTail(value: number): void {
this.stack1.push(value)
}
private reverse(): void {
while (this.stack1.length) {
this.stack2.push(this.stack1.pop())
}
}
deleteHead(): number {
const isStack1Empty = this.stack1.length === 0
const isStack2Empty = this.stack2.length === 0
if (isStack1Empty && isStack2Empty) {
return -1
}
if (isStack2Empty) {
this.reverse()
}
return this.stack2.pop()
}
}
71. 简化路径
难度中等
给你一个字符串 path ,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 '/' 开头),请你将其转化为更加简洁的规范路径。
在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,'//')都被视为单个斜杠 '/' 。 对于此问题,任何其他格式的点(例如,'...')均被视为文件/目录名称。
请注意,返回的 规范路径 必须遵循下述格式:
- 始终以斜杠
'/'开头。 - 两个目录名之间必须只有一个斜杠
'/'。 - 最后一个目录名(如果存在)不能 以
'/'结尾。 - 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含
'.'或'..')。
返回简化后得到的 规范路径 。
思路: 利用栈, .. 返回上一级pop, . 或者 ‘’ 忽略,其他push,最后记得加上'/'
var simplifyPath = function(path) {
let stack = [];
let arr = path.split('/');
for (let ch of arr){
if (ch == ''||ch=='.')
continue;
else if (ch=='..'){
stack.length&&stack.pop();
}
else{
stack.push(ch);
}
}
return '/'+stack.join('/');
};
225. 用队列实现栈
难度简单
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x)将元素 x 压入栈顶。int pop()移除并返回栈顶元素。int top()返回栈顶元素。boolean empty()如果栈是空的,返回true;否则,返回false。
思路: 一个队列push,但是当有第二个队列有栈顶元素时候要更新(把第二个队列的元素push到第一个队列中去,然后push到第二个),第二个队列一直存放栈顶元素,没有则通过第一个队列shift到最后一个然后交换。tip:交换是灵魂!
var MyStack = function() {
this.queue1 = [];
this.queue2 = [];
};
/**
* @param {number} x
* @return {void}
*/
MyStack.prototype.push = function(x) {
if (this.queue2.length){
this.queue1.push(this.queue2.shift());
this.queue2.push(x);
}else{
this.queue1.push(x);
}
};
/**
* @return {number}
*/
MyStack.prototype.transform = function(){
while(this.queue1.length>1){
this.queue2.push(this.queue1.shift());
}
[this.queue1,this.queue2] = [this.queue2,this.queue1];
}
MyStack.prototype.pop = function() {
if (!this.queue2.length)
this.transform();
return this.queue2.shift();
};
/**
* @return {number}
*/
MyStack.prototype.top = function() {
if (!this.queue2.length)
this.transform();
return this.queue2[0];
};
/**
* @return {boolean}
*/
MyStack.prototype.empty = function() {
return !this.queue1.length&&!this.queue2.length;
};
思路二
class MyStack {
stack = []
constructor() {
this.stack = []
}
push(x: number): void {
const size = this.stack.length
this.stack.push(x)
for (let i = 0; i < size; i++) {
this.stack.push(this.stack.shift())
}
}
pop(): number {
return this.stack.shift()
}
top(): number {
return this.stack[0]
}
empty(): boolean {
return this.stack.length === 0
}
}
279. 完全平方数
难度中等
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
思想: 构造一个queue,用来存放步数和nextNum,利用队列的先进先出的特点来实现最少步数step,每次都把同一层的所有步骤都走一遍,达到层序的效果(同一step),优化是通过set来进行,后面出现的nextNum(可能是同一层,也可能是下一层等)都不再进行遍历。
var numSquares = function(n) {
let queue = [];
queue.push([n,0]);
let set = new Set();
while(queue.length){
let [num,step] = queue.shift();
for(let i=1;;i++){
let nextNum = num-i*i;
if (nextNum<0)
break;
else if (nextNum>0){
if (set.has(nextNum))
continue;
set.add(nextNum);
queue.push([nextNum,step+1]);
}
else
return step+1;
}
}
};
数学 || 逻辑
509. 斐波那契数
难度简单
斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n ,请计算 F(n) 。
思路:动态 dp[n] = dp[n-1] + dp[n-2]; 三个变量模拟数组 p,q,r 返回r
var fib = function(n) {
let p=0,q=0,r=1;
if (n<2)
return n;
n--;
while(n--){
p = q;
q = r;
r = p+q;
}
return r;
};
171. Excel 表列序号
难度简单
给你一个字符串 columnTitle ,表示 Excel 表格中的列名称。返回 该列名称对应的列序号 。
例如:
A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28
...
思路: 26进制,每次减去A-1(65-1)=64
var titleToNumber = function(columnTitle) {
let sum = 0;
for (let item of columnTitle){
sum =sum*26+item.charCodeAt()-64;
}
return sum;
};
695. 岛屿的最大面积
难度中等
给你一个大小为 m x n 的二进制矩阵 grid 。
岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
岛屿的面积是岛上值为 1 的单元格的数目。
计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。
思路: dfs深度遍历,使用方向dir数组reduce,不是岛屿返回0,每次返回1,使用reduce累加,遍历后将岛屿淹没。
var maxAreaOfIsland = function(grid) {
let max = 0;
let row = grid.length;
let col = grid[0].length;
let dir = [
[0,1],
[0,-1],
[1,0],
[-1,0]
];
let dfs = (i,j)=>{
if (i<0||i>=row||j<0||j>=col||grid[i][j]==0){
return 0;
}
grid[i][j]=0;
return dir.reduce((sum,cur)=>{
return sum+dfs(cur[0]+i,cur[1]+j);
},1);
}
for (let i=0;i<row;i++){
for (let j=0;j<col;j++){
if (grid[i][j]!==0){
max = Math.max(max,dfs(i,j));
}
}
}
return max;
};
54. 螺旋矩阵
难度中等
给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
思路: 四个边界,每次按顺序push,遍历完外层缩减,最后对一行或者一竖进行补充,也可能正好走完。
var spiralOrder = function(matrix) {
let row = matrix.length;
let col = matrix[0].length;
let top = 0;
let bottom =row-1;
let left = 0;
let right = col-1;
let result = [];
while(left<right&&top<bottom){
for (let i=left;i<right;i++){
result.push(matrix[top][i]);
}
for (let i=top;i<bottom;i++){
result.push(matrix[i][right]);
}
for (let i=right;i>left;i--){
result.push(matrix[bottom][i]);
}
for (let i=bottom;i>top;i--){
result.push(matrix[i][left]);
}
top++;
right--;
bottom--;
left++;
}
if (left==right){
for (let i=top;i<=bottom;i++){
result.push(matrix[i][left]);
}
}else if (bottom==top){
for (let i=left;i<=right;i++){
result.push(matrix[top][i]);
}
}
return result;
};
第二种写法:更优雅
function spiralOrder(matrix: number[][]): number[] {
const result = []
const row = matrix.length
const col = matrix[0].length
let top = 0
let bottom = row - 1
let left = 0
let right = col - 1
while (left <= right && top <= bottom) {
for (let j = left; j <= right; j++) {
result.push(matrix[top][j])
}
top++
for (let i = top; i <= bottom; i++) {
result.push(matrix[i][right])
}
right--
// 由于top变化,及时处理边界
if (top <= bottom) {
for (let j = right; j >= left; j--) {
result.push(matrix[bottom][j])
}
}
bottom--
// 由于rigth变化,及时处理边界
if (left <= right) {
for (let i = bottom; i >= top; i--) {
result.push(matrix[i][left])
}
}
left++
}
return result
};
200. 岛屿数量
难度中等
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
思路:和695一致,每次递归都是深度遍历,count++即可
var numIslands = function(grid) {
let count = 0;
let row = grid.length;
let col = grid[0].length;
let dirctions = [
[1,0],
[-1,0],
[0,1],
[0,-1]
];
let dfs = (i,j)=>{
if (i<0||j<0||i>=row||j>=col||grid[i][j]=="0")
return;
grid[i][j] = "0";
dirctions.forEach((dir)=>{
dfs(i+dir[0],j+dir[1]);
})
}
for (let i=0;i<row;i++){
for (let j=0;j<col;j++){
if (grid[i][j]=="1"){
dfs(i,j);
count++;
}
}
}
return count;
};
415. 字符串相加
难度简单
给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。
你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。
思路:大数相加,从末尾相加,注意字符串的0的问题,进位carry。最后翻转即可
var addStrings = function(num1, num2) {
let i = num1.length-1;
let j= num2.length-1;
let carry = 0;
let result = [];
while(i>=0||j>=0||carry){
let n1 = num1[i]?num1[i]-0:0;
let n2 = num2[j]?num2[j]-0:0;
let sum = n1+n2+carry;
if (sum>=10)
carry =1;
else
carry = 0;
result.push(sum%10);
i--;
j--;
}
return result.reverse().join('');
};
165. 比较版本号
难度中等
给你两个版本号 version1 和 version2 ,请你比较它们。
版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.33 和 0.1 都是有效的版本号。
比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 0 和 1 ,0 < 1 。
返回规则如下:
- 如果
*version1* > *version2*返回1, - 如果
*version1* < *version2*返回-1, - 除此之外返回
0。
思路: split分隔,遍历两个数组,Number可去前导0,如果有大小差异则直接返回结果,遍历完后,如果有则继续判断是否有不为0,有直接返回结果。 最后返回0
var compareVersion = function(version1, version2) {
let arr1 = version1.split('.').map((item)=>Number(item));
let arr2 = version2.split('.').map((item)=>Number(item));
while(arr1.length&&arr2.length){
let num1 = arr1.shift();
let num2 = arr2.shift();
if (num1<num2)
return -1;
if (num1>num2)
return 1;
}
while(arr1.length){
if (arr1.pop()!=0)
return 1;
}
while(arr2.length){
if (arr2.pop()!=0)
return -1;
}
return 0;
};
14. 最长公共前缀
难度简单
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。
暴力思路: 两层循环,每次遍历到不同则进行裁剪,遍历完即为最长公共前缀
var longestCommonPrefix = function(strs) {
let res = strs[0];
for (let i=1;i<strs.length;i++){
for (let j=0;j<res.length;j++){
if (res[j]!==strs[i][j]){
res = res.slice(0,j);
break;
}
}
}
return res;
};
剑指 Offer 04. 二维数组中的查找
难度中等
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
思路: 从左下角开始,target大于往右,小于往上
var findNumberIn2DArray = function(matrix, target) {
if (matrix.length ==0)
return false;
let row = matrix.length;
let col = matrix[0].length;
let x =row-1;
let y = 0;
while(x>=0&&x<row&&y>=0&&y<col){
if (matrix[x][y] ==target){
return true;
}else if(matrix[x][y]>target){
x--;
}else{
y++;
}
}
return false;
};
1. 两数之和
难度简单
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
思路: map存储target-num,遍历到本身存在map则说明相加等于target,返回结果
var twoSum = function(nums, target) {
let map = new Map();
for (let i = 0;i<nums.length;i++){
if (map.has(nums[i])){
return [i,map.get(nums[i])]
}
map.set(target-nums[i],i);
}
};
56. 合并区间
难度中等
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
思路: 按照起始区间排序,记录第一个,从第二个开始遍历,pre,cur,如果pre的结束大于cur的开始,进行合并,否则push result
var merge = function(intervals) {
intervals.sort((arr1,arr2)=>arr1[0]-arr2[0]);
let result = [];
let pre = intervals[0];
let cur;
for (let i=1;i<intervals.length;i++){
cur = intervals[i];
if (pre[1]>=cur[0]){
pre[1] = Math.max(pre[1],cur[1]);
}else{
result.push(pre);
pre = cur;
}
}
result.push(pre);
return result;
};
function merge(intervals: number[][]): number[][] {
// 思路: 先排序,后合并/创建区间
const sortedArr = intervals.sort((a, b) => a[0] - b[0])
const result = [sortedArr[0]]
for (let i = 1; i < sortedArr.length; i++) {
const last = result[result.length - 1]
const cur = sortedArr[i]
if (last[1] >= cur[0]) {
// 合并
last[1] = Math.max(last[1], cur[1])
} else {
result.push(cur)
}
}
return result
};
剑指 Offer 62. 圆圈中最后剩下的数字
难度简单541收藏分享切换为英文接收动态反馈
0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
思路: 规律,F(n) = (F(n-1)+m)%n , F(n)表示剩下的下标,最后一个人下标为0,进行反推
var lastRemaining = function(n, m) {
//F(n) = (F(n-1)+m)%n;
let pos = 0;
for (let i=2;i<=n;i++){
pos = (pos+m)%i;
}
return pos;
};
400. 第 N 位数字
难度中等
给你一个整数 n ,请你在无限的整数序列 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ...] 中找出并返回第 n 位上的数字。
思路: 1-9 9*0 —— 10 -99 9 *10 —— 每次减去 9 ,90...,计算出剩余多少个位,然后再算出是第几个数字和数字的第几位,返回数字
var findNthDigit = function(n) {
let digit = 1;
while (n-digit*9*Math.pow(10,digit-1)>0){
n-= 9*digit*Math.pow(10,digit-1);
digit++;
}
let num = Math.ceil(n/digit);
let a = n%digit;
let str = (num+Math.pow(10,digit-1)-1)+'';
if (a==0)
return parseInt(str[str.length-1])
else
return parseInt(str[a-1]);
};
剑指 Offer 61. 扑克牌中的顺子
难度简单
从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
思路: 最大最小,如果有重复false,return max-min<5
var isStraight = function(nums) {
let min = 14;
let max = -1;
let set = new Set();
for (num of nums){
if (num!==0){
if (set.has(num))
return false;
set.add(num);
min = Math.min(num,min);
max = Math.max(num,max);
}
}
return max-min<5;
};
242. 有效的字母异位词
难度简单
给定两个字符串 *s* 和 *t* ,编写一个函数来判断 *t* 是否是 *s* 的字母异位词。
**注意:**若 *s* 和 *t* 中每个字符出现的次数都相同,则称 *s* 和 *t* 互为字母异位词。
思路: str排序 or map
var isAnagram = function(s, t) {
return [...s].sort().join('') == [...t].sort().join('');
};
88. 合并两个有序数组
难度简单
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
**注意:**最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n
思路一: push后排序
var merge = function(nums1, m, nums2, n) {
for (let i=0;i<n;i++){
nums1[i+m] = nums2[i];
}
return nums1.sort((a,b)=>a-b);
};
思路二:双指针
var merge = function(nums1, m, nums2, n) {
let p1 = 0, p2 = 0;
const sorted = new Array(m + n).fill(0);
var cur;
while (p1 < m || p2 < n) {
if (p1 === m) {
cur = nums2[p2++];
} else if (p2 === n) {
cur = nums1[p1++];
} else if (nums1[p1] < nums2[p2]) {
cur = nums1[p1++];
} else {
cur = nums2[p2++];
}
sorted[p1 + p2 - 1] = cur;
}
for (let i = 0; i != m + n; ++i) {
nums1[i] = sorted[i];
}
};
125. 验证回文串
难度简单
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
**说明:**本题中,我们将空字符串定义为有效的回文串。
思路:对字符串过滤字符和字母,两种方法:翻转或者双指针
//api+翻转
var isPalindrome = function(s) {
let strarray = s.toLowerCase().split('').filter((item)=>{
if (item==' ')
return false;
return item>='a'&&item<='z'|| item>=0&&item<=9;
});
let str1 = strarray.join('');
let str2 = strarray.reverse().join('');
return str1 ==str2;
};
//双指针
var isPalindrome = function(s) {
let left = 0;
let right =s.length-1;
s = s.toLowerCase();
let isValid = (ch)=>{
return ch>='a'&&ch<='z' || ch>=0&&ch<=9&&ch!=' ';
}
while(left<right){
while(left<=right&&!isValid(s[left])){
left++;
}
while(left<=right&&!isValid(s[right])){
right--;
}
if (s[left]!=s[right]&&left<=right){
return false;
}
left++;
right--;
}
return true;
};
7. 整数反转
难度中等
给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。
如果反转后整数超过 32 位的有符号整数的范围 [−231, 231 − 1] ,就返回 0。
思路:利用字符串api进行翻转,parseInt(能去除前导0)
var reverse = function(x) {
let res = parseInt(String(x).split('').reverse().join(''));
if (x<0)
res = -res;
return res<-Math.pow(2,31)||res>Math.pow(2,31)-1 ? 0:res;
};
384. 打乱数组
难度中等
给你一个整数数组 nums ,设计算法来打乱一个没有重复元素的数组。打乱后,数组的所有排列应该是 等可能 的。
实现 Solution class:
Solution(int[] nums)使用整数数组nums初始化对象int[] reset()重设数组到它的初始状态并返回int[] shuffle()返回数组随机打乱后的结果
思路: 储存原数组,每次洗牌创建拷贝(深),洗牌的算法为每次使用random*length来选出一个index放置在前边,从0开始道最后一个
var Solution = function(nums) {
this.nums = nums;
};
/**
* @return {number[]}
*/
Solution.prototype.reset = function() {
return this.nums;
};
/**
* @return {number[]}
*/
Solution.prototype.shuffle = function() {
let temp = this.nums.slice();
for (let i=0;i<temp.length;i++){
let random = i+Math.floor(Math.random()*(temp.length-i));
[temp[random],temp[i]] = [temp[i],temp[random]];
}
return temp;
};
146. LRU 缓存
难度中等2369收藏分享切换为英文接收动态反馈
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity)以 正整数 作为容量capacity初始化 LRU 缓存int get(int key)如果关键字key存在于缓存中,则返回关键字的值,否则返回-1。void put(int key, int value)如果关键字key已经存在,则变更其数据值value;如果不存在,则向缓存中插入该组key-value。如果插入操作导致关键字数量超过capacity,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
// 思路:利用map的迭代器,如get则重新set
/**
* @param {number} capacity
*/
var LRUCache = function(capacity) {
this.capacity = capacity;
this.map = new Map();
};
/**
* @param {number} key
* @return {number}
*/
LRUCache.prototype.get = function(key) {
if (this.map.has(key)) {
let value = this.map.get(key);
this.map.delete(key)
this.map.set(key, value);
return value;
}
return -1;
};
/**
* @param {number} key
* @param {number} value
* @return {void}
*/
LRUCache.prototype.put = function(key, value) {
if (this.map.has(key)) {
this.map.delete(key);
}
this.map.set(key, value);
if (this.map.size > this.capacity) {
this.map.delete(this.map.keys().next().value)
}
};
/**
* Your LRUCache object will be instantiated and called as such:
* var obj = new LRUCache(capacity)
* var param_1 = obj.get(key)
* obj.put(key,value)
*/
回溯
46. 全排列
难度中等
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
思路: 每次递归遍历nums,选取一个当前没有的数字,记录,深度遍历,当遍历完后需要将这个数字取消记录给其他遍历选取
var permute = function(nums) {
let result = [];
let set = new Set();
let dfs = (arr)=>{
if (arr.length == nums.length){
result.push(arr.slice());
return;
}
for (let num of nums){
if (!set.has(num)){
set.add(num);
dfs(arr.concat(num));
set.delete(num);
}
}
};
dfs([]);
return result;
};
78. 子集
难度中等
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
思路: 与46很相似,但性质不同,这一题是2的nums.length次,每次走或者不走,然后把结果push。46题是num.length的num.length其中会记录和删除记录
var subsets = function(nums) {
let result = [];
let dfs = (arr=[],step=0)=>{
if (step==nums.length){
result.push(arr.slice());
return;
}
arr.push(nums[step]);
dfs(arr,step+1);
arr.pop();
dfs(arr,step+1);
}
dfs();
return result;
};
22. 括号生成
难度中等
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
function generateParenthesis(n: number): string[] {
const result = []
const backtrack = (open, close, res) => {
if (res.length === n * 2) {
result.push(res)
}
if (open < n) {
backtrack(open + 1, close, res + '(')
}
if (close < open) {
backtrack(open, close + 1, res + ')')
}
}
backtrack(0, 0, '')
return result
};
思路二:构造()的n为1的情况,之后每次构建新set来对n-1的每种情况进行来进行对每个位置的插入(),即n=2 可以是()(),(()),()()后去重,依次迭代
var generateParenthesis = function(n) {
let result = new Set(['()']);
for (let i=2;i<=n;i++){
let newSet = new Set();
for (let str of result){
for (let j=0;j<str.length;j++){
newSet.add(str.slice(0,j)+'()'+str.slice(j));
}
}
result = newSet;
}
return [...result];
};
二分查找
704. 二分查找
难度简单
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
var search = function(nums, target) {
let left = 0;
let right = nums.length;
while(left<=right){
let mid = Math.floor((left+right)/2);
if (nums[mid]==target){
return mid;
}else if (nums[mid]<target){
left =mid+1;
}else{
right = mid-1;
}
}
return -1;
};
35. 搜索插入位置
难度简单
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
思路: 还是二分查找,跳出循环说明没有目标值,此时left刚好满足所有插入情况(最左边,中间,最右边)
var searchInsert = function(nums, target) {
let left = 0;
let right =nums.length-1;
while(left<=right){
let mid = Math.floor((left+right)/2);
if (nums[mid]<target){
left = mid+1;
}else if (nums[mid]>target){
right =mid-1;
}else{
return mid;
}
}
return left;
};
思路二: 暴力(效率还挺高,直接遍历,当num大于等于target直接返回当前index,包括插入和查找),最后出循环代表target为最大值,返回数组长度即可
var searchInsert = function(nums, target) {
for (var i=0;i<nums.length;i++){
if (nums[i]>=target)
return i;
}
return nums.length;
};
287. 寻找重复数
难度中等
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
//思路,模拟一个arr[i]表示nums中小于等于i的数字个数,如果大于i说明已经有重复,每次循环判断,二分查找
var findDuplicate = function(nums) {
let left = 1;
let right =nums.length-1;
let result = -1;
while(left<=right){
let arr = 0;
let mid = Math.floor((left+right)/2);
for (let num of nums){
arr += num<=mid;
}
if (arr<=mid){
left = mid+1;
}else{
result = mid;
right = mid-1;
}
}
return result;
};
剑指 Offer II 071. 按权重生成随机数
难度中等
给定一个正整数数组 w ,其中 w[i] 代表下标 i 的权重(下标从 0 开始),请写一个函数 pickIndex ,它可以随机地获取下标 i,选取下标 i 的概率与 w[i] 成正比。
思路: 将w的权重数组化成一个n+1项的数组,第0项为0,然后每次叠加w权重,将随机变成w权重之和*random(0-1)在哪个区间。 然后是有序数组,可以使用二分查找返回下标
var Solution = function(w) {
this.array = [];
this.sum = 0;
for (let num of w){
this.array.push(this.sum);
this.sum+=num;
}
this.array.push(this.sum);
};
/**
* @return {number}
*/
Solution.prototype.pickIndex = function() {
let random = Math.random()*this.sum;
let left = 0;
let right = this.array.length-1;
while(left<right){
let mid = Math.floor((left+right)/2);
if (random>=this.array[mid]){
left = mid+1;
}else{
right = mid;
}
}
return left-1;
};
例如,对于 w = [1, 3],挑选下标 0 的概率为 1 / (1 + 3) = 0.25 (即,25%),而选取下标 1 的概率为 3 / (1 + 3) = 0.75(即,75%)。
也就是说,选取下标 i 的概率为 w[i] / sum(w) 。
875. 爱吃香蕉的珂珂
难度中等420收藏分享切换为英文接收动态反馈
珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。
珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。
思路: 二分查找speed,最小值为1,最大值为所有堆的最大香蕉数(符合题意,即所有堆都是一次性吃完)
var minEatingSpeed = function(piles, h) {
let low = 1;
let high = piles.sort((a, b) => b - a)[0];
let minSpeed = high;
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (getTime(piles, mid) <= h) {
minSpeed = mid;
high = mid;
} else {
low = mid + 1;
}
}
return minSpeed;
};
let getTime = (piles, speed) => {
let time = 0;
for (let pile of piles) {
time += pile % speed == 0 ? pile / speed : Math.ceil(pile / speed);
}
return time;
}
动态规划
70. 爬楼梯
难度简单
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
每次爬n阶的方法可以由爬n-1阶的方法加上爬n-2阶的方法之和得来
var climbStairs = function(n) {
let dp = [0,1,2];
for (let i=3;i<=n;i++){
dp[i] = dp[i-1]+dp[i-2];
}
return dp[n];
};
53. 最大子数组和
难度简单
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
思路: dp[i] 为数组到第i项的具有的最大连续数字和,dp[i] = max(dp[i-1] +num[i], num[i] ) 即更新为连续或者是从自身开始。
var maxSubArray = function(nums) {
let max = nums[0];
let dp= [];
dp[0] = nums[0];
for (let i=1;i<nums.length;i++){
dp[i] = Math.max(dp[i-1]+nums[i],nums[i]);
max = Math.max(max,dp[i]);
}
return max;
};
322. 零钱兑换
难度中等
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
思路: dp[amount] 为题目所求,最少硬币数, dp[amount] = min (dp[amount-coin1],dp[amount-coin2],....) +1;
var coinChange = function(coins, amount) {
let dp = [];
for (let i=1;i<=amount;i++){
dp[i] = Infinity;
}
dp[0] = 0;
for (let i=1;i<=amount;i++){
for (let coin of coins){
if (i-coin>=0){
dp[i] = Math.min(dp[i],dp[i-coin]+1);
}
}
}
return dp[amount]==Infinity?-1:dp[amount];
};
64. 最小路径和
难度中等
给定一个包含非负整数的 *m* x *n* 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
**说明:**每次只能向下或者向右移动一步。
思路: dp[i] [j] 表示到i,j最小的路径和,因为只能往下或者往右,第一排和第一列的dp可以初始化, 每一格的最小路径可以化成本格大小加 上格dp和左格dp的小值
var minPathSum = function(grid) {
let row = grid.length;
let col = grid[0].length;
let dp = [];
for (let i=0;i<row;i++){
let temp = [];
dp.push(temp);
}
dp[0][0] = grid[0][0];
for (let i=1;i<row;i++){
dp[i][0] = dp[i-1][0] + grid[i][0];
}
for (let j=1;j<col;j++){
dp[0][j] = dp[0][j-1] + grid[0][j];
}
for (let i=1;i<row;i++){
for (let j=1;j<col;j++){
dp[i][j] = grid[i][j] + Math.min(dp[i-1][j],dp[i][j-1]);
}
}
return dp[row-1][col-1];
};
300. 最长递增子序列
难度中等
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
思路: dp表示当前下标最长的子序列的长度,因为是子序列,所以每次dp[x] 都可以由比nums[x]的小的dp来更新,意味着连接。结果可以用max记录最长,这里是用拓展运算符。
var lengthOfLIS = function(nums) {
let dp = new Array(nums.length).fill(1);
for (let i=1;i<nums.length;i++){
for (let j=0;j<i;j++){
if (nums[j]<nums[i]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
}
return Math.max(...dp);
};
62. 不同路径
难度中等
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
思路:和64最小路径和类似,dp[i] [j]表示从左上角到i行j格的路径条数,因为每次只能往右走,往下走,所以dp[i] [j] = dp[i-1] [j] + dp[i] [j-1];
tip:创建二维数组利用map每次返回一个j列的数组
var uniquePaths = function(m, n) {
let dp = [...(new Array(m))].map(()=>new Array(n).fill(1));
let row = m;
let col = n;
for (let i=1;i<row;i++){
for (let j=1;j<col;j++){
dp[i][j] = dp[i-1][j]+dp[i][j-1];
}
}
return dp[row-1][col-1];
};
72. 编辑距离
难度困难2572收藏分享切换为英文接收动态反馈
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
var minDistance = function(word1, word2) {
// dp 思路: dp[i][j] 是word第i位到word第j位最少
// 1 word1[i - 1] === word[j - 1] 相等 复用上一个 dp[i][j] = dp[i - 1][j - 1]
// 2 word1[i - 1] !== word[j - 1] 三种情况取最小值 + 1 (进行一步操作)
// 2.1 dp[i - 1][j] 插入
// 2.2 dp[i][j - 1] 删除
// 2.3 dp[i - 1][j - 1] 替换
const len1 = word1.length;
const len2 = word2.length;
let dp = Array.from({length: len1 + 1}, num => new Array(len2 + 1).fill(0));
// 初始化 都是插入的情况
for (let i = 0; i <= len1; i++) {
dp[i][0] = i;
}
for (let j = 0; j <= len2; j++) {
dp[0][j] = j;
}
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
if (word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1;
}
}
}
return dp[len1][len2];
};
279. 完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
function numSquares(n: number): number {
// 动态规划 d[i] = min(d[i], d[i - x * x]) 所有可能的x,最坏情况是每个都是1相加得来(最大值)
const dp = [...new Array(n + 1)].map((_, index) => index)
for (let i = 1; i <= n; i++) {
for (let j = 1; j * j <= i; j++) {
dp[i] = Math.min(dp[i], dp[i - j * j] + 1)
}
}
return dp[n]
};
贪心
121. 买卖股票的最佳时机
难度简单
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
思路:每一次更新最小值和最大值
排序
冒泡
思路:经典排序方法,双重循环,每一轮会把一个最大值冒泡到最后,第二层即为0到length-i-1(因为每i就排好了i个最大值),通过j和j+1进行交换。优化:把temp放外层,以及设置falg,如果第二层没有进行交换说明目前已经是升序了,break即可
var sortArray = function(nums) {
let flag;
let temp;
for (let i=0;i<nums.length;i++){
flag = false;
for (let j=0;j<nums.length-i-1;j++){
if (nums[j+1]<nums[j]){
flag = true;
temp = nums[j+1];
nums[j+1] = nums[j];
nums[j] = temp;
}
}
if (!flag)
break;
}
return nums;
};
空间 o(1) 时间最好o(n) , 平均o(n*2)
选择
思路: 双重循环,每次找出最小值的下标,每轮将最小值和当前项进行交换
var sortArray = function(nums) {
for (let i=0;i<nums.length-1;i++){
let minIndex = i;
for (let j=i;j<nums.length;j++){
if (nums[j]<nums[minIndex])
minIndex = j;
}
[nums[minIndex],nums[i]] = [nums[i],nums[minIndex]];
}
return nums;
};
快排
时间复杂度: 递归的次数 * 递归的程度
最好的情况log2n ,每次双指针,n
n*logn
/**
* 快速排序函数
* @param nums 要排序的数组
* @returns 排序后的数组
*/
function sortArray(nums: number[]): number[] {
// 如果数组长度小于等于1,已经是排序状态
if (nums.length <= 1) return nums;
// 调用快速排序的主函数
quickSort(nums, 0, nums.length - 1);
return nums;
}
/**
* 快速排序的主函数
* @param arr 要排序的数组
* @param left 左边界
* @param right 右边界
*/
function quickSort(arr: number[], left: number, right: number): void {
// 递归终止条件
if (left >= right) return;
// 获取分区点
const pivotIndex = partition(arr, left, right);
// 递归排序左右两部分
quickSort(arr, left, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, right);
}
/**
* 分区函数
* @param arr 数组
* @param left 左边界
* @param right 右边界
* @returns 基准元素的最终位置
*/
function partition(arr: number[], left: number, right: number): number {
// 随机选择一个元素作为基准,并将其交换到左边界位置
const randomIndex = Math.floor(Math.random() * (right - left + 1)) + left;
[arr[left], arr[randomIndex]] = [arr[randomIndex], arr[left]];
// 选择第一个元素作为基准
const pivot = arr[left];
// 初始化指针
let i = left + 1;
let j = right;
// 分区过程
while (true) {
// 从左向右找到第一个大于等于基准的元素
while (i <= j && arr[i] < pivot) i++;
// 从右向左找到第一个小于等于基准的元素
while (i <= j && arr[j] > pivot) j--;
// 如果两个指针相遇或交叉,结束循环
if (i >= j) break;
// 交换找到的两个元素
[arr[i], arr[j]] = [arr[j], arr[i]];
// 移动指针继续查找
i++;
j--;
}
// 将基准元素放到正确的位置(j指向的位置)
[arr[left], arr[j]] = [arr[j], arr[left]];
// 返回基准元素的最终位置
return j;
}
堆排序 nlogn
function sortArray(nums: number[]): number[] {
const n = nums.length
// 构建最大堆,从最后一个非叶子节点开始,叶子节点 >= n/2
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapify(nums, n, i)
}
// 每次从堆顶取最大值,交换到最后一个,size--
for (let i = n - 1; i > 0; i--) {
[nums[0], nums[i]] = [nums[i], nums[0]]
heapify(nums, i, 0)
}
return nums
};
// 下沉调整维持堆排序
const heapify = (arr: number[], heapSize: number, i: number) => {
let largest = i
let left = 2 * i + 1
let right = 2 * i + 2
if (left < heapSize && arr[largest] < arr[left]) {
largest = left
}
if (right < heapSize && arr[largest] < arr[right]) {
largest = right
}
if (largest !== i) {
[arr[largest], arr[i]] = [arr[i], arr[largest]]
heapify(arr, heapSize, largest)
}
}
双指针 || 滑动窗口
9. 回文数
难度简单1811收藏分享切换为英文接收动态反馈
给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。
回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
- 例如,
121是回文,而123不是。
思路: 双指针,头和尾,如果不同直接false;
var isPalindrome = function(x) {
if (x<0)
return false;
let str = (''+x);
let left = 0;
let right = str.length-1;
while (left<right){
if (str[left]!==str[right])
return false;
left++;
right--;
}
return true;
};
思路二:翻转,相同true,不同false
var isPalindrome = function(x) {
if (x<0)
return false;
return x.toString() == x.toString().split('').reverse().join('');
};
209. 长度最小的子数组
难度中等
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0 。
思路: 滑动窗口,如果和大于等于则记录当前min并缩减左边,如果小于则扩张右边。
function minSubArrayLen(target: number, nums: number[]): number {
let result = Infinity
let left = 0
let sum = 0
const n = nums.length
// 遍历每个右边界
for (let right = 0; right < n; right++) {
sum += nums[right]
// 寻找最优左边界
while(sum >= target) {
result = Math.min(result, right - left + 1)
// 左边缩减,同步边界和值
sum -= nums[left]
left++
}
}
return result === Infinity ? 0 : result
};
5. 最长回文子串
难度中等
给你一个字符串 s,找到 s 中最长的回文子串。
思路: 中心扩散,每次替换最长的回文子串,tip:记得循环条件,从左到右,因此left大于等于0;
var longestPalindrome = function(s) {
let result = '';
let helper = (left,right)=>{
while(left>=0&&s[left]==s[right]){
left--;
right++;
}
let strlen = right-1 - (left+1)+1;
if (strlen>result.length){
result = s.slice(left+1,right);
}
}
for (let i=0;i<s.length;i++){
helper(i,i);
helper(i,i+1);
}
return result;
};
3. 无重复字符的最长子串
难度中等
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
思路: 创建滑动窗口,每次从开头推出到重复的字符(如果有),push字符并记录最长长度
var lengthOfLongestSubstring = function(s) {
let window = [];
let max = 0;
for (let i=0;i<s.length;i++){
if (window.indexOf(s[i])!==-1){
window.splice(0,window.indexOf(s[i])+1);
}
window.push(s[i]);
max = Math.max(max,window.length);
}
return max;
};
效率更高的滑动窗口
function lengthOfLongestSubstring(s: string): number {
let result = 0
let left = 0
const n = s.length
const set = new Set<string>()
for (let right = 0; right < n; right++) {
while (set.has(s[right])) {
set.delete(s[left])
left++
}
set.add(s[right])
result = Math.max(result, right - left + 1)
}
return result
};
15. 三数之和
难度中等
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 *a,b,c ,*使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
**注意:**答案中不可以包含重复的三元组。
思路: 数组排序,每次固定i,left为i+1,right为length-1 ,小了则left++,大了right-- —— 注意边界条件
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
let res = [];
if (nums.length < 3) {
return res;
}
nums = nums.sort((a, b) => a - b);
for (let i = 0; i < nums.length; i++) {
if (nums[i] > 0) {
break;
}
if (i > 0 && nums[i] === nums[i - 1]) {
continue;
}
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
if (nums[i] + nums[left] + nums[right] === 0) {
const arr = [nums[i], nums[left], nums[right]];
res.push(arr);
while (nums[left + 1] === nums[left]) {
left++;
}
while (nums[right - 1] === nums[right]) {
right--;
}
left++;
right--;
}
else if (nums[i] + nums[left] + nums[right] > 0) {
right--;
}
else {
left++;
}
}
}
return res;
};
239. 滑动窗口最大值
难度困难
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
思路: window是单向递减的存储下标的数组(因为由下标取值很容易,反过来不那么容易),每次都会把window末尾小于要加入的部分pop(这样就做到了第一个一定是最大的),同时边界要注意窗口和push结果。
function maxSlidingWindow(nums: number[], k: number): number[] {
const result = []
// 使用队列保证了窗口的顺序,存储单调递减的下标
const queue = []
// 遍历右边界
for (let i = 0; i < nums.length; i++) {
// 移除不在窗口的下标
if (queue.length > 0 && queue[0] <= i - k) {
queue.shift()
}
// 保证单调递减
while (queue.length > 0 && nums[queue[queue.length - 1]] < nums[i]) {
queue.pop()
}
queue.push(i)
if (i + 1 >= k) {
result.push(nums[queue[0]])
}
}
return result
};
快慢指针
剑指 Offer 22. 链表中倒数第k个节点
难度简单
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
思路: 快fast先走k步,然后fast和slow同时遍历,fast走出,slow即为答案。
var getKthFromEnd = function(head, k) {
let slow = head;
let fast = head;
while(k--){
fast = fast.next;
}
while(fast){
slow = slow.next;
fast = fast.next;
}
return slow;
};
141. 环形链表
难度简单
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
思路:快慢指针,有环会相遇
var hasCycle = function(head) {
let slow = head;
let fast = head;
while(fast&&fast.next){
fast = fast.next.next;
slow = slow.next;
if (fast==slow)
return true;
}
return false;
};
234. 回文链表
难度简单
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
思路: 可以使用On数组来存储链表,再进行一一对比; 这里不开辟数组,通过快慢指针找到中间节点,对后半截进行翻转并一一对比即可。
var isPalindrome = function(head) {
let newhead = new ListNode(-1,head);
let fast = slow = newhead;
while (fast&&fast.next){
fast = fast.next.next;
slow = slow.next;
}
let p = slow.next;
let reverse = (root)=>{
if (!root||!root.next)
return root;
let tail = reverse(root.next);
root.next.next = root;
root.next = null;
return tail;
}
p = reverse(p);
while(p){
if (p.val!==head.val)
return false;
p = p.next;
head = head.next;
}
return true;
};