1、时空复杂度
本文所说的时空复杂度指的是:时间空复杂度 和 空间复杂度
时空复杂度的作用:不用具体的测试数据来测试,就可以粗略地估计算法的执行效率或者内存使用率的方法。
大 O
既可以表示时间复杂度,也可以表示空间复杂度。大 O
接收一个函数,该函数指明:某个算法的 耗时 / 耗空间 与 数据增长量
之间的关系。其中的 n
代表输入数据的量。
时间复杂度(T(n) 指的是:执行时间): T ( n ) = O ( f ( n ) ) T(n) = O(f(n)) T(n)=O(f(n))
空间复杂度(S(n) 指的是:存储空间): S ( n ) = O ( f ( n ) ) S(n) = O(f(n)) S(n)=O(f(n))
2、常见的时间复杂度
算法时间复杂的排行如下:
O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(n^2)平方阶 < O(n^3)立方阶 < O(2^n)指数阶
常见的时间复杂度:
O(n)
:代表数据量增大几倍,耗时也增大几倍。比如常见的遍历算法。O(n^2)
:代表数据量增大n倍时,耗时增大n的平方倍,这是比线性更高的时间复杂度。比如冒泡排序,就是典型的O(n^2)
的算法,对n个数排序,需要扫描n×n次。O(logn)
:当数据增大n倍时,耗时增大logn倍(这里的log是以2为底的,比如,当数据增大256倍时,耗时只增大8倍,是比线性还要低的时间复杂度)。二分查找就是O(logn)
的算法,每找一次排除一半的可能,256个数据中查找只要找8次就可以找到目标。O(nlogn)
:当数据增大n倍时,耗时增大n乘以logn倍。比如,当数据增大256倍时,耗时增大256*8=2048倍。这个复杂度高于线性低于平方。归并排序就是O(nlogn)
的时间复杂度。O(1)
:O(1)
就是最低的时空复杂度了,也就是耗时/耗空间与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。 哈希算法就是典型的O(1)
时间复杂度,无论数据规模多大,都可以在一次计算后找到目标(不考虑冲突的话)。
【拓展】补充一个数学知识:
如果ax=N(a>0,且a≠1),那么数x叫做以a为底N的对数,记作x=logaN,读作以a为底N的对数,其中a叫做对数的底数,N叫做真数。
例如:2 ^3 = 8 即 log 8 = 3。
3、数据结构与时间复杂度表
数据结构 | 查找 | 插入 | 删除 | 遍历 | |||
---|---|---|---|---|---|---|---|
平均 | 最坏 | 平均 | 最坏 | 平均 | 最坏 | ||
数组 | O(n) | O(n) | O(n) | O(n) | O(n) | O(n) | -- |
有序数组 | O(logn) | O(n) | O(n) | O(n) | O(n) | O(n) | O(n) |
链表 | O(n) | O(n) | O(1) | O(1) | O(1) | O(1) | -- |
有序链表 | O(n) | O(n) | O(n) | O(n) | O(1) | O(1) | O(n) |
二叉树查找 | O(logn) | O(n) | O(logn) | O(n) | O(logn) | O(n) | O(n) |
红黑树 | O(logn) | O(logn) | O(logn) | O(logn) | O(logn) | O(logn) | O(n) |
平衡树 | O(logn) | O(logn) | O(logn) | O(logn) | O(logn) | O(logn) | O(n) |
二叉堆 / 优先队列 | O(1) | O(1) | O(logn) | O(logn) | O(logn) | O(logn) | O(n) |
哈希表 | O(1) | O(1) | O(1) | O(1) | O(1) | O(1) | O(n) |
4、对增长数量级的常见假设的总结表
描述 | 增长的数量级 | 典型的代码 | 说明 | 举例 |
---|---|---|---|---|
常数级别 | 1 | a = b + c a = b + c a=b+c | 普通语句 | 将两个数相加 |
对数级别 | log 2 N \log_2 N log2N | 另见 对数级别代码的案例 | 二分策略 | 二分查找 |
线性级别 | N N N | 一个for 循环嵌套 | 循环 | 找出最大元素 |
线性对数级别 | N log N N\log N NlogN | 另见 线性对数级别代码的案例 | 分治 | 归并排序 |
平方级别 | N 2 N^2 N2 | 双for 循环嵌套,另见 平方级别代码案例 | 双层循环 | 查找所有元素 |
立方级别 | N 3 N^3 N3 | 三个for 循环嵌套 | 三层循环 | 查找所有三元组 |
指数级别 | 2 N 2^N 2N | 另见 指数级别代码案例 | 穷举查找 | 查找所有子集 |
【题目】:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
// 示例:
// 输入
nums = [-1,0,3,5,9,12], target = 9
// 输出: 4
// 解释: 9 出现在 nums 中并且下标为 4
分析:本题应采用 “二分查找” 的方式来解答。
二分查找的实现方式取决于区间的定义,区间的定义就是不变量,循环不变量规则是 二分查找 的核心。
二分查找的区间的定义有 2 种方式:
- 左闭右闭,也就是:[left, right]。
- while(left <= right) if (nums[middle] >
target) 则right = middle - 1
;if (nums[middle] < target) 则left = middle + 1
。
- while(left <= right) if (nums[middle] >
- 左闭右开,也就是也就是:[left, right)。
- while(left < right) if
(nums[middle] > target) 则right = middle
;if (nums[middle] < target) 则left = middle + 1
。
- while(left < right) if
解答:
// 采用 二分查找
const search = (nums, target) => {
let l = 0
let r = nums.length - 1
while(l <= r) {
let m = Math.floor((r - l) / 2) + l
if (nums[m] > target) {
r = m - 1
} else if (nums[m] < target) {
l = m + 1
} else {
return m
}
}
return -1
};
search([-1,0,3,5,9,12], 9); // 4
【题目】:将数组 [8, 4, 5, 7, 1, 3, 6, 2] 按照升序的方式进行归并排序。
分析:
归并排序使用分而治之的概念对给定的元素列表进行排序。它将问题分解为较小的子问题,直到它们变得足够简单以至可以直接解决为止。
以下是归并排序的步骤:
- 将给定的列表分为两半(如果列表中的元素数为奇数,则使其大致相等)。
- 以相同的方式继续划分子数组,直到只剩下单个元素数组。
- 从单个元素数组开始,合并子数组,以便对每个合并的子数组进行排序。
- 重复第 3 步,直到最后得到一个排好序的数组。
解答:
// 对数组进行排序并合并
function merge(left, right) {
let arr = [];
// 此条件代表当有一个数组为空之后,则停止循环
while (left.length && right.length) {
// 从左右子数组的最小元素中选择较小的元素 push 到 arr
if (left[0] > right[0]) {
arr.push(right.shift());
} else {
arr.push(left.shift());
}
}
// 输出的数组要加上 left 或 right 剩下的值,由于每次都 push 最小值,则剩余的一定是大值,所以 push 到最后
// 由于 left 和 right length 可能为相等或相差 1,所以这里 left 和 right 不分先后
return [...arr, ...left, ...right];
}
function mergeSort(arr) {
// 1. 当数组元素小于 2 个,则无需排序,直接返回
if (arr.length < 2) {
return arr;
}
// 2. 获取数组中间值,无需取整,splice 会以向下取整截取
const half = arr.length / 2;
// 3. 截取数组前一半
const left = arr.splice(0, half);
// 4. 剩余为数组右半边
const right = arr;
// 5. 返回合并和排序后的数组
return merge(mergeSort(left), mergeSort(right));
}
mergeSort([8, 4, 5, 7, 1, 3, 6, 2]); // [1, 2, 3, 4, 5, 6, 7, 8]
所有可以用 双 for 循环 算法实现的,都可以改为 双指针 算法进行优化,以空间换时间。
【题目一】:将数组 [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48] 按照从小到大的顺序进行冒泡排序。
动态演示:
解答:
function bubbleSort(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]){
let tmp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = tmp;
}
}
}
return arr;
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
bubbleSort(arr); // [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
双指针优化冒泡排序:
var bubbleSort = function(nums) {
let len = nums.length;
// 特殊情况
if (len === 1) { return nums[0] }
let i = 0, j = 1; // 快慢双指针
let t = 1; // 记录循环被初始化的次数
while (j <= len - t) {
if (nums[i] > nums[j]) { [nums[i], nums[j]] = [nums[j], nums[i]] }
i++;
j++;
// 初始化循环
if (j > len - t) {
i = 0;
j = 1;
t++;
}
}
return nums;
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
bubbleSort(arr);
【题目二】:将数组 [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48] 按照升序进行选择排序。
动态演示:
解答:
function selectSort(arr){
for(let i = 0;i < arr.length-1;i++){
let min = i;
for(let j = min+1;j < arr.length;j++){
// 如果前面的数大,放到后面(当然是从小到大的冒泡排序)
if(arr[min] > arr[j]){
min = j;
}
}
let tmp = arr[min];
arr[min] = arr[i];
arr[i] = tmp;
}
return arr;
}
const arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
selectSort(arr); // [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
双指针优化选择排序:
var bubbleSort = function(nums) {
let len = nums.length;
// 特殊情况
if (len === 1) { return nums[0] }
let i = 0; // 慢指针,双重含义:记录循环被初始化的次数,以及当前的存储地址
let j = 1; // 快指针,指向原数组里非首个的数据
while (j <= i * (len - i)) {
let m = i; // 原数组里尚未被选择的最小值
if (m > nums[j]) {
m = j
}
// 初始化循环
if (j > i * (len - i)) {
[nums[i], nums[m]] = [nums[m], nums[i]]
i++;
j = i + 1;
}
}
return nums;
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
bubbleSort(arr);
更多冒泡排序和选择排序请看这篇文章。
枚举(穷举)算法的本质是从所有候选答案中去搜索正确的解。
使用该算法需要满足两个条件:
- 可预先确定候选答案的数据量。
- 候选答案的范围在求解之前必须有一个确定的集合。
优点:算法简单,在局部地方使用枚举法效果十分的好。
缺点:运算量过大,当问题的规模变大时候,循环的阶数越大,执行速度越慢。
【题目一】获取数组 [1, 2, 3] 的所有可能子集。
解答:
function getAllSubsets(array) {
const subsets = [[]];
let m = 0, n = 0;
for (const el of array) {
m++;
const last = subsets.length-1;
for (let i = 0; i <= last; i++) {
n++;
subsets.push( [...subsets[i], el] );
}
}
console.log(`m:${m}, n:${n}`)
return subsets;
}
getAllSubsets([1, 2, 3]); // 结果见下图
对于数组 [1, 2, 3],上述算法的执行过程如下:
- 从一个空子集开始: []
- 通过向每个现有子集添加“1”来创建新子集。这将是:[] [1]
- 通过向每个现有子集添加“2”来创建新子集。这将是:[], [1] [2], [1, 2]
- 通过向每个现有子集添加“3”来创建新子集。这将是:[], [1], [2], [1, 2] [3], [1, 3], [2, 3], [1, 2, 3]
【题目二】“百钱百鸡问题”——鸡翁一,值钱五,鸡母一,值钱三,鸡雏三,值钱一;百钱买百鸡,问翁、鸡、雏各几值?
分析:设翁为 x、母为 y、雏为 z,则有以下关系成立:
- x + y + z = 100
- 5x + 3y + z/3 = 100
// x不可能超过20只,y不可能超过33只
//此方法 21 * 34 = 714
function hundredBuyChicken(){
var z = 0;
let m = 0, n = 0;
for(var x = 0; x <= 20; x++){
m++;
for(var y = 0; y <= 33; y++){
n++;
z = 100 - x - y;
if(z % 3 == 0 && 5 * x + 3 * y + z/3 == 100){
console.log(`x=${x}, y=${y}, z=${z}`);
}
}
}
console.log(`m:${m}, n:${n}`);
}
hundredBuyChicken();
/**
x=0, y=25, z=75
x=4, y=18, z=78
x=8, y=11, z=81
x=12, y=4, z=84
m:21, n:714
**/
5、对数(了解)
对数运算法则:指积、商、幂、方根的对数运算的运算法则。
(1)、对数的恒等
对数的恒等公式:
a
log
a
M
=
M
a^{\log{_a{M}}} = M
alogaM=M
(2)、指数和对数
指数和对数的互相转换关系:
若:
a
x
=
b
则:
x
=
log
a
b
若:a^{x} = b 则:x = \log _a{b}
若:ax=b则:x=logab
由指数和对数的互相转换关系,可得以下 3 个法则:
-
两个正数的积的对数 = 同一底数的这两个数的对数的和。比如:
log a m × n = log a m + log a n 例如: log 2 3 × 5 = log 2 3 + log 2 5 \log{_a{m \times n}} = \log{_a{m}} + \log{_a{n}} 例如:\log{_2{3 \times 5}} = \log{_2{3}} + \log{_2{5}} logam×n=logam+logan例如:log23×5=log23+log25 -
两个正数商的对数 = 同一底数的被除数的对数减去除数对数的差, 比如:
log a m n = log a m − log a n 例如: log 2 5 3 = log 2 5 − log 2 3 \log{_a{\frac{m}{n}}} = \log{_a{m}} - \log{_a{n}} 例如:\log{_2{\frac{5}{3}}} = \log{_2{5}} - \log{_2{3}} loganm=logam−logan例如:log235=log25−log23 -
一个正数幂的对数 = 幂的底数的对数乘以幂的指数。比如:
log a b m = m log a b 例如: log 2 3 2 = 2 log 2 3 \log _a{b}^{m} = m\log _a{b} 例如:\log _2{3}^{2} = 2\log _2{3} logabm=mlogab例如:log232=2log23
(3)、对数的换底
对数的换底公式(c可以是任意正整数):
log
a
b
=
log
c
b
log
c
a
例如:
log
2
5
=
log
10
5
log
10
2
\log{_a{b}} = \frac{\log{_c{b}}}{\log{_c{a}}} 例如:\log{_2{5}} = \frac{\log{_{10}{5}}}{\log{_{10}{2}}}
logab=logcalogcb例如:log25=log102log105
由对数的换底公式,可以推出以下 2 个法则:
-
对数的导数定理:对数乘法的运算原则——换底–>约分
log a b × log b a = 1 例如: log 3 5 × log 5 3 = 1 \log{_a{b}} \times \log{_b{a}} = 1 例如:\log{_3{5}} \times \log{_5{3}} = 1 logab×logba=1例如:log35×log53=1 -
底数包含幂的对数的处理法则:
log a m b n = n m log a b 例如: log 2 5 7 3 = 3 5 log 2 7 \log {_{a^{m}}{{b^{n}}}} = \frac{n}{m}\log {_a{b}} 例如:\log {_{2^{5}}{{7^{3}}}} = \frac{3}{5}\log {_2{7}} logambn=mnlogab例如:log2573=53log27
【数学公式】
MathJax basic tutorial and quick reference
LaTeX在线:吴文中 数学公式编辑器(推荐)
markdown中数学符号公式和字母表示
【推荐】
时间复杂度