算法与数据结构详解


一、 什么是算法和数据结构

你可能会在一些教材上看到这句话:程序 = 算法 + 数据结构

1. 算法

算法(Algorithm):是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。也就是说,能够对一定规模的输入,在有限时间内获得所要求的输出。(任何代码片段都可视为算法)
算法本质就是高效解决问题的方法。

1.1 算法的特性

算法具有五个基本特征:输入、输出、有穷性、确定性和可行性。

  1. 输入:算法具有零个或多个输入。

  2. 输出:算法具有零个或多个输出。

  3. 有穷性:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。

  4. 确定性:算法的每一个步骤都具有确定的含义,不会出现歧义。算法在一定条件下,只有一条执行路径,相同的输入只能有唯一的输出结果。

  5. 可行性:算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。

1.2 算法设计的要求

算法并不是唯一的。也就是说同一个问题,可以有多种解决问题的算法,要灵活运用。

  • 正确性

  • 可读性

  • 健壮性

  • 时间效率高和存储量低(时间复杂度和空间复杂度)


2.数据结构

数据结构(Data Structures):是计算机存储和组织数据的一种方式,可以用来高效地处理数据。

什么样的程序才是好的程序?好的程序设计无外乎两点,“快"和"省”。"快"指程序执行速度快,高效,"省"指占用更小的内存空间。这两点其实就对应"时间复杂度"和"空间复杂度"的问题。

举个例子:二分查找就是一个非常经典的算法,而二分查找经常需要作用在一个有序数组上。这里二分就是一种折半的算法思想, 而数组是我们最常用的一种数据结构,支持根据下标快速访问。很多算法需要特定的数据结构来实现,所以经常把它们放到一块讲。

实际上,在真正的项目开发中,大部分时间都是 从数据库取数据 —> 数据操作和结构化 —> 返回给前端,在数据操作过程中需要合理地抽象、 组织、处理数据,如果选用了错误的数据结构,就会造成代码运行低效。这也是我们需要学习算法和数据结构的原因。

三、算法复杂度

3.1.什么是算法复杂度?

算法复杂度分为时间复杂度和空间复杂度。

  • 时间复杂度是指执行这个算法所需要的计算工作量;
  • 而空间复杂度是指执行这个算法所需要的内存空间;
  • 时间和空间都是计算机资源的重要体现,而算法的复杂性就是体现在运行该算法时的计算机所需的资源多少;

3.2.什么是空间复杂度?

一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分。

  • 固定部分:这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
  • 可变空间:这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。

3.3.什么是时间复杂度?

在这里插入图片描述
关于时间频度:

一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)

T(n)表示代码执行的时间频度或语句频度, n表示数据规模的大小, f(n) 表示每行代码执行的次数总和。因为这是一个公式, 所以用f(n)来表示。公式中的O,表示代码的执行时间T(n)与f(n)表达式成正比。当n趋向无穷大时T(n)/f(n)无限趋近于一个非零常数,则称f(n)是T(n)的同阶函数 记作:
T(n)=O(f(n))

在刚才提到的时间频度中,n称为问题的规模(输入的数据规模),当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。 记为O(…),也称为大O表示法。

另外,时间频度不同,但时间复杂度可能相同。如:T(n)=n2+3n+4 与 T(n)= 4n2+2n+1它们的频度不同,但时间复杂度相同,都为 O(n2)。

时间复杂度去估算算法优劣的时候注重的是算法的潜力,也就是在数据规模有压力的情况之下(最坏情况)算法的执行频度,什么意思呢?比如2个算法,在只有100条数据的时候,算法a比算法b快,但是在有10000条数据的时候算法b比算法a快,这时候我们认为算法b的时间复杂对更优;

3.4.时间复杂度与空间复杂度的取舍问题

就目前来说,除了在一些特殊情况下,我们都是更加注重时间复杂度,而不是空间复杂度。注意,这里我们强调了,除了一些特殊情况外,有些特殊情况下,空间复杂度可能会更加重要。
那么,究竟什么时候应该着重考虑时间复杂度,什么时候应该着重考虑空间复杂度呢?我们来看一个例子:
设想现在需要由你来完成一个程序设计,程序要求是这样的:要求输入年份,返回该年份是否是闰年。
一提到这个问题,我想如果你学习过任何一门语言,你可能都做过类似的题目。你可能思路已经非常清晰了,满百除四百,不满除以4。

