目录
1.常考算法
排序算法
- 冒泡排序
// 冒泡排序 性能较差较少用 const bubblingSort = (arr) => { for (let i = 0; i < arr.length - 1; i++) { for (let j = 0; j < arr.length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; } } } return arr; }; const arr = bubblingSort([29, 10, 14, 37, 14]);
- 选择排序
在未排序序列中找到最小/最大元素,存放到排序序列的起始位置,再从剩余未排序元素中继续寻找最小/最大元素,然后放到已排序序列的末尾。
// 选择排序 性能较差较少用
const selectSort = (arr) => {
for (let i = 0; i < arr.length; i++) {
let indexMin = i;
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[indexMin]) {
indexMin = j;
}
}
[arr[i], arr[indexMin]] = [arr[indexMin], arr[i]];
}
return arr;
};
const arr1 = selectSort([5, 4, 3, 2, 1]);
- 插入排序
1.从第一个元素开始,该元素被认为已经被排序
2.取出下一个元素,在已经排序好的序列中,从后往前扫描
3.直到找到小于或等于该元素的位置(从大到小排)
4.将该位置后面的所有已经排序好的元素往后移动一位
5.将该元素插入到该位置
6.重复2-5
const insertSort = (arr) => {
for (let i = 1; i < arr.length; i++) {
const current = arr[i]; // 待插入元素
let j = i - 1; // 有序序列最后一个元素的索引
// 有序序列中从后向前扫描,找到相应位置并插入
while (j >= 0 && arr[j] > current) {
// 已排序元素大于新元素,将该元素往后移动一位
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = current; // 插入元素
}
return arr;
};
const val = insertSort([3, 5, 3, 3, 1]);
- 归并排序
function merge(left, right) {
let result = [],
leftIndex = 0,
rightIndex = 0;
// 当左右两个数组都还有元素时,进行比较并合并
while (leftIndex < left.length && rightIndex < right.length) {
if (left[leftIndex] <= right[rightIndex]) {
result.push(left[leftIndex++]);
} else {
result.push(right[rightIndex++]);
}
}
// 如果左数组还有剩余元素,直接添加到结果数组
while (leftIndex < left.length) {
result.push(left[leftIndex++]);
}
// 如果右数组还有剩余元素,直接添加到结果数组
while (rightIndex < right.length) {
result.push(right[rightIndex++]);
}
return result;
}
const mergeSort1 = (arr) => {
if (arr.length < 2) return arr;
const middle = Math.floor(arr.length / 2);
const left = mergeSort1(arr.slice(0, middle));
const right = mergeSort1(arr.slice(middle));
return merge(left, right);
};
- 快速排序
const quickSort = (arr) => {
if (arr.length <= 1) return arr;
let mid = Math.floor(arr.length / 2);
let pivot = arr.splice(mid, 1)[0];
let left = [];
let right = [];
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
item < pivot ? left.push(item) : right.push(item);
}
return quickSort(left).concat([pivot], quickSort(right));
};
const val2 = quickSort([8, 5, 7, 3, 1, 6, 4, 2]);
查找算法:二分查找
// 二分搜索 前提: 数组有序
const search = (arr, target) => {
let count = 1; // 寻找次数
let start = 0;
let end = arr.length - 1;
while (start <= end) {
let middle = Math.floor((start + end) / 2);
let guess = arr[middle];
if (guess === target) return middle; // 返回位置
if (guess > target) end = middle - 1; // 往左找
if (guess < target) start = middle + 1; // 往右找
count++;
}
return -1;
};
const val3 = search([1, 2, 3, 4, 5, 6, 7], 6);
console.log(val3); // 5
动态规划:解决最优化问题,如斐波那契数列、最长公共子序列等。
1.与分治法相似点:将待求解问题分解成若干子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
2.与分治法不同点:适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。
3.基本思路:用一个表(可能预置一些值)来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中,避免重复计算。
分治:子问题都是独立的,分解得到的子问题数目太多,有些子问题被重复计算了很多次。
// 斐波那契
// const fn = n => {
// if(n <= 2) return 1
// return fn(n-1) + fn(n-2)
// }
const fn = n => {
const res = [0,1,1]
for (let i = 3; i <= n; i++) {
res[i] = res[i-1] + res[i-2]
}
return res[n]
}
console.log(fn(11)); // 89
/**
* @description 力扣 #70 爬楼梯
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
const dp = [1,1]
for (let i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
};
/**
* @description 力扣 #198 打家劫舍
* @param {number[]} nums
* @return {number}
*/
var rob = function(nums) {
const len = nums.length
if(len === 0) return 0
const dp = [0, nums[0]]
for (let i = 2; i <= len; i++) {
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1])
}
return dp[len]
};
console.log(rob([1,2,3]));
图论算法:最短路径(Dijkstra、Floyd-Warshall)、拓扑排序等。
字符串处理:KMP算法、正则表达式匹配等。
2.遍历方法
数组 | break | 迭代器 | |
for | √ | √ | × |
for...of | √ | √ | √ |
for... in | √ | √ | × |
forEach | √ | × | × |
map | √ | × | × |
while | √ | √ | √ |
3.链式调用
- 数组的很多操作可以构成链式操作,类似这样的格式:…map().filter(…).sort(…).map(….)
- 链式操作就是对象方法返回类型是自身的。比如map是属于数组的方法,它返回数组,所以构成了链式操作
- 优势:语义清晰、思考方便,数据量小的时候很有用(<1W)
- 问题:性能、空间
4.递归
- 条件:递归通常需要初始条件和递归表达式
- 阶乘:n! = n x (n-1) !
// 阶乘
function factorial(n) {
if (n === 1) return 1
return n * factorial(n - 1)
}
- 斐波那契:f(1) = 1, f(2) = 1,f(n) = f(n-1) + f(n-2), n>2
// 斐波那契数列 1 1 2 3 5 8 13 21 34 55 89 144
function fibonacci(n) {
if (n === 1 || n === 2) return 1
return fibonacci(n - 1) + fibonacci(n - 2)
}
const fn = (n) => {
const res = [0, 1, 1];
for (let i = 3; i <= n; i++) {
res[i] = res[i - 1] + res[i - 2];
}
return res[n];
};
// 从底端构造递归
function fibonacci1(n) {
let [prev, curr] = [1,1]
for(let i = 3; i <= n; i++) {
[prev, curr] = [curr, prev + curr]
}
return curr
}
// console.log('测试 fibonacci1=============');
// console.log(fibonacci1(10));
function fibonacci2(n) {
return Array(n - 2).fill(0).reduce(([prev, curr]) => {
return [curr, prev + curr]
}, [1,1])[1]
}
// console.log('测试 fibonacci2=============');
// console.log(fibonacci2(10));
- DOM结点的绝对位置
offsetLeft、offsetRight相对于offsetParent的位置
Element.getBoundingClientRect()相对于视窗的位置,受滚动的影响
// DOM节点的绝对位置
function getLayout1(el) {
if (!el) return;
const layout = {
width: el.offsetWidth,
height: el.offsetHeight,
top: el.offsetTop,
left: el.offsetLeft
}
if(el.offsetParent) {
const parentLayout = getLayout1(el.offsetParent)
layout.top += parentLayout.top
layout.left += parentLayout.left
}
return layout
}
function getLayout2(el) {
if (!el) return;
let left = el.offsetLeft
let top = el.offsetTop
let p = el.offsetParent
while(p) {
left += p.offsetLeft
top += p.offsetTop
p = p.offsetParent
}
return {
width: el.offsetWidth,
height: el.offsetHeight,
top,
left
}
}
5.拷贝和比较
push/pop/shift/unshift/splice:都在原始数据上进行修改
concat/slice/map/reduce:都会对原始数据进行浅拷贝
// 递归实现深拷贝
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) return obj
// const newObj = Array.isArray(obj) ? [] : {}
// const newObj = obj instanceof Array ? [] : {}
// const newObj = obj.constructor === Array ? [] : {}
// const newObj = Object.prototype.toString.call(obj) === '[object Array]' ? [] : {}
const newObj = new obj.constructor()
for(let key in obj) {
if(obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key])
}
}
return newObj
}
// 测试用例
function testDeepClone() {
console.log("测试普通对象:");
const obj1 = { a: 1, b: { c: 2 } };
const clonedObj1 = deepClone(obj1);
console.assert(obj1 !== clonedObj1, "对象应该是不同的引用");
console.assert(obj1.b !== clonedObj1.b, "嵌套对象也应该是不同的引用");
console.assert(obj1.b.c === clonedObj1.b.c, "嵌套对象的属性值应该相等");
console.log("测试数组:");
const arr1 = [1, 2, [3, 4]];
const clonedArr1 = deepClone(arr1);
console.assert(arr1 !== clonedArr1, "数组应该是不同的引用");
console.assert(arr1[2] !== clonedArr1[2], "嵌套数组也应该是不同的引用");
console.assert(arr1[2][0] === clonedArr1[2][0], "嵌套数组的元素值应该相等");
console.log("测试特殊对象(Date):");
const date1 = new Date();
const clonedDate1 = deepClone(date1);
console.assert(date1 !== clonedDate1, "Date 对象应该是不同的引用");
console.assert(date1.getTime() === clonedDate1.getTime(), "Date 的时间戳应该相等");
// console.log("测试特殊对象(RegExp):"); // 失败
// const reg1 = /hello/g;
// const clonedReg1 = deepClone(reg1);
// console.assert(reg1 !== clonedReg1, "RegExp 对象应该是不同的引用");
// console.assert(reg1.source === clonedReg1.source && reg1.global === clonedReg1.global, "RegExp 的属性和标志应该相等");
// console.log("测试循环引用:"); // 失败
// const obj2 = {};
// obj2.self = obj2;
// const clonedObj2 = deepClone(obj2);
// console.assert(obj2 !== clonedObj2, "对象应该是不同的引用");
// console.assert(clonedObj2.self === clonedObj2, "循环引用应该被正确处理");
console.log("所有测试通过!");
}
// testDeepClone();
// 深度比较
function deepCompare(a,b){
if (a === null || typeof a !== 'object' || b === null || typeof b !== 'object') {
return a === b
}
// Object.getOwnPropertyDescriptors 方法会返回对象自身的所有属性描述符,包括不可枚举的属性
const propsA = Object.getOwnPropertyDescriptors(a)
const propsB = Object.getOwnPropertyDescriptors(b)
if(Object.keys(propsA).length !== Object.keys(propsB).length) return false
return Object.keys(propsA).every(key => deepCompare(a[key],b[key]))
}
// 测试用例
function testDeepCompare() {
console.log("测试基本相等性:");
console.assert(deepCompare(1, 1), "1 应该等于 1");
console.assert(!deepCompare(1, 2), "1 不应该等于 2");
console.assert(deepCompare(null, null), "null 应该等于 null");
console.assert(deepCompare(undefined, undefined), "undefined 应该等于 undefined");
console.assert(!deepCompare(null, undefined), "null 不应该等于 undefined");
console.log("测试对象比较:");
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
const obj3 = { a: 1, b: { c: 3 } };
console.assert(deepCompare(obj1, obj2), "obj1 应该等于 obj2");
console.assert(!deepCompare(obj1, obj3), "obj1 不应该等于 obj3");
console.log("测试数组比较:");
const arr1 = [1, 2, [3, 4]];
const arr2 = [1, 2, [3, 4]];
const arr3 = [1, 2, [3, 5]];
console.assert(deepCompare(arr1, arr2), "arr1 应该等于 arr2");
console.assert(!deepCompare(arr1, arr3), "arr1 不应该等于 arr3");
// console.log("测试循环引用(此实现可能无法正确处理):");
// const obj4 = {};
// obj4.self = obj4;
// const obj5 = {};
// obj5.self = obj5;
// 注意:此实现可能无法正确处理循环引用,因为它会陷入无限递归
// 这里我们假设它不会处理循环引用,并跳过这个测试
// console.assert(deepCompare(obj4, obj5), "循环引用对象应该相等(但这里不测试)");
console.log("所有测试通过!");
}
// testDeepCompare();