数据结构面试(Java语言)NO.1
- 课程大纲
- 算法之间的比较
- 简介
- 重要的评判标准:时间和空间
- 比较执行时间
- 实验评估
- 理论分析评估
- 分析一个简单的Java程序
课程大纲
数据结构是计算机科学的基础,也是每个程序中的重要决策。因此,在进行面试时,数据结构也会作为一项重要的考核基准。本课程包含对所有常见数据结构的详细回顾,并提供Java实现级别的详细信息,使各位读者能够详细准备面试中的此部分知识。
算法之间的比较
本节当中,我们将学习如果比较两个或多个算法,我们将会涉及以下知识:
- 简介;
- 重要的评判标准:时间和空间复杂度;
- 比较执行时间:
- 实验评估;
- 理论分析评估;
- 最好情况下(Best Case)的分析;
- 最差情况下(Worst Case)的分析;
- 平均情况(Average Case)的分析;
- 分析一个简单的Java程序。
简介
在解决一个给定的计算问题的时候,我们可能会有很多不同的算法可以使用。很自然地我们会比较这些算法的性能,但是我们如何才能知道算法A的性能比算法B的性能要好呢?
重要的评判标准:时间和空间
很重要的一个评判算法性能好坏的因素是一个算法利用了多长时间来解决一个给定的问题。如果算法A相对于算法B而言使用了较少的时间,那么我们就认为算法A会好一些。
比较执行时间
在本文剩余的内容中,我们会关注于第一个因素,执行时间。我们如何比较两个算法的执行时间呢?
实验评估
好吧,其实我们可以实现这两种算法并在计算机上运行它们,同时测量执行时间,那很显然执行时间短的算法获胜。有一件事情是肯定的:这种比较必须以公平的方式进行。让我们尝试突破这个想法:
- 一个算法的输入越大,执行的时间可能会越长。因此,进行比较的算法之间必须以同等的输入数据规模进行测量。但这不是全部。由于条件控制语句的出现,对于一个给定的数据规模(注意只是规模),即使是同一个算法针对不同的输入(规模相同,数据内容不同),运行时间可能也会不一样。这也就说明进行比较的算法之间必须利用相同的输入进行测试,我们必须精疲力竭地对各种可能的输入进行测试,这是根本不可能的。
- 要比较的算法必须首先被实现出来才行。如果一个程序员针对一个算法写出了一种更好的实现方式怎么办?如果编译器对其中一个算法有更好地优化怎么办?在这个层面上会很大程度削弱比较的公平性。
- 实现要比较的算法的程序必须在相同的硬件和软件环境下进行测试。尽管可能如此,我们可以为所有科学家分配一台机器来测试他们的算法。即使我们这样做,现代操作系统中的任务调度也涉及很多随机性。如果与“最佳”算法相对应的程序遇到过多的硬件中断该怎么办?所以我们不可能保证相同的硬件/软件环境以确保比较的公平性。
理论分析评估
以上我们谈论的几点因素说明了进行公平的实验评估算法的性能是不可能的。所以,我们必须进行理论分析比较。在之前的讨论当中,我们知道有两点关键的因素,就是我们必须确保相同的数据规模和相同规模下所有可能的输入。以下是其如何完成的。
我们假设有一台虚拟计算机,它针对一些基本操作(primitive operations)会在一个恒定的时间当中被执行。我们假设一个给定的数据输入规模,比如:n。之后在一个给定的数据输入情况下,我们计算基本操作被执行的个数。在这种分析模式下,很显然基本操作个数较少的那一个算法性能更好。
那么基本操作的组成都包括哪些呢?你可以将它们视为通常作为处理器指令实现的简单操作。这些操作包括分配给变量或数组索引(index),从变量或数组索引读取,比较两个值,算术运算,函数调用(function call)等。
什么不被看做一个基本操作呢?我们用函数调用来举个例子。当一个函数被调用的时候,函数中的所有语句(statements)都被执行了。因此,我们不能将每个函数调用都仅仅视为一个基本操作。同样,打印整个数组也不是基本操作。
- 最好情况分析
在最好情况中,我们认为给定的输入会进行最少的基本操作个数。这种情况会给我们一个在给定输入规模下算法执行时间的下限(lower bound)。 - 最差情况分析
在最差情况中,我们认为一个给定的输入会执行最多的基本操作个数。这种情况会给我们一个在给定输入规模下算法执行时间的上限(upper bound)。 - 平均情况分析
在平均情况下,我们会尝试确定一个给定规模下所有可能数据执行操作个数的平均数。这并不像初学者听起来那么容易。为了计算算法的平均运行时间,我们必须知道给定大小的所有可能输入的相对频率。我们要计算每个输入执行的基本操作数的加权平均值。但是我们怎样才能准确地预测输入的分布呢?如果算法遇到输入的不同分布,则我们的分析是无用的。 - 总结
- 最好情况的分析价值有限,因为如果你使用了一个算法,但是这种最好的情况很少出现怎么办?我们渐渐发觉,最差情况下地分析是最有用的,因为不管最后的结果怎么样,一个算法A的执行时间肯定不会比最差情况下还要差。除非特殊指明,我们在接下来的文章当中说明的算法运行时间都是指最差情况下的运行时间。
- 以上述方式计算的算法的运行时间也称为时间复杂度。您经常会听到的另一个术语是算法的空间复杂度。算法的空间复杂度是该算法所需的额外或辅助存储空间量。这是去除实际输入本身以外的存储空间。在本课程的后面,我们将看到评估算法空间复杂度的示例。
分析一个简单的Java程序
假设我们给了Java代码而不是算法。这是我们如何分析给定程序的算法复杂度的方法。让我们计算一下下面给出的程序中的基本操作个数:
class Main {
public static void main(String args[]) {
int x = 0;
x += 1;
System.out.println(x);
}
}
我们来分析一下main()
方法,我们在第3行分配了一个变量,这是一个基本操作。访问变量的值,执行加法操作,然后在第4行进行赋值,因此这是三个基本操作。访问变量的值,然后在第5行进行打印,因此这是两个基本操作。因此,以上程序中总共有6个基本操作。下表中也提供了此分析:
行号 | 基本操作 | 基本操作数 |
3 | 变量分配 | 1 |
4 | 变量访问,添加,变量分配 | 3 |
5 | 变量访问,打印值 | 2 |
所以针对以上的程序,我们的时间复杂度为:
请注意,在此简单示例中没有输入大小的概念,因为该程序没有输入。时间复杂度与输入大小无关的算法被称为恒定时间算法(constant time algorithms)。
在下一文中,我们将分析涉及简单循环的算法的运行时间。