本人的算法总结(js)

276 阅读1小时+

持续分类别记录算法题总结

二叉树

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);
    }
};

思路二:递归,核心在递归左右子树,两种情况:

  1. 如果左右子树都有最近p,q公共祖先,说明当前的root是p,q的最近公共祖先
  2. 左右子树单独有结果,将这个左右子树往外层吐(满足最近的公共祖先)
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. 二叉树的直径

难度简单

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

思路:某节点的直径为左右子树的高度和,题目即求所有节点中左右子树的最大高度和。

  1. 定义求深度函数,每次对左右子树调用,更新最大值
  2. 或者 进行深度遍历,遍历过程中拿到左右子树的深度,然后更新最大值,每次递归返回本节点的最高深度。

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. 相同的树

难度简单

给你两棵二叉树的根节点 pq ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

思路:和判断对称二叉树有相似的地方,构造两个参数的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. 从前序与中序遍历序列构造二叉树

难度中等

给定两个整数数组 preorderinorder ,其中 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收藏分享切换为英文接收动态反馈

给定两个整数数组 inorderpostorder ,其中 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. 相交链表

难度简单

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 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 和两个整数 leftright ,其中 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 高的元素。你可以按 任意顺序 返回答案。

思路:

  1. 记录次序是map的思路,没有赋值1,有加1。前k高频是最小堆
  2. 实现带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. 最小栈

难度简单

设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。

实现 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 ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。

思路:利用栈,开入栈,闭压栈,判断,最后判断栈是否为空

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. 用两个栈实现队列

难度简单

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTaildeleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,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)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。

实现 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 的完全平方数的最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

思想: 构造一个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) 表示)形成的序列称为 斐波那契数列 。该数列由 01 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0F(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. 螺旋矩阵

难度中等

给你一个 mn 列的矩阵 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. 字符串相加

难度简单

给定两个字符串形式的非负整数 num1num2 ,计算它们的和并同样以字符串形式返回。

你不能使用任何內建的用于处理大整数的库(比如 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. 比较版本号

难度中等

给你两个版本号 version1version2 ,请你比较它们。

版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.330.1 都是有效的版本号。

比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 010 < 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. 合并两个有序数组

难度简单

给你两个按 非递减顺序 排列的整数数组 nums1nums2,另有两个整数 mn ,分别表示 nums1nums2 中的元素数目。

请你 合并 nums2nums1 中,使合并后的数组同样按 非递减顺序 排列。

**注意:**最终,合并后数组不应由函数返回,而是存储在数组 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 ,则应该 逐出 最久未使用的关键字。

函数 getput 必须以 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] 范围内(包括 1n),可知至少存在一个重复的整数。

假设 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 小时内吃掉所有香蕉的最小速度 kk 为整数)。

思路: 二分查找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 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

每次爬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收藏分享切换为英文接收动态反馈

给你两个单词 word1word2请返回将 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 的完全平方数的最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,149 和 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;
};