神族九帝's blog 神族九帝's blog
首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)

神族九帝,永不言弃

首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)
  • 面试突击

  • 复习指导

  • HTML

  • CSS

  • JavaScript

  • 设计模式

  • 浏览器

  • 手写系列

  • Vue

  • Webpack

  • Http

  • 前端优化

  • 项目

  • 面试真题

  • 算法

    • 三天学会算法
      • 两数之和
      • 三数之和
      • 回文字符串
      • 链表的合并
      • 链表的删除
      • 链表删除,dummy
      • 总结
      • 快慢指针
      • 链表的反转
      • 局部反转一个链表
      • 环形链表
        • 判断换的起点
        • 快慢指针思路找起点
      • 有效括号
      • 每日温度问题
      • 如何用栈实现队列
      • 滑动窗口的最大值
        • 双指针+遍历法
        • 双端队列
      • 深度优先搜索和广度优先搜索
      • 全排列
      • 跳过没有看完的
      • 迭代法先序遍历
      • 迭代法后序遍历
      • 迭代法中序遍历
      • 层序遍历延伸
      • 反转二叉树
      • 搜索二叉树,插入新节点
      • 删除二叉搜索树中的节点
      • 二叉搜索树的验证
      • 将排序数组转化为二叉搜索树
      • 平衡二叉树跳过
      • 冒泡排序
      • 选择排序
      • 插入排序
      • 归并排序
      • 快速排序
      • 动态规划
        • 最少硬币数目
        • 背包问题
        • 最长上升子序列
      • 谷歌真题
        • 最长回文子串问题
    • 数组
    • 字节飞书
  • 精选文章

  • 八股文

  • 前端工程化

  • 面试题
  • 算法
wu529778790
2021-11-19

三天学会算法

算法的设计真的很美,有时候会佩服设计的人真厉害

  • https://www.iamshuaidi.com/ (opens new window)
  • https://codetop.cc/#/home (opens new window)
  • https://juejin.cn/book/6844733800300150797 (opens new window)
  • https://leetcode-cn.com/leetbook/detail/top-interview-questions-easy/ (opens new window)
  • http://febook.hzfe.org/awesome-interview/ (opens new window)

91 天学算法 Leetcode 图解题解集合 notion 笔记主要是 #Leetcode 经典题目的解析,包括思路,代码实现和复杂度分析,部分题解包含手绘图解。

  • https://suukii.notion.site/ca5e6d9d43704a9d82e44758636642d6?v=e7bdde0a51e9424fa96b876e78b03958 (opens new window)

  • https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/ (opens new window)

数据结构和算法-思维导图

# 两数之和

https://leetcode-cn.com/problems/two-sum/ (opens new window)

真题描述: 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

给定 nums = [2, 7, 11, 15], target = 9 因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]

function twoSum(nums, target) {
  const diff = new Map();
  const len = nums.length;
  for (let i = 0; i < len; i++) {
    const num = nums[i];
    // target-当前值的差值是否存在
    if (diff.get(target - num) !== undefined) {
      return [diff.get(target - num), i];
    }
    // 不存在则记录当前值
    diff.set(num, i);
  }
}

# 三数之和

https://leetcode-cn.com/problems/3sum/ (opens new window)

真题描述:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

const threeSum = function (nums, target) {
  // 用于存放数组结果
  let res = [];
  // 排序
  nums = nums.sort((a, b) => a - b);
  // 缓存length
  const len = nums.length;
  for (let i = 0; i < len; i++) {
    // 左指针j
    let j = i + 1;
    // 右指针k
    let k = len - 1;
    // 如果遇到重复的数字,则跳过
    if (i > 0 && nums[i] === nums[i - 1]) {
      continue;
    }
    while (j < k) {
      // 三数之和小于0,左指针前进
      if (nums[i] + nums[j] + nums[k] < 0) {
        j++;
        // 处理左指针元素重复的情况
        while (nums[j] === nums[j - 1]) {
          j++;
        }
      } else if (nums[i] + nums[j] + nums[k] > 0) {
        // 三数之和大于0,右指针后退
        k--;
        while (nums[k] === nums[k + 1]) {
          k--;
        }
      } else {
        // 得到的目标组合,推入结果数组
        res.push([nums[i], nums[j], nums[k]]);
        // 左右指针一起前进
        j++;
        k--;
        // 若左指针元素重复,跳过
        while (nums[j] === nums[j - 1]) {
          j++;
        }
        // 若有指针元素重复,跳过
        while (nums[k] === nums[k + 1]) {
          k--;
        }
      }
    }
  }
  return res;
};
console.log(threeSum([-1, 0, 1, 2, -1, -4]));

# 回文字符串

https://leetcode-cn.com/problems/RQku0D/ (opens new window)

真题描述:给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串

