【图解算法】前端思维学习图解算法数据结构笔记(一)之算法复杂度

发表更新算法 / 图解算法 / 算法复杂度18 分钟读完 (大约2732个字)0次访问

【图解算法】前端思维学习图解算法数据结构笔记(一)之算法复杂度

书名是《图解算法数据结构》,在力扣看到的,准备开始系统学习一下算法了。会用前端的语言和思维来学习学习~希望能获得一些见解。第一篇是关于算法复杂度的学习笔记记录。

算法复杂度

复杂度概念

书中介绍,算法复杂度旨在计算在输入数据量 N 的情况下,算法的时间使用空间使用情况;体现算法运行使用的时间和空间随「数据大小N」而增大的速度

  • 时间:假设各操作的运行时间为固定常数,统计算法运行的计算操作的数量,以代表算法运行所需时间
  • 空间:统计在最差情况下,算法运行所需使用的”最大空间”
  • 数据大小N:算法处理的输入数据量;根据不同算法,具有不同定义
    • 排序算法:N 代表需要排序的元素数量
    • 搜索算法:N 代表搜索范围的元素总数

理解:算法复杂度是在一定的输入数据量下,计算所需要的时间操作数量大小(计算操作数量)和空间的大小来体现复杂度量。

时间复杂度

概念:时间复杂度指输入数据大小为 N 时,算法运行所需花费的时间

注意的是,这里的时间是计算操作数量,和运行绝对时间呈正相关关系,并不相等。

时间复杂度具有最差平均最佳三种情况,分别使用O, Θ, Ω三种符号表示

1
2
3
4
5
6
7
8
9
10
11
12
13

const find_num = (nums) => {
for (const num in nums) {
if (num === 7) return false
}
return false
}






常见的算法时间复杂度大小排序:O(1) < O(logN) < O(N) < O(NlogN) < O(N^2) < O(2^N) < O(N!)

  • 常数O(1):不随输入数据大小 N 的变化而变化
  • 线性O(N):循环运行次数与 N 大小呈线性关系
  • 平方O(N^2):两层循环相互独立,都与 N 呈线性关系,因此总体与 N 呈平方关系
  • 指数O(2^N):指数阶常出现于递归
  • 阶乘O(N!):阶乘阶对应数学上常见的 “全排列” 。即给定 NN 个互不重复的元素,求其所有可能的排列方案。阶乘常使用递归实现,算法原理:第一层分裂出 N 个,第二层分裂出 N−1 个,…… ,直至到第 N 层时终止并回溯
  • 对数O(logN):对数阶与指数阶相反,指数阶为 “每轮分裂出两倍的情况” ,而对数阶是 “每轮排除一半的情况”
  • 线性对数O(NlogN):两层循环相互独立,第一层和第二层时间复杂度分别为 O(logN) 和 O(N)

个人理解

  • 常数O(1):就是传入变量不影响计算,相当于一个未使用的变量。eslint下应该是遇不到的
  • 线性O(N):与输入N线性关系,其中只有一次循环是和输入的N有关
  • 平方O(N^2):两层互相独立的线性关系嵌套,两层嵌套循环,比如冒泡排序
  • 指数O(2^N): 树形图,每个节点都有两个操作,节节往下,第一层祖父节点,2^0 ,第二层就是2^1 ,以此类推,最后就是 2^n-1 次方,加起来就是 2^n
  • 阶乘O(N!):全排列,和上面反过来,第一次=层全部循环一遍,然后第二层操作剔除一个特征操作,以此类推
  • 对数O(logN):和指数反过来,就是二分法,分治这样的算法基础,一分为二或者一分为多。
  • 线性对数O(NlogN):两层循环相互独立,第一层和第二层时间复杂度分别为O(logN)和O(N)

空间复杂度

概念:空间复杂度指在输入数据大小为 N 时,算法运行所使用的暂存空间+输出空间的总体大小

  • 输入空间: 存储输入数据所需的空间大小;输入数据
  • 暂存空间: 算法运行过程中,存储所有中间变量和对象等数据所需的空间大小;包括数据空间``栈帧空间``指令空间
    • 数据空间:各项变量使用的空间,包括:声明的常量、变量、动态数组、动态对象等使用的内存空间。
    • 栈帧空间:程序调用函数是基于栈实现的,函数在调用期间,占用常量大小的栈帧空间,直至返回后释放。
    • 指令空间:编译后,程序指令所使用的内存空间。
  • 输出空间: 算法运行返回时,存储输出数据所需的空间大小;输出数据

