【数据结构与算法】第一章 算法概论

第1章 概论

  • 1.3 算法
    • 1.3.1 算法的概念
    • 1.3.2 算法的设计
    • 1.4 算法的分析


天下至深是学海,世间极顶是书山❗❗❗
《数据结构与算法》阅读笔记,强烈建议购买此书反复学习
坚持每天阅读习惯,总有一日量变为质变,最终通向成功✌️
这几天会先整理以前的笔记,加油
️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️

1.3 算法

1.3.1 算法的概念

算法是对特定问题求解过程的描述,是指令的有限序列,即为解决某一特定问题而采取的具体而有限的操作步骤。程序是算法的一种实现,计算机按照程序逐步执行算法,实现对问题的求解。

NOTE:

算法具有以下性质:

  1. 算法的通用性:符合算法输入数据类型的任意输入数据,都能根据算法进行问题求解,并保证计算结果的正确性。
  2. 算法的有效性:算法是有限条指令组成的指令序列,其中每一条指令都必须能够被人或者机器确切执行。
  3. 算法的确定性:算法的确定性就是要保证每一步之后都有关于下一步动作的指令,不能缺乏下一步指令,或仅含有模糊不清的指令。
  4. 算法的有穷性:算法的执行必须要在有限步内结束。(算法不能含有死循环)

⭐设计算法时,应该关注算法的结束条件。

1.3.2 算法的设计

算法设计与算法分析是计算机科学的核心问题。

常用的算法设计方法有:

  1. 穷举法(enumeration)
    • 也称为枚举法,将求解对象一一列举出来,并验证是否满足给定条件;
    • 对象应该是有限的,有明显的穷举范围;
    • 有穷举规则,可按照某种规则列举对象;
    • 如果没有更好的方法时,可以考虑穷举法。
  2. 回溯法(backtrack)
    • 也称试探法;将问题的候选解按某中顺序逐一枚举和检验,来寻找一个满足预定条件的解;
    • 当发现当前候选解不满足条件时,就退回上一步重新选择下一个候选解,这个过程称为回溯;
    • 通常用于决策问题、优化问题、枚举问题。
  3. 分治法(divide and conquer)
    • 将一个难以直接解决的大问题,分割成一些规模较小的小问题,逐个击破,分而治之;
    • 分治的过程中会导致递归的过程产生。
  4. 递归法(recursion)
  5. 贪心法(greedy)
    • 从问题的初始状态出发,一句某种贪心的标准,通过若干次贪心选择而得出最优值。
    • 贪心算法并不是从整体上考虑问题,它所做出的选择只是在某种意义上的局部最优解;
    • 通过局部最优解构建最终的全局最优解。
  6. 动态规划法(dynamic programming)

1.4 算法的分析

⭐算法的标准

解决一个问题的方法可能有很多,但能称得上算法的,首先他必须能彻底解决这个问题(称为准确性),且根据其编写出的程序在任何情况下都不能崩溃(称为健壮性)。

在满足准确性和健壮性的基础上,还有一个重要的筛选条件,即通过算法所编写出的程序的运行效率。程序的运行效率具体可以从2哥方面衡量,分别为:

  • 程序运行的时间。
  • 程序运行所需内存空间的大小。

根据算法编写出的程序,运行时间更短,运行期间占用的内存更少,该算法的运行效率更高,算法也就越好。

在数据结构中,用时间复杂度来衡量程序运行时间的多少;用空间复杂度来衡量程序运行所需内存空间的大小。

时间复杂度

判断一个算法所需程序运行时间的多少,并不是将程序编写出来,通过在计算机运行所消耗的时间来度量。一方面,解决一个问题的算法可能有很多种——实现的工作量无疑是巨大的,得不偿失;另一方面,不同计算机的软、硬件环境不同,即便使用同一台计算机,不同时间段其系统环境也不相同,程序的运行时间很可能会受影响,严重时甚至会导致误判。

实际情况下,我们更喜欢用一个估值来表示算法所编程序的运行时间。

NOTE:虽然估值无法准确的表达算法所编程序的运行时间,但它的由来并没凭空揣测,需要通过缜密的计算后才能得出。

