当前位置: 首页 > article >正文

1.1 算法基本概念与复杂度分析

1.1 算法基本概念与复杂度分析

算法是一种解决问题的具体方法。在这一章节中,我们将会了解到算法的基本概念,包括输入、输出、算法正确性、算法复杂度等。我们还将会学习如何分析算法的时间复杂度和空间复杂度,以便在实际应用中选择最优的算法。

什么是算法?

算法是一种解决问题的具体方法。我们可以将生活中的很多问题都看作一个算法。例如,制作一杯咖啡的过程就是一个算法:

  1. 准备一个空杯子。
  2. 将咖啡豆研磨成合适的粒度。
  3. 将咖啡粉放入咖啡机中。
  4. 加入足够的水。
  5. 启动咖啡机。
  6. 等待几分钟,直到咖啡机完成任务。
  7. 将咖啡倒入杯子中。
  8. 按需添加糖或牛奶。
  9. 享受你的咖啡。

这个过程就是一个算法,它有输入(空杯子、咖啡豆、水)、输出(一杯咖啡)、过程(研磨、冲泡)、正确性(研磨成合适的粒度,加入足够的水等)和复杂度(咖啡豆的研磨时间、水的加入量等)。同样,制作一个蛋糕、修理一辆车、打扫房间等都可以看作是一个算法,只是它们的 输入输出过程正确性复杂度 都不同而已。

初识复杂度

复杂度是一个用于衡量算法好坏的参考标准。就像我们评价一个人的诚信度,可以使用芝麻分作为参考一样。

那什么又算是好的算法呢,下面我们来看一张图:

在这里插入图片描述

从图中我们可以看出,蓝色的线随着数据规模的增加,程序运行时间并没有显著增加。而红色的线,随着数据规模增加,运行时间增长的非常快,以至于数据规模到达一定程度,程序的运行时间会变得不可接受。

你可能已经发现了,所谓的复杂度其实就是一个

时间复杂度和空间复杂度

复杂度分析中我们通常会讨论两种复杂度:

  • 时间复杂度:程序的运行时间随数据规模变化的函数。
  • 空间复杂度:程序占用的存储空间随数据规模变化的函数。

复杂度更优的算法,可以起到事半功倍的效果。而复杂度过高的算法,只能停留在理论层面,无法在真实世界中解决工程问题,毕竟如果做一次计算需要一万年,那没人会真正去运行这个程序。

在当今这个时代,计算机的存储成本已经极其低廉,所以我们更侧重关注时间复杂度,有时还会刻意的牺牲空间来换取时间。

复杂度的计算和表示方式

用一台20年前的计算机,和你现在正在用的电脑,运行同一个程序的话,你觉得哪台设备速度更快?显然,这没有可比性。所以复杂度的计算,需要排除硬件和环境的干扰,我们纯粹在理论层面去评判算法的优劣。

可以假设任何计算机的CPU运行一行代码需要使用1个单位时间(Unit Time),记作 1(ut) 。

那么我们看看下面这个函数:

int fun1(int n){
  return n + 1;
}

核心代码只有一行 return n + 1; ,我们粗略的将其运行时间记作 1(ut),随着输入参数n的变化,函数的运行时间永远都是 1(ut)。

接下来看看这个函数:

int fun2(int n){
  n /= 2;
  n = n * n * 3;
  return n + 5;
}

核心代码有三行 ,我们粗略的将其运行时间记作 3(ut),随着输入参数 n 的变化,函数的运行时间永远都是 3(ut)。

上面的两个例子中的算法,无论输入数据怎么变化,近似的计算时间永远是一个常数,我们把这种算法叫做常数阶。

习惯上我们用一种叫做”大O表示法的形式”,将上面两个程序的时间复杂度记为 O(1) 和 O(3)。

由于任何常数在无穷大面前都可以忽略不计,所以我们在表示常数阶时,一般不会保留具体的数字,而是全部简单的用数字 1 来代替,所以上面两个算法的复杂度都可以写为 O(1)。

下面以Java代码为例,列举常见的各种复杂度的示例算法。

  1. O(1) 常数阶函数:该函数的运行时间与输入规模无关。
public static int constantTime(int x, int y) {
    return x + y; // 该函数的运行时间不随输入规模而变化
}
  1. O(log n) 对数阶函数:该函数的运行时间随着输入规模的增长而增长,但增长缓慢。
public static int logarithmicTime(int n) {
    int count = 0;
    for (int i = 1; i < n; i *= 2) {
        count++;
    }
    return count; // 该函数的运行时间随输入规模的增长而增长,但增长缓慢
}
  1. O(n) 线性阶函数:该函数的运行时间随着输入规模的增长而线性增长。
