题库:https://codetop.cc/home
https://juejin.cn/post/6992775762491211783
https://github.com/nettee/little-algorithm
https://lucifer.ren/fe-interview/#/?id=%E7%BC%96%E7%A8%8B%E9%A2%98-%E2%9C%8D%ef%b8%8f
400. 第 N 位数字
首先题干意思是 把所有正整数排在一起,
1, 2, … 9,10,11, 12…一位一位地看成1 2 … 9 1 0 1 1 1 2
输入一个n,计算第n个位置上的数是几;
前9个数都是1位的直接返回n就行了,但后面有那么多两位数、三位数…得仔细考虑:
即使数值被拆成多个数位了,我们仍可以把原数值想象成 用[]
包裹的一个个单元
1,2,...,9,10,11,...99,100,101
=> [1][2]...[9][1,0][1,1]...[9,9][1,0,0][1,0,1]
对正整数划分若干区间:0~9, 10~99, 100~999, 1000~9999…
那么在相同的区间内的单元 所占的位数相同,区间如果向后移动 单元的占位会增加
第1个区间 单元占位均为1,第N个区间 单元占位均为N,
那么每个区间总共占了多少位 是可以计算的,输入一个n肯定可以计算出是它在第几个区间 以及区间内的偏移量。
// https://leetcode-cn.com/problems/nth-digit/
const findNthDigit = (n) => {
if (n < 10) return n;
// 按照数值所占位数 可对正整数划分若干区间:0~9, 10~99, 100~999, 1000~9999... 位数递增1
// 0实际上并不应该存在 为了形式一致才写成这样,对答案没影响 因为1~9其实已经特殊处理直接返回n了
let digitUnit = 1 // 区间内每个数值占几位,起个名叫“单元”吧Unit
let [bottom, top] = [0, 10] // 区间数值的上下限,左闭右开 [0,10) [10,100) [100,1000)...
while (n > (top - bottom) * digitUnit) { // 当n大于 当前区间所有单元 占的位数
n -= (top - bottom) * digitUnit // 我们希望不断右移区间,让n减掉当前区间占的位数
digitUnit += 1 // 每个数占的位数+1
bottom = top // 更新下限
top = top * 10 // 更新上限,左闭右开的好处就是可以直接*10
}
// 剩下的n除以当前区间的单元占位 得到的商num即表示 它是此区间的第几个单元,余数r是单元内字符的位置偏移量
let [num, r] = [Math.floor(n / digitUnit), n % digitUnit]
return +(num + bottom + '')[r] // +''转string访问下标再用+转number
};
264. 丑数
自己定义最小堆 每次弹堆顶 即可得到递增的结果
答案:
// https://leetcode-cn.com/problems/chou-shu-lcof/submissions/
var nthUglyNumber = function (n) {
//利用堆能够自动保持自身的堆顶元素是最大元素或者最小元素
//这里需要用到集合Set保证没有向堆中插入相同大小的元素,假如插入了就不能保证堆顶元素是第n个丑数了
//因为要循环遍历n次
let set = new Set();
set.add(1);
let heap = new minHeap();
heap.insert(1);
let top = 0;
let list = [2, 3, 5];
for (let j = 0; j < n; j++) {
top = heap.pop();
for (let i of list) {
if (!set.has(i * top)) {
set.add(i * top);
heap.insert(i * top)
}
}
}
return top;
};
最小堆定义:
var swap = (arr, i, j) => {
[arr[i], arr[j]] = [arr[j], arr[i]]
}
class minHeap {
constructor(arr = []) {
this.container = [];
arr.forEach(this.insert.bind(this))
}
insert(data) {
const { container } = this;
//插入需要插入对尾,向前比较
container.push(data);
let index = container.length - 1;
while (index) {
let parent = Math.floor((index - 1) / 2);
if (container[index] >= container[parent]) {
break;
}
swap(container, index, parent)
index = parent
}
}
pop() {
const { container } = this;
if (!container.length) return null;
//删除,首尾互换,取出原来堆顶元素,减少数组长度,从上到下依次比较
swap(container, 0, container.length - 1)
let res = container.pop();
let index = 0;
let exchange = index * 2 + 1;
while (exchange < container.length) {
if (exchange + 1 < container.length && container[exchange] > container[exchange + 1]) {
exchange++;
}
if (container[exchange] >= container[index]) {
break;
}
swap(container, index, exchange)
index = exchange;
exchange = index * 2 + 1;
}
return res;
}
top() {
const { container } = this;
if (container.length) return container[0];
return null
}
}
415. 字符串相加
维护临时工作变量carry
负责收集每一位的和,carry
的个位添入res
,十位参与下一个位置的加法;
下标索引i, j
各自从两个字符串尾部向前遍历取出字符,记得各自判断是否到头了
// https://leetcode-cn.com/problems/add-strings/
const addStrings = (numStr1, numStr2) => {
let [res, carry, i, j] = [[], 0, numStr1.length - 1, numStr2.length - 1];
while (i >= 0 || j >= 0 || carry != 0) {
(i >= 0) && (carry += Number(numStr1[i--]));
(j >= 0) && (carry += Number(numStr2[j--]));
res.unshift(carry % 10);
carry = Math.floor(carry / 10);
}
return res.join('');
};
88. 合并两个有序数组
题目说第一个数组后面空了一段空间以容纳第二个数组,因此从第一个数组的尾部t开始填空,决定此位置放的元素来自于哪一个数组;
数组
// https://leetcode-cn.com/problems/merge-sorted-array/
const merge = (nums1, m, nums2, n) => {
let [i, j, t] = [m - 1, n - 1, nums1.length - 1];
// 短的还剩
while (j >= 0) {
// 长的还剩
while (i >= 0 && nums1[i] > nums2[j]) {
[nums1[t], nums1[i]] = [nums1[i], nums1[t]];
t--; i--;
}
[nums1[t], nums2[j]] = [nums2[j], nums1[t]];
t--; j--;
}
};
3. 无重复字符的最长子串
滑动窗口 由左坐标+当前长度构成 试探增加当前长度并更新窗口内元素,本题要找最长长度,为满足无重复的条件 设置一个lookup集合检查新元素是否可填入窗口
// https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/
const lengthOfLongestSubstring = (s) => {
if (!s.length) return 0
let [leftIdx, currLen, maxLen] = [0, 0, 0];
let workSet = new Set();
for (let idx in s) {
currLen++;
while (workSet.has(s[idx])) {
workSet.delete(s[leftIdx]);
leftIdx++;
currLen--;
}
maxLen = Math.max(maxLen, currLen);
workSet.add(s[idx]);
}
return maxLen;
}
129. 求根节点到叶节点上的数字 之和
本题结点上的数字要随路径拼成一整个数字 因此自顶向下*10加和即可得到路径代表的数字大小,到达叶节点时返回求的和
// https://leetcode-cn.com/problems/sum-root-to-leaf-numbers/
const sumNumbers = (root) => {
const recur = (root, acc = 0) => {
if (!root) return 0;
const sum = acc * 10 + root.val;
if (!root.left && !root.right) { // 说明当前结点为叶节点 可以返回总和了
return sum;
};
return recur(root.left, sum) + recur(root.right, sum)
}
return recur(root);
};
112. 路径总和
本题要检查一颗树中 是否存在一条根到叶节点的路径 的结点和 等于目标值,自顶向下消耗目标值,到达叶节点时判断当前节点值是否等于剩余目标值 (子问题划分视角)
// https://leetcode-cn.com/problems/path-sum/
const hasPathSum = (root, targetSum) => {
const recur = (root, target) => {
if (!root) return false;
if (!root.left && !root.right) { // 说明当前结点为叶节点 可以判断总和了
return root.val == target;
};
return recur(root.left, target - root.val) || recur(root.right, target - root.val);
}
return recur(root, targetSum);
};
凡是题目描述里提到叶结点的,都需要显式判断叶结点,在叶结点处结束递归。
113. 路径总和 II
找出所有满足上一题的条件的路径,这次并不能单纯划分子问题,而是要用遍历视角看待;
路径遍历的顺序就是 DFS 序:当 DFS 进入一个结点时,路径中就增加一个结点;当 DFS 从一个结点退出时,路径中就减少一个结点。
根据回溯操作的特性,使用栈 记录遍历时的当前路径。当进入一个结点时,做 push 操作;当退出一个结点时,做 pop 操作,进行回溯。
// https://leetcode-cn.com/problems/path-sum-ii/
const pathSum = (root, targetSum) => {
const recur = (root, sum, path, res) => {
// terminator
if (!root) return;
// current level
path.push(root.val);
if (!root.left && !root.right) {
if (sum == root.val) {
res.push(path.slice()); // 存入res的是path的拷贝,否则后面pop会导致res中存的结果也变了
}
}
// drill down
const target = sum - root.val;
recur(root.left, target, path, res);
recur(root.right, target, path, res);
// backtrace
path.pop();
}
let [res, path] = [[], []];
recur(root, targetSum, path, res);
return res;
};