额,先不要急。我们来看看还能不能进一步提高性能,降低时间复杂度。也就是用空间复杂度来换取时间复杂度。比如,如果使用我们程序的用户,只会查看当前年份未来几年和过去几年的日历的话,我们完全可以使用一个比如:2100个元素的数组,每个元素为0或1,分别表示平年和闰年。这样当用户查询的时候,就不需要再进行复杂的逻辑判断,而只需要取出对应下标位置的元素即可。
反过来,如果我们的用户经常查询跨度上万年的日历信息(万年历),那么,我们肯定不能使用上面牺牲空间复杂度来换取时间复杂度的方案解决。因为如此巨大的空间消耗是我们损失不起的。

而编程的精髓和美,并不在于一方的退让和妥协。而是在于如何在二者之间取一个平衡点,完成华丽变身。那么,对于我们这种程序应该如何权衡呢?

我想到的一种方案是:将与当前年份相近的几年存为固定数据,查询时只需要读取即可。而对于那些和当前年份相距较远的年份的数据,在用户请求查询时动态生成。

这样,既能在损失可接受空间的情况下,大幅度提高性能,又能保证空间的损失不至于太大而无法接受。我想当用户查询据今较远的数据时,有一些时间上的等待,也是可以接受的。

总结下这一段的核心思想:
不能简单的说时间复杂度就比空间复杂度重要,在特定场景下空间复杂度反而比时间复杂度重要,在程序中我们需要综合考虑让时间和空间的消耗达到一个平衡点,从上面平闰年计算的例子来看,我们可以缓存前后几年间的平润年,因为内存开销在可控范围内,至少是在现有条件下能够体验到的可接受范围,所以这几年的数据我们可以用增大空间消耗来减少时间的消耗,如果说要将一万年的所有平闰年数据都存上,那么即便是内存能撑得住也是得不偿失的,所以这时候我们用增大时间开销(网络请求,动态加载)去交换减少空间的开销(省去了万级数据的存储空间); 所以综上,这就是个综合考量的问题;

上面也分析了,时间换空间或空间换时间都是根据现实情况来分析,而目前的现实情况就是,硬件内存这些东西的成本与极致的用户体验,比如更快的响应,操作的流畅感比起来,就目前的条件来看,重要程度是更低的,随着空间可接受度增大,时间可接受程度相对变小,说白了,就是现在空间条件宽裕了(各大设备厂家无脑怼硬件,说明目前空间资源成本相对较低),大家拼的就是速度,谁的算法执行快,谁的产品用户体验更好,谁在竞争中更有优势;

四、 算法分析

