时空复杂度


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 N2for循环嵌套,另见 平方级别代码案例双层循环查找所有元素
立方级别 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
  • 左闭右开,也就是也就是:[left, right)。
    • while(left < right) if
      (nums[middle] > target) 则 right = middle;if (nums[middle] < target) 则 left = middle + 1

解答:

// 采用 二分查找
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=logamlogan例如:log235=log25log23

  • 一个正数幂的对数 = 幂的底数的对数乘以幂的指数。比如:
    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中数学符号公式和字母表示





【推荐】
时间复杂度

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值