表示一个算法所编程序运行时间的多少,用的不是准确值(事实上也无法得出),而是根据合理方法得到的预估值。

如何预估:先分别计算按程序中每条语句的执行次数,然后用总的执行次数间接表示程序运行的时间。

代码举例:

for(int i =0; i <n ;i++) // 从 0 到 n,执行 n+1 次
{
	a++;                     // 从 0 到 n-1,执行 n 次
}

可以看到,这段程序中仅有 2 行代码,其中:

  • for 循环从 i 的值为 0 一直逐增至 n(注意,循环退出的时候 i 值为 n),因此 for 循环语句执行了 n+1 次;
  • 而循环内部仅有一条语句,a++ 从 i 的值为 0 就开始执行,i 的值每增 1 该语句就执行一次,一直到 i 的值为 n-1,因此,a++ 语句一共执行了 n 次。

因此,整段代码中所有语句共执行了 (n+1)+n 次,即 2n+1 次。数据结构中,每条语句的执行次数,又被称为该语句的频度。整段代码的总执行次数,即整段代码的频度。

另一个例子:

for(int i =0;i <n ;i++)	    // n+1
{
	for(int j =0;j <m ;j++) // n*(m+1)
	{
		num++;      		// n*m
	}
}

计算此段程序的频度为:(n+1)+n*(m+1)+nm,简化后得 2nm+2n+1。当n,m逐渐增大时,完全可以认为n==m,可以简化为2n^2+2n+1。

在数据结构中,频度表达式可以这样简化:

  • 去掉频度表达式中,所有的加法常数式子;
  • 如果表达式有多项含有无限大变量的式子,只保留一个拥有指数最高的变量的式子;
  • 如果最高项存在系数,且不为 1,直接去掉系数。

事实上,对于一个算法(或者一段程序)来说,其最简频度往往就是最深层次的循环结构中某一条语句的执行次数。

大 O 记法

  • 在得到最简频度的基础上,建立统一的规范。
  • O(频度)

常用的几种时间复杂度,以及它们之间的大小关系:

O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(n^2)平方阶 < O(n^3)(立方阶) < O(2^n) (指数阶)

NOTE:这里仅介绍了以最坏情况下的频度作为时间复杂度,而在某些实际场景中,还可以用最好情况下的频度和最坏情况下的频度的平均值来作为算法的时间复杂度。

空间复杂度

和时间复杂度类似,一个算法的空间复杂度,也常用大 O 记法表示。

要知道每一个算法所编写的程序,运行过程中都需要占用大小不等的存储空间,例如:

  • 程序代码本身所占用的存储空间;
  • 程序中如果需要输入输出数据,也会占用一定的存储空间;
  • 程序在运行过程中,可能还需要临时申请更多的存储空间。

首先,程序自身所占用的存储空间取决于其包含的代码量,如果要压缩这部分存储空间,就要求我们在实现功能的同时,尽可能编写足够短的代码。

程序运行过程中输入输出的数据,往往由要解决的问题而定,即便所用算法不同,程序输入输出所占用的存储空间也是相近的。

事实上,对算法的空间复杂度影响最大的,往往是程序运行过程中所申请的临时存储空间。不同的算法所编写出的程序,其运行时申请的临时存储空间通常会有较大不同。

所以,如果程序所占用的存储空间和输入值无关,则该程序的空间复杂度就为 O(1);反之,如果有关,则需要进一步判断它们之间的关系:

  • 如果随着输入值 n 的增大,程序申请的临时空间成线性增长,则程序的空间复杂度用 O(n) 表示;
  • 如果随着输入值 n 的增大,程序申请的临时空间成 n^2 关系增长,则程序的空间复杂度用 O(n^2) 表示;
  • 如果随着输入值 n 的增大,程序申请的临时空间成 n^3 关系增长,则程序的空间复杂度用 O(n^3) 表示;
  • 等等。

NOTE:在多数场景中,一个好的算法往往更注重的是时间复杂度的比较,而空间复杂度只要在一个合理的范围内就可以。

你可能感兴趣的:(阅读笔记,算法,数据结构)