# 算法
# LRU-Cache 算法
class LRUCache {
/**
* @param {number} capacity - 缓存容量
*/
constructor(capacity = 2) {
/**
* @type {number}
* 缓存容量
*/
this.capacity = capacity;
/**
* @type {Map}
* 用来存储缓存项的 Map
*/
this.cache = new Map();
}
/**
* @param {number} key - 缓存键
* @return {number}
* 获取缓存值,如果不存在返回 -1
*/
get(key) {
// 如果缓存中不存在该键,则返回 -1
if (!this.cache.has(key)) {
return -1;
}
const value = this.cache.get(key)
// 从缓存中删除该键值对,并重新插入以保证它是最近使用的
this.cache.delete(key);
this.cache.set(key, value);
// 返回缓存值
return value;
}
/**
* @param {number} key - 缓存键
* @param {number} value - 缓存值
*/
put(key, value) {
// 如果缓存中已经存在该键,则将其删除
if (this.cache.has(key)) {
this.cache.delete(key);
}
// 将该键值对插入缓存中
this.cache.set(key, value);
// 如果缓存已满,则删除最近最少使用的缓存项
if (this.cache.size > this.capacity) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
}
}
class LRUCache {
/**
* @param {number} capacity - 缓存容量
*/
constructor(capacity) {
/**
* @type {number}
* 缓存容量
*/
this.capacity = capacity;
/**
* @type {Map}
* 用来存储缓存项的 Map
*/
this.cache = new Map();
}
/**
* @param {number} key - 缓存键
* @return {number}
* 获取缓存值,如果不存在返回 -1
*/
get(key) {
// 如果缓存中不存在该键,则返回 -1
if (!this.cache.has(key)) {
return -1;
}
// 获取该键对应的缓存项
const { value, expireTime } = this.cache.get(key);
// 检查该缓存项是否已经过期
if (expireTime < Date.now()) {
// 如果已经过期,则删除该缓存项并返回 -1
this.cache.delete(key);
return -1;
}
// 从缓存中删除该键值对,并重新插入以保证它是最近使用的
this.cache.delete(key);
this.cache.set(key, { value, expireTime });
// 返回缓存值
return value;
}
/**
* @param {number} key - 缓存键
* @param {number} value - 缓存值
* @param {number} expireTime - 缓存过期时间(单位为毫秒)
*/
put(key, value, expireTime) {
// 如果缓存中已经存在该键,则将其删除
if (this.cache.has(key)) {
this.cache.delete(key);
}
// 将该键值对插入缓存中
this.cache.set(key, { value, expireTime });
// 如果缓存已满,则删除最近最少使用的缓存项
if (this.cache.size > this.capacity) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
}
}
// 创建一个容量为 3 的 LRU Cache
const cache = new LRUCache(3);
// 将几个缓存项插入缓存中
cache.put(1, 'foo', Date.now() + 10000); // 缓存 1, 过期时间为 10 秒后
cache.put(2, 'bar', Date.now() + 20000); // 缓存 2, 过期时间为 20 秒后
cache.put(3, 'baz', Date.now() + 30000); // 缓存 3, 过期时间为 30 秒后
// 获取缓存项的值
const value1 = cache.get(1); // 'foo'
console.log(value1);
const value2 = cache.get(2); // 'bar'
console.log(value2);
// 增加一个新的缓存项
cache.put(4, 'qux', Date.now() + 40000); // 缓存 4, 过期时间为 40 秒后
// 获取不存在的缓存项的值
const value3 = cache.get(5); // -1
console.log(value3);
# diff 算法
# QuickSort
let arr = [3, 5, 4, 3, 6, 8, 99, 6, 8, 51, 3, 33, 1]
function quickSort(arr) {
let len = arr.length
let left = [], right = []
if (len <= 1) return arr
let index = Math.floor(len / 2)
let currentItem = arr.splice(index, 1)[0]
arr.forEach(el => currentItem < el ? left.push(el) : right.push(el));
return [...quickSort(left), currentItem, ...quickSort(right)]
}
console.log(quickSort(arr));
const arr = [6, 3, 5, 6, 1, 3, 9, 7, 8];
const quickSort = array => {
if (array.length <= 1) return array;
let [pivot, ...rest] = array;
let small = rest.filter(i => i <= pivot);
let big = rest.filter(i => i > pivot);
console.log(small, pivot, big);
return [...quickSort(small), pivot, ...quickSort(big)];
};
console.log(quickSort(arr));
# 斐波那契数列
// 一般递归解法
const fib = n => n < 2 ? n : fib(n - 1) + fib(n - 2)
// 动态规划 利用变量把累加的值存储起来
// 这种算法的时间复杂度仅为O(n),从底部向上算
function fib(n) {
let current = 0;
let next = 1;
while (n-- > 0) { // while(n>0) {n--} n---–的返回值是n
[current, next] = [next, current + next];
} return current;
}
// 使用尾调用
// 尾递归就是函数的最后一步是调用另一个函数
'use strict'
function fib(n, current = 0, next = 1) {
if (n == 0) return 0;
if (n == 1) return next; // return next
console.log(`fibonacci(${n}, ${next}, ${current + next})`);
return fib(n - 1, next, current + next);
}
# 阶乘
// 2,6,24, 120
const f = n => n === 1 ? 1 : n * f(n - 1)
// 动态规划版
function f(n) {
if (n === 1) return 1
let r = 1
while (n > 0) {
r *= n--
console.log(r);
}
return r
}
console.log(f(5));
// 尾递归版本
const f = (n, r = 1) => n <= 1 ? r : f(n - 1, r *= n)
# 尾调用 与 js 调用栈
递归就是先递进再回归,呈一种 > 形状
尾递归就是把上一步计算的结果作为参数传递给下一次调用
了解 js 的调用栈我们知道,当脚本要调用一个函数时,解析器把该函数添加到栈中并且执行这个函数,并形成一个栈帧(调用帧),保存调用位置和内部变量等信息。
如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会销毁。此时如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。例如递归操作,一个调用栈中如果保存了大量的栈帧,调用栈非常长,消耗了巨大的内存,会导致爆栈(栈溢出,stack overflow)。后入先出的结构。
如果所有函数的调用都是尾调用,即只保留内层函数的调用帧,做到每次执行时(例如递归操作),一个调用栈中调用帧只有一项,那么调用栈的长度就会小很多,这样需要占用的内存也会大大减少。这就是尾调用优化的含义。
# 大数相加 (差值法)
/**
*
* 输入:nums = [2,7,11,15], target = 9
* 输出:[0,1]
* 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]
*/
var twoSum = function (nums, target) {
const map = new Map()
for (i = 0; i < nums.length; i++) {
const n = nums[i]
const diff = target - n
if (map.get(diff) !== undefined) {
return [map.get(diff), i]
} else {
map.set(n, i)
}
}
};
# 回文数 (头尾双指针)
var isPalindrome = function(x) {
const nums = String(x).split('')
const len = nums.length
const halfLen = Math.floor(nums.length / 2)
for (let i = 0; i < nums.length; i++) {
if (nums[i] !== nums[len - 1 - i]) return false
if (i === halfLen) return true
}
};
# 有效括号
/**
* s = '({})]['
*/
var isValid = function (s) {
const map = {
'}': '{',
']': '[',
')': '('
}
const stack = []
const arr = s.split('')
for (let i = 0; i < arr.length; i++) {
if (arr.length % 2 !== 0) return false
const s = arr[i]
const lastStr = stack[stack.length - 1]
if (!lastStr) {
stack.push(s)
continue
}
if (lastStr === map[s]) stack.pop()
else stack.push(s)
}
return !stack.length
};
# 反转链表
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
/*function ListNode(x){
this.val = x;
this.next = null;
}*/
//时间复杂度O(n),空间复杂度O(1)
function ReverseList(pHead) {
//保存当前结点
let cur = pHead;
if (cur === null || cur.next === null) return cur;
let prev = null; //前一个结点
let next = null; //下一个结点
while (cur) {
next = cur.next; //保存下一个结点
cur.next = prev; //当前结点的下一个结点指向前面的节点
prev = cur; //把所有节点往后挪
cur = next;
}
return prev;
}
/**------------------------------**/
//时间复杂度O(n),空间复杂度O(n)
function ReverseList(pHead) {
//把链表存到数组中
let node = pHead;
let ary = [];
while (node) {
ary.push(node.val);
node = node.next; //指向下一个结点
}
node = pHead;
while (node) {
node.val = ary.pop();
node = node.next;
}
return pHead;
}
# 无重复字符的最长子串(滑动窗口)
/**
* 滑动窗口 快慢指针
* 时间复杂度 O(n)
* 每次右指针走一步, 用一个 map 存储每个字符的index,
* 当字符重复时, 更新map 里字符的 index, 确定最长的非重复字符, 左边 * 指针走一步
*/
const lengthOfLongestSubstring = function (s) {
const map = new Map()
let left = 0
let right = 0
let len = 0
while (right < s.length) {
const char = s.charAt(right)
const charIndex = map.get(char)
if (charIndex === undefined) {
// 右指针指向的字符不存在时,以字符为键,保存其下标,并继续迭代
map.set(char, right)
} else {
// 存在重复值, 更新重复字符的下标, 确定一个最长距离
map.set(char, right)
/**
* 因为每次左指针变动都已经确定了当前左指针至右指针的最长区间
* 重复字符的索引值如果小于左指针的情况, 必定不可能产生更大的最长区间
* 故跳过这种情况, 没有意义
*/
if (charIndex >= left) {
len = Math.max(len, right - left)
/**
* 左指针, 指向重复字符位置的下一个字符, 确定一个当前可能的最长区间
* 去等待下一个新的最长非重复字符
*/
left = charIndex + 1
}
}
// 当左指针到终点的距离小于现存最大长度, 就没有后续比较的必要了
if ((s.length - left) < len) break
// 每次右指针向后走一格
right++
}
// 最后对比最大长度 和 最后左右指针的差额, 因为有这一步, 所以 charIndex < left 的可能性不用考虑, 因为最后会比较
return Math.max(len, right - left)
}
# 比较版本号
const compareVersion = (version1, version2) => {
const a = version1.split('.');
const b = version2.split('.');
const maxLength = Math.max(a.length, b.length);
for (let i = 0; i < maxLength; i++) {
const cur = a[i] || 0;
const next = b[i] || 0;
if (a[i] === b[i]) continue;
if (parseInt(cur) > parseInt(next)) {
return 1;
} else if(parseInt(cur) < parseInt(next)) {
return -1;
}
}
return 0;
}
# 千位分隔数
/**
* @param {number} n
* @return {string}
*/
var thousandSeparator = function (n) {
n = String(n)
let len = n.length
const arr = [];
if (len <= 3) return n;
for (var i = len - 1; i >= 0; i -= 3) {
arr.push(n[i]);
arr.push(n[i - 1]);
arr.push(n[i - 2]);
arr.push('.');
}
arr.pop();
return arr.reverse().join('');
};
← TypeScript 设计模式 →