【程序人生】数据结构杂记(一)

说在前面

个人读书笔记

起泡排序

在由一组整数组成的序列A[0,n1]A[0, n - 1]中,满足A[i1]<=A[i]A[i - 1] <= A[i]的相邻元素称作顺序的,否则是逆序的。不难看出,有序序列中每一对相邻元素都是顺序的,亦即,对任意1<=i<n1 <=i < n都有A[i1]<=A[i]A[i - 1] <= A[i];反之,所有相邻元素均顺序的序列,也必然整体有序。

由有序序列的上述特征,我们可以通过不断改善局部的有序性实现整体的有序:从前向后依次检查每一对相邻元素,一旦发现逆序即交换二者的位置。对于长度为nn的序列,共需做n1n - 1次比较和不超过n1n - 1次交换,这一过程称作一趟扫描交换。

在这里插入图片描述
可见,经过这样的一趟扫描,序列未必达到整体有序。果真如此,则可对该序列再做一趟扫描交换。事实上,很有可能需
要反复进行多次扫描交换,直到在序列中不再含有任何逆序的相邻元素。多数的这类交换操作,都会使得越小(大)的元素朝上(下)方移动,直至它们抵达各自应处的位置。

排序过程中,所有元素朝各自最终位置亦步亦趋的移动过程,犹如气泡在水中的上下沉浮,起泡排序(bubblesort)算法也因此得名。

在这里插入图片描述
经过kk趟扫描交换之后,最大的前kk个元素必然就位;经过k趟扫描交换之后,待求解问题的有效规模将缩减至nkn - k

时间复杂度

随着输入规模的扩大,算法的执行时间将如何增长?执行时间的这一变化趋势可表示为输入规模的一个函数,称作该算法的时间复杂度(time complexity)。具体地,特定算法处理规模为n的问题所需的时间可记作T(n)T(n)

OO记号

同样地出于保守的估计,我们首先关注T(n)T(n)的渐进上界。为此可引入所谓“大OO记号”(big-O notation)。具体地,若存在正的常数cc和函数f(n)f(n),使得对任何n>>2n >> 2,都有T(n)<=cf(n)T(n) <= c * f(n)。则可认为在nn足够大之后,f(n)f(n)给出了T(n)T(n)增长速度的一个渐进上界。此时,记之为:
T(n)=O(f(n))T(n)=O(f(n))
由这一定义,可导出大OO记号的以下性质:

  • 对于任一常数c>0c > 0,有O(f(n))=O(cf(n))O(f(n)) = O(c * f(n))
  • 对于任意常数a>b>0a > b > 0,有O(na+nb)=O(na)O(n^a + n^b )=O(n^a )

前一性质意味着,在大OO记号的意义下,函数各项正的常系数可以忽略并等同于1。后一性质则意味着,多项式中的低次项均可忽略,只需保留最高次项。可以看出,大O记号的这些性质的确体现了对函数总体渐进增长趋势的关注和刻画。

以大OO记号形式表示的时间复杂度,实质上是对算法执行时间的一种保守估计,称作最坏实例或最坏情况。

起泡排序的时间复杂度

bubblesort1A()算法由内、外两层循环组成。内循环从前向后,依次比较各对相邻元素,如有必要则将其交换。故在每一轮内循环中,需要扫描和比较n - 1对元素,至多需要交换n - 1对元素。元素的比较和交换,都属于基本操作,故每一轮内循环至多需要执行2(n - 1)次基本操作。另外,外循环至多执行n - 1轮。因此,总共需要执行的基本操作不会超过2(n1)22(n - 1)^2次。若以此来度量该算法的时间复杂度,则有T(n)=O(2(n1)2)T(n)=O(2(n-1)^2)

根据大OO记号的性质,可进一步简化和整理为:
T(n)=O(n2)T(n) = O(n^2)

复杂度分析

常数时间复杂度O(1)O(1)

一般地,仅含一次或常数次基本操作的算法均属此类。此类算法通常不含循环、分支、子程序调用等。

对数时间复杂度O(logn)O(logn)

考查如下问题:对于任意非负整数,统计其二进制展开中,数位1的总数。
在这里插入图片描述
在这里插入图片描述

根据右移运算的性质,每右移一位,n都至少缩减一半(n是输入的十进制整数)。也就是说,至多经过1+log2n1 + log_2n次循环,n必然缩减至0,从而算法终止。实际上从另一角度来看,1+log2n1 + log_2n恰为n二进制展开的总位数,每次循环都将其右移一位,总的循环次数自然也应是1+log2n1 + log_2n

无论是该循环体之前、之内还是之后,均只涉及常数次(逻辑判断、位与运算、加法、右移等)基本操作。因此,countOnes()算法的执行时间主要由循环的次数决定,亦即:
O(1+log2n)=O(log2n)O(1 + log_2n)=O(log_2n)
由大OO记号定义,在用函数logrnlog_rn界定渐进复杂度时,常底数rr的具体取值无所谓,故通常不予专门标出而笼统地记作O(logn)O(logn)

线性时间复杂度O(n)O(n)

对于输入的每一单元,此类算法平均消耗常数时间。就大多数问题而言,在对输入的每一单元均至少访问一次之前,不可能得出解答。以数组求和为例,在尚未得知每一元素的具体数值之前,绝不可能确定其总和。

递归

以数组求和问题为例。易见,若n = 0则总和必为0,这也是最终的平凡情况;否则一般地,总和可理解为前n - 1个整数(A[0,n1))(A[0, n - 1))之和,再加上末元素(A[n1])(A[n - 1])
按这一思路,可基于线性递归模式,设计出另一sum()算法如下图所示。
在这里插入图片描述
由此实例,可以看出保证递归算法有穷性的基本技巧:
首先判断并处理n = 0之类的平凡情况,以免因无限递归而导致系统溢出。这类平凡情况统称“递归基”(base case of recursion)。平凡情况可能有多种,但至少要有一种(比如此处),且迟早必然会出现。

线性递归的模式,往往对应于所谓减而治之(decrease-and-conquer)的算法策略:
递归每深入一层,待求解问题的规模都缩减一个常数,直至最终蜕化为平凡的小(简单)问题。
按照减而治之策略,此处随着递归的深入,调用参数将单调地线性递减。因此无论最初输入的n有多大,递归调用的总次数都是有限的,故算法的执行迟早会终止,即满足有穷性。当抵达递归基时,算法将执行非递归的计算(这里是返回0)。

为保证有穷性,递归算法都必须设置递归基,且确保总能执行到。为此,针对每一类可能出现的平凡情况,都需设置对应的递归基,故同一算法的递归基可能(显式或隐式地)不止一个。

递归算法所消耗的空间量主要取决于递归深度
递归要保证子问题与原问题在接口形式上的一致

结语

如果您有修改意见或问题,欢迎留言或者通过邮箱和我联系。
手打很辛苦,如果我的文章对您有帮助,转载请注明出处。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章