const validPalindrome = function (s) {
  // 缓存字符串长度
  const len = s.length;
  let i = 0;
  let j = len - 1;
  while (i < j && s[i] === s[j]) {
    i++;
    j--;
  }
  // 尝试判断跳过左指针元素后,字符串是否回文
  if (isPalindrome(i + 1, j)) {
    return true;
  }
  // 常识判断跳过右指针元素后,字符串是否回文
  if (isPalindrome(i, j - 1)) {
    return true;
  }
  // 工具方法,判断字符串是否回文
  function isPalindrome(st, ed) {
    while (st < ed) {
      if (s[st] !== s[ed]) {
        return false;
      }
      st++;
      ed--;
    }
    return true;
  }
  return false;
};
console.log(validPalindrome("abca"));

# 链表的合并

https://leetcode-cn.com/problems/merge-two-sorted-lists/ (opens new window)

真题描述:将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的。

输入:1->2->4, 1->3->4 输出:1->1->2->3->4->4

function ListNode(val, next) {
  this.val = val === undefined ? 0 : val;
  this.next = next === undefined ? null : next;
}

const mergeTwoLists = function (l1, l2) {
  // 定义头结点,确保链表可以访问
  let head = new ListNode();
  // cur 这里就是咱们的针
  let cur = head;
  // 针开始穿梭
  while (l1 && l2) {
    // 如果l1的节点值较小
    if (l1.val <= l2.val) {
      // 先串起l1的节点
      cur.next = l1;
      // l1往前一步
      l1 = l1.next;
    } else {
      cur.next = l2;
      l2 = l2.next;
    }
    // 针在串起一个节点后,也会往前走一步
    cur = cur.next;
  }
  cur.next = l1 !== null ? l1 : l2;
  return head.next;
};
const l1 = {
  val: 1,
  next: {
    val: 2,
    next: {
      val: 3,
      next: null,
    },
  },
};
const l2 = {
  val: 1,
  next: {
    val: 3,
    next: {
      val: 4,
      next: null,
    },
  },
};
console.log(mergeTwoLists(l1, l2));

# 链表的删除

https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/ (opens new window)

https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/ (opens new window)

真题描述:给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。

输入: 1->1->2 输出: 1->2 示例 2: 输入: 1->1->2->3->3 输出: 1->2->3

function ListNode(val) {
  this.val = val;
  this.next = null;
}
const deleteDuplicates = function (head) {
  let cur = head;
  while (cur !== null && cur.next !== null) {
    if (cur.val === cur.next.val) {
      cur.next = cur.next.next;
    } else {
      cur = cur.next;
    }
  }
  return head;
};
console.log(
  deleteDuplicates({
    val: 1,
    next: {
      val: 1,
      next: {
        val: 3,
        next: null,
      },
    },
  })
);

# 链表删除,dummy

真题描述:给定一个排序链表,删除所有含有重复数字的结点,只保留原始链表中 没有重复出现的数字。

输入: 1->2->3->3->4->4->5 输出: 1->2->5 示例 2: 输入: 1->1->1->2->3 输出: 2->3

function ListNode(val) {
  this.val = val;
  this.next = null;
}
const deleteDuplicates = function (head) {
  if (!head || !head.next) {
    return head;
  }
  let dummy = new ListNode();
  dummy.next = head;

  let cur = dummy;
  while (cur.next && cur.next.next) {
    if (cur.next.val === cur.next.next.val) {
      let val = cur.next.val;
      cur.next = cur.next.next.next;
      while (cur.next && cur.next.val === val) {
        cur.next = cur.next.next;
      }
    } else {
      cur = cur.next;
    }
  }
  return dummy.next;
};

console.log(
  deleteDuplicates({
    val: 1,
    next: {
      val: 1,
      next: {
        val: 1,
        next: {
          val: 2,
          next: {
            val: 3,
            next: null,
          },
        },
      },
    },
  })
);

# 总结

  • head 是链表
  • cur 是指针
  • dummy 是链表的前一项的虚拟节点,当 head 有可能发生改变时使用

# 快慢指针

https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/ (opens new window)

真题描述:给定一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

给定一个链表: 1->2->3->4->5, 和 n = 2. 当删除了倒数第二个结点后,链表变为 1->2->3->5.

function ListNode(val) {
  this.val = val;
  this.next = null;
}
const removeNthFromEnd = function (head, n) {
  // 初始化 dummy 结点
  const dummy = new ListNode();
  // dummy 指向头结点
  dummy.next = head;
  // 初始化快慢指针,均指向 dummy
  let fast = dummy;
  let slow = dummy;

  // 快指针闷头走 n 步
  while (n !== 0) {
    fast = fast.next;
    n--;
  }

  // 快慢指针一起走
  while (fast.next) {
    fast = fast.next;
    slow = slow.next;
  }

  // 慢指针删除自己的后继结点
  slow.next = slow.next.next;
  // 返回头结点
  return dummy.next;
};
console.log(
  removeNthFromEnd(
    {
      val: 1,
      next: {
        val: 2,
        next: {
          val: 3,
          next: {
            val: 4,
            next: {
              val: 5,
              next: null,
            },
          },
        },
      },
    },
    2
  )
);

# 链表的反转

https://leetcode-cn.com/problems/reverse-linked-list/ (opens new window)