public static int linearTime(int[] arr) {
    int sum = 0;
    for (int i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
    return sum; // 该函数的运行时间随输入规模的增长而线性增长
}
  1. O(n log n) 线性对数阶函数:该函数的运行时间随着输入规模的增长略微超过线性增长。
public static int linearithmicTime(int[] arr) {
    Arrays.sort(arr);
    int sum = 0;
    for (int i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
    return sum; // 该函数的运行时间随输入规模的增长略微超过线性增长
}
  1. O(n^2) 平方阶函数:该函数的运行时间随着输入规模的增长呈平方级增长。
public static int quadraticTime(int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            sum++;
        }
    }
    return sum; // 该函数的运行时间随输入规模的增长呈平方级增长
}
  1. O(n^3) 立方阶函数:该函数的运行时间随着输入规模的增长呈立方级增长。
public static int cubicTime(int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            for (int k = 0; k < n; k++) {
                sum++;
            }
        }
    }
    return sum; // 该函数的运行时间随输入规模的增长呈立方级增长
}
  1. O(2^n) 指数阶函数:该函数的运行时间随着输入规模的增长呈指数级增长。
public static int exponentialTime(int n) {
    if (n <= 0) {
        return 1;
    }
    return exponentialTime(n-1) + exponentialTime(n-2);
    // 该函数的运行时间随输入规模的增长呈指数级增长
}
  1. O(n!) 阶乘阶函数:该函数的运行时间随着输入规模的增长呈阶乘级增长。
public static int factorialTime(int n) {
    if (n == 1) {
        return 1;
    }
    return n * factorialTime(n-1);
    // 该函数的运行时间随输入规模的增长呈阶乘级增长
}
中文名称英文名称复杂度表示描述
常数阶Constant TimeO(1)算法的运行时间不随输入规模而变化
对数阶Logarithmic TimeO(log n)算法的运行时间随输入规模的增长而增长,但增长缓慢
线性阶Linear TimeO(n)算法的运行时间随输入规模的增长而线性增长
线性对数阶Linearithmic TimeO(n log n)算法的运行时间随输入规模的增长而略微超过线性增长
平方阶Quadratic TimeO(n^2)算法的运行时间随输入规模的增长而呈平方级增长
立方阶Cubic TimeO(n^3)算法的运行时间随输入规模的增长而呈立方级增长
指数阶Exponential TimeO(2^n)算法的运行时间随输入规模的增长而呈指数级增长
阶乘阶Factorial TimeO(n!)算法的运行时间随输入规模的增长而呈阶乘级增长

在这里插入图片描述

需要注意的是,对于一个算法,我们考虑他的整体复杂度时,通常只保留最高阶的,而忽略随着数据规模增加,影响可以忽略不计的低阶。

例如,如果有一个算法中既包含常数阶,对数阶又包含平方阶 O(5 + log n + 3*n^2),我们可以直接将其记作 O(n^2)。


http://www.kler.cn/a/389192.html

相关文章:

  • 在Flutter中,禁止侧滑的方法
  • 设计模式-七个基本原则之一-迪米特法则 + 案例
  • 如何在Python中实现一个简单的搜索引擎:从零开始的指南
  • 操作系统离散存储练习题
  • Linux设置socks代理
  • 【JAVA】正则表达式中的中括弧
  • qt QWebSocketServer详解
  • 第十三届交通运输研究(上海)论坛┆智能网联汽车技术现状与研究实践
  • SpringBoot项目编译报错 类文件具有错误的版本 61.0, 应为 52.0
  • ssh2-sftp-client一键将你的前端项目部署到远程服务器,你只需要专注写bug就好了
  • Rust:GUI 开源框架
  • 数据库范式、MySQL 架构、算法与树的深入解析
  • 实现rtos操作系统 【二】基本任务切换实现
  • 大模型,智能家居的春秋战国之交
  • goframe开发一个企业网站 验证码17
  • DotNet使用CsvHelper快速读取和写入CSV文件的操作方法
  • Apache DolphinScheduler + OceanBase,搭建分布式大数据调度平台的实践
  • 2411d,右值与移动
  • linux入门教程:perl库
  • 【刷题12】ctfshow刷题
  • 面试官:进程与线程的关系和区别到底是什么?
  • 大数据学习10之Hive高级
  • 【从零开始的LeetCode-算法】540. 有序数组中的单一元素
  • 营收增长,净利润却暴跌695.9%,昆仑万维在赌什么?
  • 后端-内连接(INNER JOIN),左外连接(LEFT JOIN)和右外连接(RIGHT JOIN)的区别
  • 【Vue】-组件开发-一个树组件