【前端面试】算法

算法复杂度ing

衡量算法性能的标准

时间复杂度、空间复杂度

  1. 为什么需要复杂度分析

    代码运行时,可以通过统计和监控,得到算法执行的时间和占用内存大小。但是代码运行时对算法执行效率的评估时有局限性的:

    • 测试结果非常依赖测试环境
    • 测试结果受数据规模的影响很大

    所以需要一个不用具体测试数据,就可以粗略估算算法执行效率的方法。

  2. 大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越来越大,二阶多项式的变化率要比一阶多项式更大,因此只需要通过更大变化率的二阶多项式就可以来表征复杂度。

时间复杂度分析

技巧

  1. 只关注循环执行次数最多的字段代码

    大O复杂度表示方法只是表示一种变化趋势,一般会忽略公式中的常量、低阶、系数,自己了一个最大阶的量级即可。所以,在分析一个算法或一段代码的时间复杂度时,只关注循环执行次数最多的字段代码就可以。

    function cal(n) {
    	let sum = 0; // 常量级执行时间,与n大小无关,可以认为对复杂度没有影响
    	for (let i = 0; i<= n; i++) { // O(n)
    		sum += i;
    	}
    	return sum;
    }
    
  2. 加法法则:总复杂度等于量级最大的那段代码的复杂度

    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²)

  3. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积,可以看成是嵌套循环。

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!):枚举全排列
  1. O(1)
    常量级时间复杂度的一种表示方法,并不是指只执行一行代码。
    只要代码的执行时间不随n的增大而增长,这样代码的时间复杂度都记作O(1)。
    一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行代码,其时间复杂度也是O(1)。
    let i= 1let j= 2let sum = i + j;
    
  2. 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)。

  1. 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)
}

时间复杂度分析进阶

  1. 最好、最坏时间复杂度
// 下述代码的复杂度是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),所以不同的情况下,代码的时间复杂度是不一样的。

这就需要引入两个概念:最好时间复杂度、最坏时间复杂度。

  • 最好情况时间复杂度就是,在最理想的情况下,执行这段代码的时间复杂度。
  • 最坏情况时间复杂度就是,在最糟糕的情况下,执行这段代码的时间复杂度。
  1. 平均时间复杂度

    最好时间复杂度、最坏时间复杂度对应的都是极端情况下的代码复杂度,发生的概率比较小。为了更好地表示平均情况下的复杂度,需要引入另一个概念『平均时间复杂度』。

    例如上面的例子,需要查找变量x在数组中的位置,有n+1中情况:0-n-1以及不在数组中

    把每种情况,需要查找遍历的元素个数累加起来,然后再除以n+1,就可以得到需要遍历的元素个数平均值:

    在这里插入图片描述

  2. 均摊时间复杂度

空间复杂度分析

排序

冒泡排序

依次比较大小,小的与大的进行位置交换。

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;
}

数组扁平化

  1. 递归
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), []);
}
  1. 深度递归
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), []);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值