function ListNode(val, next) {
  this.val = val === undefined ? 0 : val;
  this.next = next === undefined ? null : next;
}
const reverseListNode = function (head) {
  if (!head || head.next) {
    return head;
  }
  let pre = null;
  let cur = head;
  while (cur !== null) {
    let next = cur.next;
    cur.next = pre;
    pre = cur;
    cur = next;
  }
  return pre;
};
console.log(
  reverseListNode(
    {
      val: 1,
      next: {
        val: 2,
        next: {
          val: 3,
          next: {
            val: 4,
            next: {
              val: 5,
              next: null,
            },
          },
        },
      },
    },
    2
  )
);

# 局部反转一个链表

https://leetcode-cn.com/problems/reverse-linked-list-ii/ (opens new window)

真题描述:反转从位置 m 到 n 的链表。请使用一趟扫描完成反转

输入: 1->2->3->4->5->NULL, m = 2, n = 4 输出: 1->4->3->2->5->NULL

// 入参是头结点、m、n
const reverseBetween = function (head, m, n) {
  // 定义pre、cur,用leftHead来承接整个区间的前驱结点
  let pre, cur, leftHead;
  // 别忘了用 dummy 嗷
  const dummy = new ListNode();
  // dummy后继结点是头结点
  dummy.next = head;
  // p是一个游标,用于遍历,最初指向 dummy
  let p = dummy;
  // p往前走 m-1 步,走到整个区间的前驱结点处
  for (let i = 0; i < m - 1; i++) {
    p = p.next;
  }
  // 缓存这个前驱结点到 leftHead 里
  leftHead = p;
  // start 是反转区间的第一个结点
  let start = leftHead.next;
  // pre 指向start
  pre = start;
  // cur 指向 start 的下一个结点
  cur = pre.next;
  // 开始重复反转动作
  for (let i = m; i < n; i++) {
    let next = cur.next;
    cur.next = pre;
    pre = cur;
    cur = next;
  }
  //  leftHead 的后继结点此时为反转后的区间的第一个结点
  leftHead.next = pre;
  // 将区间内反转后的最后一个结点 next 指向 cur
  start.next = cur;
  // dummy.next 永远指向链表头结点
  return dummy.next;
};

# 环形链表

就是立 flag,等到下次发现 flag 说明就是个环

https://leetcode-cn.com/problems/linked-list-cycle/ (opens new window)

/**
 * @param {ListNode} head
 * @return {boolean}
 */
// 入参是头结点
const hasCycle = function (head) {
  // 只要结点存在,那么就继续遍历
  while (head) {
    // 如果 flag 已经立过了,那么说明环存在
    if (head.flag) {
      return true;
    } else {
      // 如果 flag 没立过,就立一个 flag 再往
      head.flag = true;
      head = head.next;
    }
  }
  return false;
};

# 判断换的起点

第一次发现 flag 的时候就是环的起点

/**
 * @param {ListNode} head
 * @return {boolean}
 */
// 入参是头结点
const hasCycle = function (head) {
  // 只要结点存在,那么就继续遍历
  while (head) {
    // 如果 flag 已经立过了,那么说明环存在
    if (head.flag) {
      return head;
    } else {
      // 如果 flag 没立过,就立一个 flag 再往
      head.flag = true;
      head = head.next;
    }
  }
  return false;
};

# 快慢指针思路找起点

定义慢指针 slow,快指针 fast。两者齐头并进, slow 一次走一步、fast 一次 走两步。这样如果它们是在一个有环的链表里移动,一定有相遇的时刻。这个原理证明起来也比较简单:我们假设移动的次数为 t,slow 移动的路程就是 t,fast 移动的路程为 2t,假如环的长度为 s,那么当下面这个条件:

2t - t = s

也就是:

t = s

满足时,slow 和 fast 就一定会相遇。反之,如果两者没有相遇,同时 fast 遍历到了链表的末尾,发现 next 指针指向 null,则链表中不存在环。

https://leetcode-cn.com/problems/linked-list-cycle-ii/solution/tu-jie-kuai-man-zhi-zhen-ji-qiao-yuan-li-5tz0/ (opens new window)

20211122102755

20211122102812

var detectCycle = function (head) {
  // 快慢指针初始化指向 head
  let slow = head;
  let fast = head;
  // 快指针走到末尾时停止
  while (fast && fast.next) {
    // 慢指针走一步,快指针走两步
    slow = slow.next;
    fast = fast.next.next;
    // 快慢指针相遇,说明含有环
    if (slow == fast) {
      // 任一一节点指向头节点
      fast = head;
      // 同步向前进
      while (fast != slow) {
        fast = fast.next;
        slow = slow.next;
      }
      // 返回入口节点
      return fast;
    }
  }
  // 不包含环
  return false;
};

# 有效括号

https://leetcode-cn.com/problems/valid-parentheses/ (opens new window)

题目描述:给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效

左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 注意空字符串可被认为是有效字符串。

示例 1:

输入: "()" 输出: true

示例 2:

输入: "()[]{}" 输出: true

示例 3:

输入: "(]" 输出: false

示例 4:

输入: "([)]" 输出: false

示例 5:

输入: "{[]}" 输出: true

// 用一个 map 来维护左括号和右括号的对应关系
const leftToRight = {
  "(": ")",
  "[": "]",
  "{": "}",
};

/**
 * @param {string} s
 * @return {boolean}
 */
const isValid = function (s) {
  // 结合题意,空字符串无条件判断为 true
  if (!s) {
    return true;
  }
  const leftToRight = {
    "(": ")",
    "{": "}",
    "[": "]",
  };
  const stack = [];
  for (let i = 0; i < s.length; i++) {
    if (["(", "{", "["].includes(s[i])) {
      stack.push(leftToRight[s[i]]);
    } else {
      if (s[i] !== stack.pop()) {
        return false;
      }
    }
  }
  return !stack.length;
};

# 每日温度问题

https://leetcode-cn.com/problems/daily-temperatures/ (opens new window)

题目描述: 根据每日气温列表,请重新生成一个列表,对应位置的输出是需要再等待多久温度才会升高超过该日的天数。如果之后都不会升高,请在该位置用 0 来代替。

例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。

理解动图:https://www.bilibili.com/video/BV12t4y1274o/ (opens new window)

/**
 * @param {number[]} temperatures
 * @return {number[]}
 */
var dailyTemperatures = function (temperatures) {
  const stack = [];
  const res = new Array(temperatures.length).fill(0);
  for (let i = 0; i < temperatures.length; i++) {
    while (
      stack.length &&
      temperatures[stack[stack.length - 1]] < temperatures[i]
    ) {
      const top = stack.pop();
      res[top] = i - top;
    }
    stack.push(i);
  }
  return res;
};

# 如何用栈实现队列

https://leetcode-cn.com/problems/implement-queue-using-stacks/ (opens new window)

/**
 * 初始化构造函数
 */
const MyQueue = function () {
  // 初始化两个栈
  this.stack1 = [];
  this.stack2 = [];
};

/**
 * Push element x to the back of queue.
 * @param {number} x
 * @return {void}
 */
MyQueue.prototype.push = function (x) {
  // 直接调度数组的 push 方法
  this.stack1.push(x);
};

/**
 * Removes the element from in front of queue and returns that element.
 * @return {number}
 */
MyQueue.prototype.pop = function () {
  // 假如 stack2 为空,需要将 stack1 的元素转移进来
  if (this.stack2.length <= 0) {
    // 当 stack1 不为空时,出栈
    while (this.stack1.length !== 0) {
      // 将 stack1 出栈的元素推入 stack2
      this.stack2.push(this.stack1.pop());
    }
  }
  // 为了达到逆序的目的,我们只从 stack2 里出栈元素
  return this.stack2.pop();
};

/**
 * Get the front element.
 * @return {number}
 * 这个方法和 pop 唯一的区别就是没有将定位到的值出栈
 */
MyQueue.prototype.peek = function () {
  if (this.stack2.length <= 0) {
    // 当 stack1 不为空时,出栈
    while (this.stack1.length != 0) {
      // 将 stack1 出栈的元素推入 stack2
      this.stack2.push(this.stack1.pop());
    }
  }
  // 缓存 stack2 的长度
  const stack2Len = this.stack2.length;
  return stack2Len && this.stack2[stack2Len - 1];
};

/**
 * Returns whether the queue is empty.
 * @return {boolean}
 */
MyQueue.prototype.empty = function () {
  // 若 stack1 和 stack2 均为空,那么队列空
  return !this.stack1.length && !this.stack2.length;
};

# 滑动窗口的最大值

https://leetcode-cn.com/problems/sliding-window-maximum/ (opens new window)

题目描述:给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3 输出: [3,3,5,5,6,7]

解释: 滑动窗口的位置


[1 3 -1] -3 5 3 6 7

1 [3 -1 -3] 5 3 6 7

1 3 [-1 -3 5] 3 6 7

1 3 -1 [-3 5 3] 6 7

1 3 -1 -3 [5 3 6] 7

1 3 -1 -3 5 [3 6 7]

最大值分别对应:

3 3 5 5 6 7

提示:你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。

# 双指针+遍历法

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var maxSlidingWindow = function (nums, k) {
  let left = 0;
  let right = k - 1;
  let res = [];
  const calcMax = function (arr, left, right) {
    let maxNum = arr[left];
    for (let i = left; i <= right; i++) {
      if (nums[i] > maxNum) {
        maxNum = nums[i];
      }
    }
    return maxNum;
  };
  while (right < nums.length) {
    let max = calcMax(nums, left, right);
    res.push(max);
    left++;
    right++;
  }
  return res;
};

