该博客主要记录我在数据结构学习过程中,遇到的重点和难点,该数据结构博客虽然基于严蔚敏的第三版《数据结构》,但在学习过程中,对于一些内容,我做了较为深入地学习与拓展。
绪论这一章主要介绍与数据结构相关的概念和算法分析。本博客中,就个别概念和算法的时间分析做了较为详细和深入的探讨。
就相关概念这一部分,其实没有什么好讲的。这个“没有什么好讲的”,不是指内容本身“没什么好讲的”,而是指如果想要深刻理解这些内容,除了记住书上的概念,还要实际写代码实践操作。因此,这部分内容主要是和后面结合起来,在后面的实践中加深认识。如果我仅仅是把相关概念复述一遍,我认为这没多大意义。但是,其中还是有一些内容值得现在就反复玩味。
(一)抽象
无论是从我们使用的编程语言(C或C++)的角度,还是从实际开发的角度,我们都必须实现抽象这一要求,这里的抽象是指:我们使用的数据结构的直接表示是抽象的。
从编程语言的角度看,我们使用的C或C++属于高级程序语言,高级程序语言不能够非常直接赤裸地去刻画数据结构,比如我通过C语言语句直接改变某个寄存器的值,一般情况下,这显然是不行的,因为一条C语言语句可被编译成多条汇编语言语句,而汇编语言语句才可以“直接”操纵内存和寄存器(这里的“直接“指的是汇编语言中拥有MOV、LDR、STR等语句)。因此,我们只能通过高级程序语言提供的已有的数据类型来制作数据结构,而这些已有的数据类型,本身就是高级抽象的,比如:我们用数组来描述顺序存储结构,我们用”指针“来描述链式存储结构。我们制作数据结构时,无论是从制作的直接原材料上看,还是从制作平台上看,都不是底层的,而是在一个已经抽象的层次上制作的。比如我们使用C语言开发,我们就可以把C语言看成是一个执行C指令和C数据类型的C虚拟处理器,我们制作的数据结构,自然也叫做虚拟存储结构。
从实际开发上看,抽象提高了效率,由于本点需要实际开发经验,故此处不再赘述。
(二)抽象数据类型
如上文所说,提供的数据类型是我们制作数据结构的原材料,故数据类型非常重要。
实际上,数据类型的重要性还体现在,对硬件来说,数据类型是解释计算机内存中信息含义的一种手段,对用户而言,数据类型实现了封装。
而这里要强调的是抽象数据类型(Abstract Data Type,ADT)。抽象数据类型是指一个数学模型以及定义在该模型上的一组操作。抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部的实现无关。ADT除了编程语言所提供的数据类型(如 int),还包括用户自定义的数据类型。
抽象数据类型这一概念之所以重要,是因为它与算法的逻辑理论密切相关,而不关心算法的物理实现,当我们分析一个问题时,一般情况下,也总是从简单到复杂,先解决逻辑,再解决物理实现。
下面,我将较为深入地探讨时间复杂度
(三)时间复杂度
算法的分析中,时间分析是极为重要的。算法的效率,就是指算法执行的时间,算法执行时间越短,效率就越高,否则越低。
1.如何量度算法的效率
我们可以将由该算法编制的程序经过运行后,通过计算机计时。显然,这种方法受实际软件、硬件环境影响,环境容易掩盖算法本身的优劣。
因此我们从理论上入手。
由算法编制的程序在计算机上运行时间受编程语言、编译程序、硬件环境影响,所以同一个算法对应的程序,绝对时间也可能不一样,所以采用绝对时间单位衡量算法效率不合适。
现在,我们从理论入手,采用估算方法,不使用绝对时间单位。
我们可以知道,问题的规模是影响算法的运行时间的因素,显然,它也是主要因素,我们完全可以认为,在一定误差下,算法的效率是是问题规模的函数。
一个算法由程序控制结构和原操作(固有数据类型的操作)构成,所以算法取决于二者的综合效果。进而,我们可以从算法中选择一种对于所研究问题来说是基本操作的原操作,以该原操作重复执行的次数作为算法的时间量度。
一般情况下,原操作重复执行的次数是问题规模n的函数,记作f(n)。
我们研究算法效率,就是研究算法的f(n),f(n)越大,效率越低。
2.关于f(n)的思考
for(i=0;i<n;i++){
statement;
}
类似上面简单的程序块,statement的f(n)很好计算,f(n)=n+1。
但有些程序的精确f(n)不是非常简单的就可以求出来,比如下面这个程序(求斐波那契数列指定项值)
Fib(int n){
if(n==1) return 1;
else if(n==2) return 1;
else return Fib(n-1)+Fib(n-2);
}
将return视为原操作,则有:
该算法的f(n)满足递推关系:
f(1)=f(2)=1
f(n)=f(n-1)+f(n-2)
它的f(n)并不是可以简单地求出的。
当然,我们也可以求出。我们注意到f(n)=f(n-1)+f(n-2)是二阶齐次常系数差分方程,可求出通解:
结合初始条件。可解得:
但问题是,我们有必要精确地求出f(n)吗?
我之前已经说了是估算,并且效率主要依靠问题规模n,倘若n非常大,那么f(n)中的常数项和低阶项就算不考虑,又有什么关系呢?
所以,我们希望找到一种方法,这种方法可以用来较好地方便地估算算法效率。
3.渐近方法
如果是学过数据结构的,那么肯定知道O记号用来刻画时间复杂度。
这里我想强调的是,不是说O记号就是时间复杂度,它只是一种渐近估算技术,它是一个数学概念,是被计算机从数学中借用的,并且除了O记号,还有Θ记号、Ω记号、o记号和ω记号。
Knuth发现O记号最早见于P.Bachmann于1892年编写的一本数论教材。
E.Landau于1909年发明o记号用于讨论素数的分布。
它们都是用来通过作用于f(n)来估算算法效率的。
下面我将依次介绍,这里说明一点,f(n)肯定是非负的。
(1)渐近紧确界记号——Θ记号
示意图:
(2)渐近上界记号——O记号
示意图:
(3)渐近下界记号——Ω记号
示意图:
(4)非渐近紧确上界记号——o记号
(5)非渐近紧确下界记号——ω记号
关于渐近方法,它们的之间的关系和性质有许多,这对于计算算法效率有巨大帮助。
但限于时间关系,我无法在此处继续展开,我仅着重说明一点。
我们常常使用O记号,其实,更精确的是Θ记号。O记号的结果算出来偏大,注意,我这里说的是偏大。某些地方会说,O记号算出来的是算法的最坏情况,这种说法实际上是错误的,因为O记号算出来的可以是非常渐进的,也可以是很大的,正如示意图所示,而o记号算出来的一定是偏大的,因为它是非渐近的。
最后,我们来计算下面程序块的大O时间复杂度
Fib(int n){
if(n==1) return 1;
else if(n==2) return 1;
else return Fib(n-1)+Fib(n-2);
}
显然地,该递归可以用递归树表示,进而得出该算法时间复杂度为
通过比较与,会发现,当n趋近于无穷大时,二者的增长率会接近。
增长率的接近或者说相等,就是这里渐近估算技术的核心思想。
参考资料:
《Introduction to Algorithms》第三版
《数据结构》严蔚敏 第三版