我们依赖秩序,而反感混乱
为什么要排序
有许多直接的应用需要我们进行排序,例如按照人名首字母的顺序对班上的人进行排序;字典的索引需要排序等。此外许多问题通过排序可以得到化简,例如寻找中位数的问题、文件压缩以及视频渲染等。
插入排序
原理及算法
插入排序的算法如下:
1 | For i = 1,2,3,...,n |
算法的可视化过程如下所示:
复杂度分析
时间复杂度
这个算法可以分为:
- 移动key:一共移动了n步,复杂度为Θ(n)
- 交换相邻元素:每一步最大可能比较并交换n次,复杂度为Θ(n)
由于底层操作的缘由,比较操作相对于交换来说,更加耗时,所以一个改进就是降低比较的次数,我们可以对已经比较好的部分A[0:i−1]进行二分查找,从而将比较操作的时间复杂度降低为Θ(logi),不过由于需要整体移动,所以总时间复杂度还是没有变化,只是比较次数变少了。改进后的时间复杂度为:
- Θ(nlogn)次比较
- Θ(n2)次交换
空间复杂度
插入排序的空间复杂度为Θ(1),不需要额外的空间
归并排序
基本原理
归并排序的思想是先将长度为n的大数组A转换为长度为n/2的小数组L和R,然后分别对其进行排序,之后再将数组进行合并,最后得到排序后的数组A′
所谓归并,就是将两个排序好的数组作为输入。下面结合具体例子对归并过程进行讲解
1 | 给定两个已经排序的数组l'和R’,两个指针分别指向L'和R'的最小元素位置,每次选择两个数组中最小的数,然后移动指针 |
归并的复杂度为Θ(n)。
复杂度分析
时间复杂度
根据归并排序的原理,我们可以写出归并排序的时间复杂度为:
T(n)=C1+2T(n/2)+C⋅n其中C1为分割数组的时间复杂度,C⋅n为归并数组的复杂度,C和C1是一个常数,T(n/2)为递归过程时间复杂度。下面我们构造一个二叉树对这个过程进行分析:
这个递归树有n个树叶,每一层和都为C⋅n,叶子节点值都为C,这个满二叉树层数为1+logn,故
T(n)=(1+logn)×C⋅n=Θ(nlgn)空间复杂度
空间复杂度为Θ(n),因为需要有拆分数组的过程,为了改进这个算法,人们提出了原地归并排序算法
两种排序方法的比较
归并排序相对于插入排序更快,但是需要额外的空间以及拷贝动作。在python中,归并排序时间为(2.2nlgn)μs,而插入排序时间为0.2n2
复杂度计算
本节一个重要的点就是复杂度的计算,仿照归并排序的复杂度分析过程,我们这里给出一道例题:
求T(n)=2T(n/2)+C⋅n的时间复杂度
构造二叉树如下:
树深度为lgn+1,最后一层叶子节点数为n,值为C,故总时间复杂度为:
T(n)=Cn2+Cn2/2+Cn2/4+⋯+Cn≤2Cn2作业
分形渲染
题目
我们首先来直观了解一下分形问题:
上面这个分型就是经典的Koch雪花,一开始有一个初始三角形,然后三角形的边上会长出新的三角形,由于CPU和GPU精度限制,分形的过程是有限的,令n为分形的深度(或者细节,Level of detail, LoD),我们规定:当n=0时,为初始三角形。分形的算法可以由如下伪代码表示:
SNOWFLAKE(n)
e1,e2,e3=edges of an equilateral triangle with side length 1
SNOWFLAKE−EDGE(e1,n)
SNOWFLAKE−EDGE(e2,n)
SNOWFLAKE−EDGE(e3,n)
SNOWFLAKE−EDGE(e,n)
if n==0
e is an edge on the snowflake
else
e1,e2,e3=split edge in 3 equal parts
SNOWFLAKE−EDGE(e1,n−1)
f2,g2=edges of an equilateral triangle whose 3rd edge is e2
pointing outside the snowflake Δ(f2,g2,e2) is a triangle on the snowflake’s surface
SNOWFLAKE−EDGE(f2,n−1)
SNOWFLAKE−EDGE(g2,n−1)
SNOWFLAKE−EDGE(e3,n−1)
这里我们提供了四种方法实现SnowflakeEdge,我们需要判定这些方法的时间复杂度。在SNOWFLAKE(n)函数中,我们实际上是构建了三棵树,构成了一个森林,但我们出于统一的考虑,将这个森林视作一个树。
第一组
1) 根据上面的SnowflakeEdge算法,渲染一个深度为n的雪花,其迭代树深度为多少?
上面的算法时间复杂度可以写为:
T(n)=4T(n−1)+Θ(1)T(0)=Θ(1)绘制其递归树,得到一个深度为n的四叉树,层数为n+1。
2) 求第i层(0≤i≤n)节点数f(i)
我们先考虑一棵树,再推广到整个森林:第i层有4i个节点,我们采用数学归纳法进行证明:当i=0时,有一个节点,显然成立;假设算法在第j层(0≤j<i)均成立,那么第j层有4j个节点,当j=i−1时,该层有4i−1个节点,现证明当j=i时算法依然成立,根据递归过程可知,该递归树每个节点度为4,那么下一层节点数为当前层的4倍,则第i层节点数f(i)=4f(i−1)=4×4i−1=4i。
由于森林中有三棵相同的树,所以最终第i层节点数为3⋅4i。
3) 在递归树的第i层,渲染一个节点的时间复杂度是多少?其中0≤i<n。
根据算法的时间复杂度:
T(n)=4T(n−1)+Θ(1)=4(4T(n−2)+Θ(1))+Θ(1)=⋯=4i+1T(n−i−1)+4iΘ(1)对于一颗递归树,渲染第i层的时间复杂度为Θ(4i)。而第i层节点数为4i,故渲染第i层一个节点的时间复杂度为Θ(1)。
4) 求第i层的时间复杂度
根据2)和3)可知时间复杂度为Θ(4i)。
5) 求总时间复杂度
因为递归树第i层时间复杂度为Θ(4i),故总时间复杂度T(n)=Θ(40)+Θ(41)+⋯+Θ(4n),根据极限定理可知,总时间复杂度T(n)满足Θ(4n)≤T(n)≤Θ(4n+1)。所以时间复杂度为T(n)=Θ(4n)。
第二组
为了提高分形效率,我们采用GPU对程序进行加速,将雪花图案视为一个闭合的直线路径,通过CPU计算路径的端点,最后将深度为n的路径点交给GPU进行渲染。当n=0时,需要绘制三条边,当LoD增加后,每增加一层,那么原先的每一条边会被更新为4条边。请回答如下问题:
1) 根据硬件加速的SnowflakeEdge算法,渲染一个深度为n的雪花,其迭代树深度为多少?
硬件加速的时间复杂度为:
T(n)=4T(n−1)+Θ(1)T(0)=Θ(1)同样也是一个四叉树,深度为n。第i层节点数为3⋅4i,这里一个节点代表构造一条边,
2) 渲染第i层一个节点的时间复杂度为?
当0≤i<n时,只构造边但是不渲染,故时间复杂度为0,而当i=n时,进行一次渲染,渲染一条边的时间复杂度为Θ(1).
3) 渲染第i层的所有节点时间复杂度为?
由于中间层不进行渲染,因此时间复杂度为0,最后一层渲染时时间复杂度为Θ(4n),总的时间复杂度为Θ(4n)。
所以硬件加速省略了中间的渲染过程,最后待分形图案的点完成后,统一进行渲染,节约了渲染时间,但是构造分形的复杂度没有降低。
第三组
继续改进算法,我们依旧将雪花图案保存为闭合直线路径,但是我们不使用硬件加速,而是根据起点和端点,对线条进行绘制,绘制时间是线性的,即如果线长为1,那么绘制时间为1,如果线长为1/3,相应地绘制时间也会变为原来的三分之一。渲染依旧是在最后一步进行,回答下面的问题:
1) 求Lod为n时树的深度以及第i层的节点数
这个树是一个四叉树,高度为n,第i层节点数为3×4i。
2) 渲染第i层的一个节点时间为?
第i层不渲染,所以时间为0
3) 渲染第n层的一个节点和所有节点时间为?
第n层节点数为3×4n,而渲染一个节点(即一条边)的过程和边长有关,第n层边长为(1/3)n,所以渲染一个节点的时间为(1/3)n,那么总的时间复杂度为(43)n。
4) 总渲染时间为?
虽然每一层不用渲染了,但是依然需要做保存端点的操作,因此每一层每个节点时间复杂度为Θ(1),最后一层渲染的总时间复杂度为(43)n。所以总时间为:
T(n)=n−1∑i=04i+(43)n占主要部分的是前半部分,所以时间复杂度为Θ(4n),不过如果只看渲染时间,那么就是(43)n。
第四组
不使用硬件加速渲染3d的雪花图,CPU保存一系列的三角形,渲染时间和三角形面积成正比。如果边长为1,那么面积为√3/4;如果边长为1/2,那么面积为√3/16。是指数关系。首先,不变的是树的高度和每层节点数,由于最后渲染,所以第i层(0≤i<n)的渲染时间为0,最后一层节点数为3×4n,第n层边长为(1/3)n,出于简单考虑,令面积为(1/3)2n,所以总时间为(49)n,即Θ(1)。
更正
这里有一个理解错误,渲染形状和渲染线段是不一样的,渲染形状不能只看最后一层的顶点和线段,要将每一层三角形的形状都考虑进来,因此每一层都要考虑,第i层边长为(1/3)i+1,面积为(1/9)i+1,所以每一层的渲染时间为3×4i×(1/9)i+1,总时间为:
T(n)=n∑i=03⋅19⋅(49)i=Θ(1)