# 双端队列

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var maxSlidingWindow = function (nums, k) {
  // 缓存数组的长度
  const len = nums.length;
  // 初始化结果数组
  const res = [];
  // 初始化双端队列
  const deque = [];
  // 开始遍历数组
  for (let i = 0; i < len; i++) {
    // 当队尾元素小于当前元素时
    while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
      // 将队尾元素(索引)不断出队,直至队尾元素大于等于当前元素
      deque.pop();
    }
    // 入队当前元素索引(注意是索引)
    deque.push(i);
    // 当队头元素的索引已经被排除在滑动窗口之外时
    while (deque.length && deque[0] <= i - k) {
      // 将队头元素索引出队
      deque.shift();
    }
    // 判断滑动窗口的状态,只有在被遍历的元素个数大于 k 的时候,才更新结果数组
    if (i >= k - 1) {
      res.push(nums[deque[0]]);
    }
  }
  // 返回结果数组
  return res;
};

# 深度优先搜索和广度优先搜索

https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-lcof/ (opens new window)

https://leetcode-cn.com/problems/binary-tree-level-order-traversal/ (opens new window)

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。

例如: 给定二叉树: [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回:

[3,9,20,15,7]

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var levelOrder = function (root) {
  const res = [];
  if (!root) {
    return res;
  }
  const queue = [];
  queue.push(root);
  while (queue.length !== 0) {
    const top = queue.shift();
    res.push(top.val);
    if (top.left) {
      queue.push(top.left);
    }
    if (top.right) {
      queue.push(top.right);
    }
  }
  return res;
};

# 全排列

https://leetcode-cn.com/problems/permutations/78 (opens new window).

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
// 入参是一个数组
const permute = function (nums) {
  // 缓存数组的长度
  const len = nums.length;
  // curr 变量用来记录当前的排列内容
  const curr = [];
  // res 用来记录所有的排列顺序
  const res = [];
  // visited 用来避免重复使用同一个数字
  const visited = {};
  // 定义 dfs 函数,入参是坑位的索引(从 0 计数)
  function dfs(nth) {
    // 若遍历到了不存在的坑位(第 len+1 个),则触碰递归边界返回
    if (nth === len) {
      // 此时前 len 个坑位已经填满,将对应的排列记录下来
      res.push(curr.slice());
      return;
    }
    // 检查手里剩下的数字有哪些
    for (let i = 0; i < len; i++) {
      // 若 nums[i] 之前没被其它坑位用过,则可以理解为“这个数字剩下了”
      if (!visited[nums[i]]) {
        // 给 nums[i] 打个“已用过”的标
        visited[nums[i]] = 1;
        // 将nums[i]推入当前排列
        curr.push(nums[i]);
        // 基于这个排列继续往下一个坑走去
        dfs(nth + 1);
        // nums[i]让出当前坑位
        curr.pop();
        // 下掉“已用过”标识
        visited[nums[i]] = 0;
      }
    }
  }
  // 从索引为 0 的坑位(也就是第一个坑位)开始 dfs
  dfs(0);
  return res;
};

# 跳过没有看完的

https://juejin.cn/book/6844733800300150797/section/6844733800358871048 (opens new window)

# 迭代法先序遍历

https://leetcode-cn.com/problems/binary-tree-preorder-traversal/ (opens new window)

使用迭代法,while,要注意后进先出,所以这里先判断 right,和递归法相反

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
const preorderTraversal = function (root) {
  // 定义结果数组
  const res = [];
  // 处理边界条件
  if (!root) {
    return res;
  }
  // 初始化栈结构
  const stack = [];
  // 首先将根结点入栈
  stack.push(root);
  // 若栈不为空,则重复出栈、入栈操作
  while (stack.length) {
    // 将栈顶结点记为当前结点
    const cur = stack.pop();
    // 当前结点就是当前子树的根结点,把这个结点放在结果数组的尾部
    res.push(cur.val);
    // 若当前子树根结点有右孩子,则将右孩子入栈
    if (cur.right) {
      stack.push(cur.right);
    }
    // 若当前子树根结点有左孩子,则将左孩子入栈
    if (cur.left) {
      stack.push(cur.left);
    }
  }
  // 返回结果数组
  return res;
};

# 迭代法后序遍历

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
const preorderTraversal = function (root) {
  // 定义结果数组
  const res = [];
  // 处理边界条件
  if (!root) {
    return res;
  }
  // 初始化栈结构
  const stack = [];
  // 首先将根结点入栈
  stack.push(root);
  // 若栈不为空,则重复出栈、入栈操作
  while (stack.length) {
    // 将栈顶结点记为当前结点
    const cur = stack.pop();
    // 当前结点就是当前子树的根结点,把这个结点放在结果数组的尾部
    res.push(cur.val);
    // 若当前子树根结点有右孩子,则将右孩子入栈
    if (cur.right) {
      stack.push(cur.right);
    }
    // 若当前子树根结点有左孩子,则将左孩子入栈
    if (cur.left) {
      stack.push(cur.left);
    }
  }
  // 返回结果数组
  return res;
};

# 迭代法中序遍历

https://leetcode-cn.com/problems/binary-tree-inorder-traversal/submissions/ (opens new window)

刚开始 cur 当做向左的游标,left 到头的时候,又充当向右的游标

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
const inorderTraversal = function (root) {
  // 定义结果数组
  const res = [];
  // 初始化栈结构
  const stack = [];
  // 用一个 cur 结点充当游标
  let cur = root;
  // 当 cur 不为空、或者 stack 不为空时,重复以下逻辑
  while (cur || stack.length) {
    // 这个 while 的作用是把寻找最左叶子结点的过程中,途径的所有结点都记录下来
    while (cur) {
      // 将途径的结点入栈
      stack.push(cur);
      // 继续搜索当前结点的左孩子
      cur = cur.left;
    }
    // 取出栈顶元素
    cur = stack.pop();
    // 将栈顶元素入栈
    res.push(cur.val);
    // 尝试读取 cur 结点的右孩子
    cur = cur.right;
  }
  // 返回结果数组
  return res;
};

# 层序遍历延伸

https://leetcode-cn.com/problems/binary-tree-level-order-traversal/ (opens new window)

二叉树:[3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回其层序遍历结果:

[
  [3],
  [9,20],
  [15,7]
]
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
const levelOrder = function (root) {
  // 初始化结果数组
  const res = [];
  // 处理边界条件
  if (!root) {
    return res;
  }
  // 初始化队列
  const queue = [];
  // 队列第一个元素是根结点
  queue.push(root);
  // 当队列不为空时,反复执行以下逻辑
  while (queue.length) {
    // level 用来存储当前层的结点
    const level = [];
    // 缓存刚进入循环时的队列长度,这一步很关键,因为队列长度后面会发生改变
    const len = queue.length;
    // 循环遍历当前层级的结点
    for (let i = 0; i < len; i++) {
      // 取出队列的头部元素
      const top = queue.shift();
      // 将头部元素的值推入 level 数组
      level.push(top.val);
      // 如果当前结点有左孩子,则推入下一层级
      if (top.left) {
        queue.push(top.left);
      }
      // 如果当前结点有右孩子,则推入下一层级
      if (top.right) {
        queue.push(top.right);
      }
    }
    // 将 level 推入结果数组
    res.push(level);
  }
  // 返回结果数组
  return res;
};

# 反转二叉树

https://leetcode-cn.com/problems/invert-binary-tree/ (opens new window)

/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
const invertTree = function (root) {
  // 定义递归边界
  if (!root) {
    return root;
  }
  // 递归交换右孩子的子结点
  let right = invertTree(root.right);
  // 递归交换左孩子的子结点
  let left = invertTree(root.left);
  // 交换当前遍历到的两个左右孩子结点
  root.left = right;
  root.right = left;
  return root;
};

我自己是这样写的,也通过了

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var invertTree = function (root) {
  if (!root) {
    return root;
  }
  let right = root.right;
  let left = root.left;
  root.left = right;
  root.right = left;
  invertTree(root.right);
  invertTree(root.left);
  return root;
};

# 搜索二叉树,插入新节点

https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/submissions/ (opens new window)

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} val
 * @return {TreeNode}
 */
var insertIntoBST = function (root, val) {
  if (!root) {
    root = new TreeNode(val);
    return root;
  }
  if (root.val < val) {
    root.right = insertIntoBST(root.right, val);
  }
  if (root.val > val) {
    root.left = insertIntoBST(root.left, val);
  }
  return root;
};

# 删除二叉搜索树中的节点

https://leetcode-cn.com/problems/delete-node-in-a-bst/ (opens new window)

function deleteNode(root, n) {
  // 如果没找到目标结点,则直接返回
  if (!root) {
    return root;
  }
  // 定位到目标结点,开始分情况处理删除动作
  if (root.val === n) {
    // 若是叶子结点,则不需要想太多,直接删除
    if (!root.left && !root.right) {
      root = null;
    } else if (root.left) {
      // 寻找左子树里值最大的结点
      const maxLeft = findMax(root.left);
      // 用这个 maxLeft 覆盖掉需要删除的当前结点
      root.val = maxLeft.val;
      // 覆盖动作会消耗掉原有的 maxLeft 结点
      root.left = deleteNode(root.left, maxLeft.val);
    } else {
      // 寻找右子树里值最小的结点
      const minRight = findMin(root.right);
      // 用这个 minRight 覆盖掉需要删除的当前结点
      root.val = minRight.val;
      // 覆盖动作会消耗掉原有的 minRight 结点
      root.right = deleteNode(root.right, minRight.val);
    }
  } else if (root.val > n) {
    // 若当前结点的值比 n 大,则在左子树中继续寻找目标结点
    root.left = deleteNode(root.left, n);
  } else {
    // 若当前结点的值比 n 小,则在右子树中继续寻找目标结点
    root.right = deleteNode(root.right, n);
  }
  return root;
}

// 寻找左子树最大值
function findMax(root) {
  while (root.right) {
    root = root.right;
  }
  return root;
}

// 寻找右子树的最小值
function findMin(root) {
  while (root.left) {
    root = root.left;
  }
  return root;
}

# 二叉搜索树的验证

https://leetcode-cn.com/problems/validate-binary-search-tree/ (opens new window)

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isValidBST = function (root) {
  // 定义递归函数
  function dfs(root, minValue, maxValue) {
    // 若是空树,则合法
    if (!root) {
      return true;
    }
    // 若右孩子不大于根结点值,或者左孩子不小于根结点值,则不合法
    if (root.val <= minValue || root.val >= maxValue) return false;
    // 左右子树必须都符合二叉搜索树的数据域大小关系
    return (
      dfs(root.left, minValue, root.val) && dfs(root.right, root.val, maxValue)
    );
  }
  // 初始化最小值和最大值为极小或极大
  return dfs(root, -Infinity, Infinity);
};

# 将排序数组转化为二叉搜索树

https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree/ (opens new window)

/**
 * @param {number[]} nums
 * @return {TreeNode}
 */
const sortedArrayToBST = function (nums) {
  // 处理边界条件
  if (!nums.length) {
    return null;
  }

  // root 结点是递归“提”起数组的结果
  const root = buildBST(0, nums.length - 1);

  // 定义二叉树构建函数,入参是子序列的索引范围
  function buildBST(low, high) {
    // 当 low > high 时,意味着当前范围的数字已经被递归处理完全了
    if (low > high) {
      return null;
    }
    // 二分一下,取出当前子序列的中间元素
    const mid = Math.floor(low + (high - low) / 2);
    // 将中间元素的值作为当前子树的根结点值
    const cur = new TreeNode(nums[mid]);
    // 递归构建左子树,范围二分为[low,mid)
    cur.left = buildBST(low, mid - 1);
    // 递归构建左子树,范围二分为为(mid,high]
    cur.right = buildBST(mid + 1, high);
    // 返回当前结点
    return cur;
  }
  // 返回根结点
  return root;
};

# 平衡二叉树跳过

https://juejin.cn/book/6844733800300150797/section/6844733800363065358 (opens new window)

# 冒泡排序

function betterBubbleSort(arr) {
  const len = arr.length;
  for (let i = 0; i < len; i++) {
    // 区别在这里,我们加了一个标志位
    let flag = false;
    for (let j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        // 只要发生了一次交换,就修改标志位
        flag = true;
      }
    }
    // 若一次交换也没发生,则说明数组有序,直接放过
    if (flag == false) return arr;
  }
  return arr;
}

