算法
算法复杂度ing
衡量算法性能的标准
时间复杂度、空间复杂度
-
为什么需要复杂度分析
代码运行时,可以通过统计和监控,得到算法执行的时间和占用内存大小。但是代码运行时对算法执行效率的评估时有局限性的:
- 测试结果非常依赖测试环境
- 测试结果受数据规模的影响很大
所以需要一个不用具体测试数据,就可以粗略估算算法执行效率的方法。
-
大O复杂度表示法
复杂度是一个关于输入数据量n的函数。
假设代码复杂度是f(n),那么就用大写字母O和括号,把f(n)括起来就可以了,即O(f(n))。
例如,O(n)表示的是,复杂度与计算实例个数线性相关;
O(logn)表示的是,复杂度与计算实例的个数n对象相关,这就是大O时间复杂度。大O时间复杂度实际上并不具体代表代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以也叫做渐进时间复杂度,简称时间复杂度。
复杂度计算方法的原则:
- 复杂度与具体常系数无关:例如O(n)和O(2n)表示的是相同的复杂度。
- 多项式级的复杂度像假的时候,选择高者作为结果:例如O(n)+O(n²)和O(n²)表示的是相同的复杂度。因为随着n越来越大,二阶多项式的变化率要比一阶多项式更大,因此只需要通过更大变化率的二阶多项式就可以来表征复杂度。
时间复杂度分析
技巧
-
只关注循环执行次数最多的字段代码
大O复杂度表示方法只是表示一种变化趋势,一般会忽略公式中的常量、低阶、系数,自己了一个最大阶的量级即可。所以,在分析一个算法或一段代码的时间复杂度时,只关注循环执行次数最多的字段代码就可以。
function cal(n) { let sum = 0; // 常量级执行时间,与n大小无关,可以认为对复杂度没有影响 for (let i = 0; i<= n; i++) { // O(n) sum += i; } return sum; }
-
加法法则:总复杂度等于量级最大的那段代码的复杂度
function cal(n) { let sum1 = 0; for (let q = 1; q < 100; q++) { // T1:常量级执行时间 sum2 += q; } let sum2 = 0; for (let p = 1; p < n; p++) { // T2:O(n) sum2 += p; } let sum3 = 0; for (let i = 1; i <= n; i++) { for (let j = 1; j <= n; j++) { // T3: O(n²) sum3 += i * j; } } return sum1 + sum2 + sum3; }
所以fn的时间复杂度为:T(n) = T2(n) + T3(n) ,取最大量级就是T3(n) :O(n²)
-
乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积,可以看成是嵌套循环。
function cal(n) {
let res = 0;
for (let i = 1; i <= n; i++) {
res += f(i);
}
function f(n) {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
}
常见的时间复杂度
- O(1):基本运算+、-*、/、%、寻址
- O(logn):二分查找,跟分治(Divide & Conquer)相关的基本上都是logn
- O(n):线性查找
- O(nlogn):归并排序、快速排序的期望复杂度,基于比较排序的算法下界
- O(n²):冒泡排序、插入排序,朴素最近点对
- O(n³):Floyd最短路径,普通矩阵乘法
- O(2^n):枚举全部子集
- O(n!):枚举全排列
- O(1)
常量级时间复杂度的一种表示方法,并不是指只执行一行代码。
只要代码的执行时间不随n的增大而增长,这样代码的时间复杂度都记作O(1)。
一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行代码,其时间复杂度也是O(1)。let i= 1; let j= 2; let sum = i + j;
- O(logn)、O(nlogn)
对数阶事件复杂度非常常见,也是最难分析的一种事件复杂度。
let i = 1;
while (i <= n) {
i = i * 2;
}
代码执行:
2x= n,所以x=log2n。所以这段代码的是O(log2n)。
let i = 1;
while (i <= n) {
i = i * 3;
}
3x= n, 所以x=log3n。所以这段代码的是O(log3n)。
在实际中,不管是2为底、3为底还是10为底,都可以把所有对数阶的事件复杂度记为O(logn)。因为对数之间是可以相互转换的,log3n = log32 * log2n -> O(log3n) = O(C * log2n),而C = log32 是个常量,可以在采用大O标记复杂度的时候,忽略常量系数,即O(Cf(n)) = O(f(n))。
上述总结:在对数阶时间复杂度表示方法里,忽略对数的底,统一表示为O(logn)。
如果一段代码的时间复杂度是O(logn),而这段代码循环执行n次,那么事件复杂度就是O(nlogn)。例如归并排序、快速排序的事件复杂度都是O(nlogn)。
- O(m+n)、O(m*n)
代码的复杂度由两个数据的规模来决定:
function cal(m, n) {
let sum1 = 0;
for(let i = 0; i < m; i++) {
sum1 += i;
}
let sum2 = 0;
for(let j = 0; j < n; j++) {
sum2 += j;
}
return sum1 + sum2;
// 若无法事先评估m和n谁的量级大,就不能简单的利用加法法则 O(m+n)
}
时间复杂度分析进阶
- 最好、最坏时间复杂度
// 下述代码的复杂度是O(n),n代表数组的长度,但是在数组中查找一个数据时,并不需要每次都把整个数组都遍历一遍,因为有可能中途就找到了。
function find(arr, n, x) {
let pos = -1;
for (let i = 0; i < n; i ++) {
if (arr[i] === x) {
pos = i;
}
}
return pos;
}
// 改进代码,下面代码显然不能直接用O(n)去表示复杂度了。
function find(arr, n, x) {
let pos = -1;
for (let i = 0; i < n; i ++) {
if (arr[i] === x) {
pos = i;
break;
}
}
return pos;
}
因为需要查找的变量x可能出现中数组的任意位置,如果第一个就是寻找的变量,那么时间复杂度就是O(1)。但如果数组中不存在变量x,那就需要把整个数组都遍历一遍,这样时间复杂度就是O(n),所以不同的情况下,代码的时间复杂度是不一样的。
这就需要引入两个概念:最好时间复杂度、最坏时间复杂度。
- 最好情况时间复杂度就是,在最理想的情况下,执行这段代码的时间复杂度。
- 最坏情况时间复杂度就是,在最糟糕的情况下,执行这段代码的时间复杂度。
-
平均时间复杂度
最好时间复杂度、最坏时间复杂度对应的都是极端情况下的代码复杂度,发生的概率比较小。为了更好地表示平均情况下的复杂度,需要引入另一个概念『平均时间复杂度』。
例如上面的例子,需要查找变量x在数组中的位置,有n+1中情况:0-n-1以及不在数组中。
把每种情况,需要查找遍历的元素个数累加起来,然后再除以n+1,就可以得到需要遍历的元素个数平均值:
-
均摊时间复杂度
空间复杂度分析
排序
冒泡排序
依次比较大小,小的与大的进行位置交换。
function bubbleSort(arr) {
for (let i = 0, L = arr.length; i < L - 1;i ++) {
for (let j = i + 1; j < L; j++) {
if (arr[i] > arr[j]) {
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
}
return arr;
}
const arr = [1, 3, 4, 8, 7, 6, 5, 2];
console.log(bubbleSort(arr));
插入排序
快速排序
算法参考某个元素值,将小于它的值放入左数组,大于它的值放入右数组,然后递归进行上一次左右数组的操作,返回合并的数组,就是已经拍好顺序的数组。
function quickSort(arr) {
if (arr.length <= 1) {
return arr;
}
const leftArr = [];
const rightArr = [];
const q = arr[0];
for (let itemIdx = 1, L = arr.length; itemIdx < L; itemIdx++) {
const item = arr[itemIdx];
if (item > q) {
rightArr.push(item);
} else {
leftArr.push(item)
}
}
return [].concat(quickSort(leftArr), [q], quickSort(rightArr));
}
const arr = [1, 3, 4, 8, 7, 6, 5, 2];
console.log(quickSort(arr));
哈希排序
二叉树
图
堆
栈
队列
链表
数组
矩阵
字符串
哈希表
其他手写题
判断一个单词是否是回文
回文是指,相同的词汇或句子,在下文中调换位置或颠倒过来,产生首尾回环的。
function checkPalindrom(str) {
return str === str.split('').reverse().join('');
}
去掉一组整型数组重复的值
例如输入[1, 13, 24, 11, 11, 11, 14, 1, 2]
输出[1, 13, 24, 11, 14, 2]
const arr = [1, 13, 24, 11, 11, 11, 14, 1, 2];
const unique = function(arr) {
let hashTable = {};
let data = [];
for (let item of arr) {
if (!hashTable[item]) {
hashTable[item] = true;
data.push(item);
}
}
return data;
}
console.log(unique(arr));
统计一个字符串出现最多的字母
输入: afjghdfraaaasdenas
输出:a
function findMaxDuplicateChar(str) {
if (str.length === 1) {
return str;
}
const chartObj = {};
for(let i = 0; i < str.length; i++) {
const char = str[i];
if (!chartObj[char]) {
chartObj[char] = 1;
} else {
chartObj[char] += 1;
}
}
let maxChar = '';
let maxValue = 0;
Object.keys(chartObj).forEach(key => {
const value = chartObj[key];
if (value > maxValue) {
maxChar = key;
maxValue = value;
}
});
console.log(maxChar, chartObj);
}
findMaxDuplicateChar('afjghdfraaaasdenas');
不借助临时变量,进行两个整数的交换
a = a + (b - a) -> a = b;
b = b - a;
a = a + b; (a + (b - a) ) -> b
b = a - b; (a + (b - a) - (b - a)) -> a
function swap(a, b) {
b = b - a;
a = a + b;
b = a - b;
return [a, b];
}
斐波那契数列曲线
fibo[i] = fibo[i-1] + fibo[i-2]
function getFibonacci(n) {
const fiboarr = [];
let i = 0;
while (i <= n) {
if (i <= 1) {
fiboarr.push(i);
} else {
fiboarr.push(fiboarr[i-1] + fiboarr[i-2]);
}
i ++;
}
return fiboarr;
}
getFibonacci(9);
找出正数组的最大差值
输入: [10, 5, 11, 7, 8, 9]
输出:6, 5, 11
简化:找出最大最小值
function getMaxProfit(arr) {
let minPrice = arr[0];
let maxPrice = arr[0];
for (let i = 1; i < arr.length; i++) {
minPrice = Math.min(minPrice, arr[i]);
maxPrice = Math.max(maxPrice, arr[i]);
}
console.log(maxPrice - minPrice + ': ', minPrice, maxPrice);
}
随机生成指定长度的字符串
function randomString(n) {
let str = 'abcdefghijklmnopqrstuvwxyz9876543210?!+-';
let tmp = '';
let i = 0;
let L = str.length;
for (i = 0; i < n; i++) {
tmp += str.charAt([Math.floor(Math.random() * L)]);
}
return tmp;
}
用js实现getElementByClassName
function queryClassName(node, name) {
const starts = '(^|[\n\t\r\f])';
const ends = '([\n\t\r\f]|$)';
const array = [];
const regex = new RegExp(starts + name + ends);
const elements = node.getElementsByTagName('*');
const L = elements.length;
let i = 0;
let element;
while(i < L) {
element = elements[i];
if (regex.test(element.className)) {
array.push(element);
}
}
return array;
}
数组扁平化
- 递归
function flat(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
if (Array.isArray(item)) {
result = result.concat(flat(item));
} else {
result.push(item);
}
}
return result;
}
function flat(arr) {
return arr
.map(r => Array.isArry(r) ? flat(r) : r)
.reduce((a, b) => a.concat(b), []);
}
- 深度递归
function flatDepth(arr, depth = 0) {
return arr
.map(depth <= 0 ? r => r : r => Array.isArray(r) ? flatDepth(r, depth - 1) : r)
.reduce((a, b) => a.concat(b), []);
}