最差情况:空间复杂度统计算法在 “最差情况” 下使用的空间大小,以体现算法运行所需预留的空间量,使用符号O表示,包含两种情况

  • 最差输入数据:当 N≤10 时,数组 nums 的长度恒定为 10 ,空间复杂度为 O(10) = O(1);当 N > 10时,数组 nums 长度为 N ,空间复杂度为 O(N) ;因此,空间复杂度应为最差输入数据情况下的 O(N)
  • 最差运行点:在执行nums = Array(10).fill(0)时,算法仅使用 O(1) 大小的空间;而当执行 nums = Array(n).fill(0)时,算法使用 O(N) 的空间;因此,空间复杂度应为最差运行点的 O(N)

常见的算法空间复杂度有:O(1) < O(logN) < O(N) < O(N^2) < O(2^N)

  • 常数O(1):普通常量、变量、对象、元素数量与输入数据大小 N 无关的集合,皆使用常数大小的空间
  • 线性O(logN):元素数量与 N 呈线性关系的任意类型集合(常见于一维数组、链表、哈希表等)
  • 平方O(N):元素数量与 N 呈平方关系的任意类型集合(常见于矩阵),皆使用平方大小的空间
  • 指数O(N^2):指数阶常见于二叉树、多叉树
  • 对数O(2^N):对数阶常出现于分治算法的栈帧空间累计、数据类型转换等。如快速排序/数字转化字符串

时空权衡

对于算法的性能,需要从时间和空间的使用情况来综合评价。优良的算法应具备两个特性,即时间和空间复杂度皆较低。而实际上,对于某个算法问题,同时优化时间复杂度和空间复杂度是非常困难的。降低时间复杂度,往往是以提升空间复杂度为代价的,反之亦然。

由于当代计算机的内存充足,通常情况下,算法设计中一般会采取空间换时间的做法,即牺牲部分计算机存储空间,来提升算法的运行速度。

一些DEMO

时间复杂度相关的demo

1
2
3
4
5
6
7
8
9
10
11
const algorithm = (n) => {
let count = 0;
const a = 100;
for (const i in n) {
for (const j in a) {
count += 1;
}
}
return count;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

const bubbleSort = (nums) => {
const n = nums.length;
for (const i in n - 1) {
for (const j in n - i - i) {
if (nums[j] > nums[j + 1]) {
cache = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = cache
}
}
}
return nums;
}

1
2
3
4
5
6
7
8

const algorithm = (n) => {
if (n <= 0) return 1
const a = algorithm(n - 1)
const b = algorithm(n - 1)
return a + b;
}

1
2
3
4
5
6
7
8
9
10

const algorithm = (n) => {
if (n <= 0) return 1
let count = 0
for (const i in n) {
count = algorithm(n - 1)
}
return count;
}

1
2
3
4
5
6
7
8
9
10
11

const algorithm = (n) => {
let count = 0
let i = n
while (i > 1) {
i = i / 2
count += 1
}
return count
}

1
2
3
4
5
6
7
8
9
10

const algorithm = (n) => {
let count = 0
let i = n
while (i > 1) {
i = i / 2
for (const j in n) count += 1
}
}

空间复杂度的相关demo

1
2
3
4
5
6
const child = () => 0

const parent = (n) => {
for (const i in n) child()
}

1
2
3
4
5
6

const parent = (n) => {
if (n <= 1) return 1
return parent(n - 1) + 1
}

1
2
3
4
5
6

const algorithm = (n) => {
const num = 5
let nums = Array(10).fill(0)
if (n > 10) nums = Array(n).fill(0)
}

时空权衡的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52


var twoSum = function(nums, target) {
const len = nums.length;
for (let i = 0; i < len - 1; i++) {
for (let j = i + 1; j < len; j++) {
if (nums[i] + nums[j] === target) {
return [i, j]
}
}
}
};




const twoSum = function(nums, target) {
const len = nums.length;


const map = new Map();
for (let i = 0; i < len; i++) {
map.set(nums[i], i);
}

for (let i = 0; i < len; i++) {
const needNum = target - nums[i];

if (map.has(needNum) && i !== map.get(needNum)) {
return [i, map.get(needNum)]
}
}
}



const twoSum = function(nums, target) {
const len = nums.length;
const map = new Map();

for (let i = 0; i < len; i++) {
const needNum = target - nums[i];

if (map.has(needNum) && i !== map.get(needNum)) {
return [i, map.get(needNum)];
}


map.set(nums[i], i);
}
}