# 选择排序

function selectSort(arr) {
  // 缓存数组长度
  const len = arr.length;
  // 定义 minIndex,缓存当前区间最小值的索引,注意是索引
  let minIndex;
  // i 是当前排序区间的起点
  for (let i = 0; i < len - 1; i++) {
    // 初始化 minIndex 为当前区间第一个元素
    minIndex = i;
    // i、j分别定义当前区间的上下界,i是左边界,j是右边界
    for (let j = i; j < len; j++) {
      // 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    // 如果 minIndex 对应元素不是目前的头部元素,则交换两者
    if (minIndex !== i) {
      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
  }
  return arr;
}

# 插入排序

function insertSort(arr) {
  // 缓存数组长度
  const len = arr.length;
  // temp 用来保存当前需要插入的元素
  let temp;
  // i用于标识每次被插入的元素的索引
  for (let i = 1; i < len; i++) {
    // j用于帮助 temp 寻找自己应该有的定位
    let j = i;
    temp = arr[i];
    // 判断 j 前面一个元素是否比 temp 大
    while (j > 0 && arr[j - 1] > temp) {
      // 如果是,则将 j 前面的一个元素后移一位,为 temp 让出位置
      arr[j] = arr[j - 1];
      j--;
    }
    // 循环让位,最后得到的 j 就是 temp 的正确索引
    arr[j] = temp;
  }
  return arr;
}

# 归并排序

function mergeSort(arr) {
  const len = arr.length;
  if (len < 1) {
    return arr;
  }
  const mid = Math.floor(len / 2);
  const leftArr = mergeSort(arr.slice(0, mid));
  const rightArr = mergeSort(arr.slice(mid, len));

  arr = mergeArr(leftArr, rightArr);
  return arr;
}

function mergeArr(arr1, arr2) {
  let i = 0;
  let j = 0;
  const res = [];
  const len1 = arr1.length;
  const len2 = arr2.length;
  while (i < len2 && j < len2) {
    if (arr[i] < arr[j]) {
      res.push(arr[i]);
      i++;
    } else {
      res.push(arr[j]);
      j++;
    }
  }

  if (i < len1) {
    return res.concat(arr1.slice(i));
  } else {
    return res.concat(arr2.slice(j));
  }
}

# 快速排序

快速排序并不会把真的数组分割开来再合并到一个新数组中去,而是直接在原有的数组内部进行排序

// 快速排序入口
function quickSort(arr, left = 0, right = arr.length - 1) {
  // 定义递归边界,若数组只有一个元素,则没有排序必要
  if (arr.length > 1) {
    // lineIndex表示下一次划分左右子数组的索引位
    const lineIndex = partition(arr, left, right);
    // 如果左边子数组的长度不小于1,则递归快排这个子数组
    if (left < lineIndex - 1) {
      // 左子数组以 lineIndex-1 为右边界
      quickSort(arr, left, lineIndex - 1);
    }
    // 如果右边子数组的长度不小于1,则递归快排这个子数组
    if (lineIndex < right) {
      // 右子数组以 lineIndex 为左边界
      quickSort(arr, lineIndex, right);
    }
  }
  return arr;
}
// 以基准值为轴心,划分左右子数组的过程
function partition(arr, left, right) {
  // 基准值默认取中间位置的元素
  let pivotValue = arr[Math.floor(left + (right - left) / 2)];
  // 初始化左右指针
  let i = left;
  let j = right;
  // 当左右指针不越界时,循环执行以下逻辑
  while (i <= j) {
    // 左指针所指元素若小于基准值,则右移左指针
    while (arr[i] < pivotValue) {
      i++;
    }
    // 右指针所指元素大于基准值,则左移右指针
    while (arr[j] > pivotValue) {
      j--;
    }

    // 若i<=j,则意味着基准值左边存在较大元素或右边存在较小元素,交换两个元素确保左右两侧有序
    if (i <= j) {
      swap(arr, i, j);
      i++;
      j--;
    }
  }
  // 返回左指针索引作为下一次划分左右子数组的依据
  return i;
}

// 快速排序中使用 swap 的地方比较多,我们提取成一个独立的函数
function swap(arr, i, j) {
  [arr[i], arr[j]] = [arr[j], arr[i]];
}

# 动态规划

# 最少硬币数目

https://leetcode-cn.com/problems/gaM7Ch/ (opens new window)

const coinChange = function (coins, amount) {
  // 用于保存每个目标总额对应的最小硬币个数
  const f = [];
  // 提前定义已知情况
  f[0] = 0;
  // 遍历 [1, amount] 这个区间的硬币总额
  for (let i = 1; i <= amount; i++) {
    // 求的是最小值,因此我们预设为无穷大,确保它一定会被更小的数更新
    f[i] = Infinity;
    // 循环遍历每个可用硬币的面额
    for (let j = 0; j < coins.length; j++) {
      // 若硬币面额小于目标总额,则问题成立
      if (i - coins[j] >= 0) {
        // 状态转移方程
        f[i] = Math.min(f[i], f[i - coins[j]] + 1);
      }
    }
  }
  // 若目标总额对应的解为无穷大,则意味着没有一个符合条件的硬币总数来更新它,本题无解,返回-1
  if (f[amount] === Infinity) {
    return -1;
  }
  // 若有解,直接返回解的内容
  return f[amount];
};

# 背包问题

有 n 件物品,物品体积用一个名为 w 的数组存起来,物品的价值用一个名为 value 的数组存起来;每件物品的体积用 w[i] 来表示,每件物品的价值用 value[i] 来表示。现在有一个容量为 c 的背包,问你如何选取物品放入背包,才能使得背包内的物品总价值最大?

注意:每种物品都只有 1 件

// 入参是物品的个数和背包的容量上限,以及物品的重量和价值数组
function knapsack(n, c, w, value) {
  // dp是动态规划的状态保存数组
  const dp = new Array(c + 1).fill(0);
  // res 用来记录所有组合方案中的最大值
  let res = -Infinity;
  for (let i = 1; i <= n; i++) {
    for (let v = c; v >= w[i]; v--) {
      // 写出状态转移方程
      dp[v] = Math.max(dp[v], dp[v - w[i]] + value[i]);
      // 即时更新最大值
      if (dp[v] > res) {
        res = dp[v];
      }
    }
  }
  return res;
}

# 最长上升子序列

https://leetcode-cn.com/problems/longest-increasing-subsequence/ (opens new window)

/**
 * @param {number[]} nums
 * @return {number}
 */
// 入参是一个数字序列
const lengthOfLIS = function (nums) {
  // 缓存序列的长度
  const len = nums.length;
  // 处理边界条件
  if (!len) {
    return 0;
  }
  // 初始化数组里面每一个索引位的状态值
  const dp = new Array(len).fill(1);
  // 初始化最大上升子序列的长度为1
  let maxLen = 1;
  // 从第2个元素开始,遍历整个数组
  for (let i = 1; i < len; i++) {
    // 每遍历一个新元素,都要“回头看”,看看能不能延长原有的上升子序列
    for (let j = 0; j < i; j++) {
      // 若遇到了一个比当前元素小的值,则意味着遇到了一个可以延长的上升子序列,故更新当前元素索引位对应的状态
      if (nums[j] < nums[i]) {
        dp[i] = Math.max(dp[i], dp[j] + 1);
      }
    }
    // 及时更新上升子序列长度的最大值
    if (dp[i] > maxLen) {
      maxLen = dp[i];
    }
  }
  // 遍历完毕,最后到手的就是最大上升子序列的长度
  return maxLen;
};

# 谷歌真题

# 最长回文子串问题

编辑 (opens new window)
上次更新: 2025/03/11, 11:29:39
海外迅雷
数组

← 海外迅雷 数组→

最近更新
01
Code Review
10-14
02
ComfyUI
10-11
03
vscode插件开发
08-24
更多文章>
Power by vuepress | Copyright © 2015-2025 神族九帝
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×