为什么要进行算法分析?

  • 预测算法所需的资源
    1. 计算时间(CPU 消耗)
    2. 内存空间(RAM 消耗)
    3. 通信时间(带宽消耗
  • 预测算法的运行时间
  1. 在给定输入规模时,所执行的基本操作数量。
  2. 或者称为算法复杂度(Algorithm Complexity)
  • 如何衡量算法复杂度?
    内存(Memory)
    时间(Time)
    指令的数量(Number of Steps)
    特定操作的数量
    磁盘访问数量
    网络包数量
    渐进复杂度(Asymptotic Complexity)
  • 算法的运行时间与什么相关?
    取决于输入的数据。(例如:如果数据已经是排好序的,时间消耗可能会减少。)
    取决于输入数据的规模。(例如:6 和 6 * 109)
    取决于运行时间的上限。(因为运行时间的上限是对使用者的承诺。)
  • 算法分析的种类:
    最坏情况(Worst Case):任意输入规模的最大运行时间。(Usually)
    平均情况(Average Case):任意输入规模的期待运行时间。(Sometimes)
    最佳情况(Best Case):通常最佳情况不会出现。(Bogus)
    例如,在一个长度为 n 的列表中顺序搜索指定的值,则
    最坏情况:n 次比较
    平均情况:n/2 次比较
    最佳情况:1 次比较

而实际中,我们一般仅考量算法在最坏情况下的运行情况,也就是对于规模为 n 的任何输入,算法的最长运行时间。这样做的理由是:

  1. 一个算法的最坏情况运行时间是在任何输入下运行时间的一个上界(Upper Bound)。
  2. 对于某些算法,最坏情况出现的较为频繁。
  3. 大体上看,平均情况通常与最坏情况一样差。
  4. 算法分析要保持大局观(Big Idea),其基本思路:
    忽略掉那些依赖于机器的常量。
    关注运行时间的增长趋势。
    比如:T(n) = 73 n3 + 29n3 + 8888 的趋势就相当于 T(n) = Θ(n3)。

渐近记号(Asymptotic Notation)通常有 O、 Θ 和 Ω 记号法。Θ 记号渐进地给出了一个函数的上界和下界,当只有渐近上界时使用 O 记号,当只有渐近下界时使用 Ω 记号。尽管技术上 Θ 记号较为准确,但通常仍然使用 O 记号表示。

使用 O 记号法(Big O Notation)表示最坏运行情况的上界。例如,

线性复杂度 O(n) 表示每个元素都要被处理一次。
平方复杂度 O(n2) 表示每个元素都要被处理 n 次。

在这里插入图片描述

例如:

  • T(n) = O(n3) 等同于 T(n) ∈ O(n3)

  • T(n) = Θ(n3) 等同于 T(n) ∈ Θ(n3).
    相当于:

  • T(n) 的渐近增长不快于 n3

  • T(n) 的渐近增长与 n3 一样快。

复杂度标记符号描述
常量(Constant)O(1)操作的数量为常数,与输入的数据的规模无关。n = 1,000,000 -> 1-2 operations
对数阶(Logarithmic)O(log2n)操作的数量与输入数据的规模 n 的比例是 log2n。n = 1,000,000 -> 20 operations
线性阶(Linear)O(n)操作的数量与输入数据的规模 n 成正比。n = 10,000 -> 10000 operations
平方阶(Quadratic)O(n2)操作的数量与输入数据的规模 n 的比例为二次平方。n = 500 -> 250,000 operations
立方阶(Cubic)O(n3)操作的数量与输入数据的规模 n 的比例为三次方。n = 200 -> 8,000,000 operations
指数阶(Exponential)O(2n)
O(kn)
O(n!)
指数级的操作,快速的增长。n = 20 -> 1048576 operations

注:

  1. 快速的数学回忆,logab = y 其实就是 ay = b。所以,log24 = 2,因为 22 = 4。同样 log28 = 3,因为 23 = 8。我们说,log2n 的增长速度要慢于 n,因为当 n = 8 时,log2n = 3。

  2. 通常将以 10 为底的对数叫做常用对数。为了简便,N 的常用对数 log10N 简写做 lg N,例如 log105 记做 lg5。

  3. 通常将以无理数 e 为底的对数叫做自然对数。为了方便,N 的自然对数 logeN 简写做 lnN,例如 loge3 记做 ln 3。

  4. 在算法导论中,采用记号 lg n = log2n ,也就是以 2 为底的对数。改变一个对数的底只是把对数的值改变了一个常数倍,所以当不在意这些常数因子时,我们将经常采用 "lg n"记号,就像使用 O 记号一样。计算机工作者常常认为对数的底取 2 最自然,因为很多算法和数据结构都涉及到对问题进行二分。

而通常时间复杂度与运行时间有一些常见的比例关系:
在这里插入图片描述
在各种不同的算法中,若算法语句的执行次数为常数,则算法的时间复杂度为O(1),按数量级递增排列,常见的时间复杂度量有:

  1. O(1):常量阶,运行时间为常量

  2. O(logn):对数阶,如二分搜索算法

  3. O(n):线性阶,如n个数内找最大值

  4. O(nlogn):对数阶,如快速排序算法

  5. O(n^2):平方阶,如选择排序,冒泡排序

  6. O(n^3):立方阶,如两个n阶矩阵的乘法运算

  7. O(2^n):指数阶,如n个元素集合的所有子集的算法

  8. O(n!):阶乘阶,如n个元素全部排列的算法

下图给出了随着n的变化,不同量级的时间复杂度变化曲线。
在这里插入图片描述

计算代码块的渐进运行时间的方法有如下步骤:

  • 确定决定算法运行时间的组成步骤。
  • 找到执行该步骤的代码,标记为 1。
  • 查看标记为 1 的代码的下一行代码。如果下一行代码是一个循环,则将标记 1 修改为 1 倍于-循环的次数 1 * n。如果包含多个嵌套的循环,则将继续计算倍数,例如 1 * n * m。
    找到标记到的最大的值,就是运行时间的最大值,即算法复杂度描述的上界。

主要启示

  1. 算法的速度指的是操作数的增速,而非时间。

  2. 谈论算法速度说的是随着输入的增加,其运行时间将以什么样的速度增加。

  3. 用大O表示法表示算法的运行时间。

  4. 随着元素的增加,快算法比慢算法增加的速度是指数级的。比如,O(log n)O(n)

参考文献:

  • https://blog.csdn.net/yq272393925/article/details/89146451?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522161838082016780271521643%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=161838082016780271521643&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v29-2-89146451.pc_search_result_hbase_insert&utm_term=%E7%AE%97%E6%B3%95%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6

  • https://blog.csdn.net/user11223344abc/article/details/8148584

  • https://www.cnblogs.com/gaochundong/p/complexity_of_algorithms.html

  • https://blog.csdn.net/user11223344abc/article/details/81485842

  • https://blog.csdn.net/qiumengchen12/article/details/45697405

  • 5
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值