INT202 Complexity of Algroithms 算法的复杂度 Pt.3 Sorting Algorithm & Divide and Conquer 排序算法和分治法

1.排序算法

排序是计算机科学中的一个基本算法问题。它涉及到将一组元素按照特定的顺序(如升序或降序)排列。
许多算法在执行过程中会执行排序操作(作为一个子程序),例如我们前面学的二分搜索,就是需要数据按照升序排列。因此高效的排序方法对于实现良好的算法性能至关重要。
我们并不总是需要一个完全排序好的列表,因此,根据手头的具体任务,某些排序方法可能更合适。
排序算法可能直接适用于执行额外任务,并以这种方式直接提供解决方案,比如查找、选择等。

1.1 优先队列(Priority Queue)

优先队列是一种容器,其中的每个元素都有一个与之关联的键(key)。这些键决定了在从优先队列中选择元素进行移除时的优先级。
优先队列提供了以下基本操作:

  1. insertItem(k, e):
    将具有键 k 的元素 e 插入到优先队列中。这个操作通常也被称为入队(enqueue)。
  2. removeMin():
    从优先队列中移除并返回具有最小键的元素。这个操作通常也称为出队(dequeue)。
  3. minElement():
    返回优先队列中具有最小键的元素,但不从队列中移除它。这允许在不改变队列的情况下查看最小元素。
  4. minKey():
    返回优先队列中具有最小键的元素的键值,但不返回元素本身或移除它。

因此我们可以通过优先队列完成排序。
方法如下:

  1. 创建一个空的优先队列 P P P
  2. 将集合 C C C中的所有元素通过 n n n次 insertItem 操作插入到优先队列 P P P中。这里的 n n n是集合 C 中元素的数量。在这个过程中,每个元素根据其优先级(通常是元素的值或其他相关属性)被插入到优先队列中。在标准的优先队列实现中,元素通常按照其优先级从低到高进行排序,即最小的元素具有最高的优先级。
  3. 从优先队列 P P P中按照非递减顺序(从最小到最大)提取元素,使用 n n n次 removeMin 操作。removeMin 操作会移除并返回优先队列中优先级最高的元素(即最小元素),然后重新调整优先队列以保持其属性。

通过这样的方式我们可以完成排序。

1.2 堆(Heap)

堆是一种特殊的优先队列实现,它允许插入和删除操作在对数时间内完成。
在堆中,元素及其键(key)存储在一个几乎完全的二叉树(binary tree)中,除了可能的最后一层外,二叉树的每一层都尽可能地填满子节点。
堆可以是最小堆(Min-Heap)或最大堆(Max-Heap):
在最小堆中,每个父节点的键都小于或等于其子节点的键,这意味着堆顶(根节点)是最小元素。
在最大堆中,每个父节点的键都大于或等于其子节点的键,这意味着堆顶是最大元素。

1.2.1 满二叉树和完全二叉树

二叉树又有很多类型:

  1. 满二叉树(Full Binary Tree)
    定义:如果一个二叉树 T 中的每个节点要么是叶子节点,要么恰好有两个子节点,那么这个二叉树就是满二叉树。
    特点:在满二叉树中,所有层级都被完全填满,除了可能的最后一层。这意味着除了最后一层外,每一层的节点数都达到了最大可能值。
  2. 完全二叉树(Complete Binary Tree)
    定义:高度为 h h h的完全二叉树是一个二叉树,它在深度 d ( 0 ≤ d ≤ h ) d(0≤d≤h) d0dh)恰好包含 2 d 2^d 2d个节点,并且深度 h h h的节点尽可能靠左排列。
    特点:完全二叉树是满二叉树的一种特殊情况,其中每个节点都尽可能地靠左排列,以确保树的平衡。
  3. 几乎完全二叉树(Nearly Complete Binary Tree)
    定义:高度为 h h h的几乎完全二叉树是一个二叉树,满足以下条件:
    在深度 d ( 1 ≤ d ≤ h − 1 ) d(1≤d≤h−1) d1dh1) 2 d 2^d 2d个节点。
    并且深度 h h h的节点尽可能靠左。
    特点:几乎完全二叉树允许最后一层的节点不完全填满,但要求这些节点尽可能地靠左排列。
    下图展示了一些例子。
    在这里插入图片描述
    第一个不是几乎完全二叉树是因为除了最后一层外,其他层级也没有完全填满节点。
    第二个不是几乎完全二叉树是因为最后一层的节点没有尽可能地向左排列。
    第三个才是几乎完全二叉树。

让我们现在回到堆的数据结构中,它的内部节点存储键(keys),并且满足特定的性质。
性质如下:
对于除了根节点之外的每个内部节点 v v v,节点 v v v的键值 k e y ( v ) key(v) key(v)大于或等于其父节点 p a r e n t ( v ) parent(v) parent(v)的键值 k e y ( p a r e n t ( v ) ) key(parent(v)) key(parent(v))
在最大堆(Max Heap)中,这个性质确保每个父节点的键值都大于或等于其子节点的键值。
在最小堆(Min Heap)中,这个性质确保每个父节点的键值都小于或等于其子节点的键值。
下图展示了一个最小堆的例子。
在这里插入图片描述

1.2.2 用数组表示

我们可以使用数组高效地表示堆,因为这种表示方法不需要显式的链接(如指针)来表示树结构。
具体方法如下:
1.数组的索引从1开始,而不是通常编程中使用的0。
2.数组中存储节点时,按照层级顺序(level order)进行,即先存储所有根节点,然后是第二层的所有节点,依此类推。
3.由于堆是一个几乎完全二叉树,所以我们不需要显式链接(如指针或引用)来表示树中节点之间的关系。
下图展示了一个用数组呈现的堆。
在这里插入图片描述
因此对于数组中任意位置 i i i的节点:
1.左子节点:如果存在,其位置是 2 × i 2×i 2×i
2.右子节点:如果存在,其位置是 2 × i + 1 2×i+1 2×i+1
3.父节点:如果存在,其位置是 ⌊ ( i / 2 ) ⌋ ⌊(i/2)⌋ ⌊(i/2)⌋

1.2.3 用堆实现优先队列

我们为了实现优先队列,除了刚刚我们说的堆以外我们还需要用两个工具。
1.last:这是一个引用,指向堆的数组表示中 T T T的最后一个使用的节点。
2.comp:这是一个比较函数,用于定义键上的全序关系,并用于维护 T T T根的最小(或最大)元素。
通过这两个工具还有堆我们便实现了优先队列。

1.2.4 时间复杂度

其实我们可以很轻松地推理出堆的高度是 O ( l o g n ) O(logn) O(logn)
定理:存储 n n n个键的堆的高度是 O ( l o g n ) O(logn) O(logn)
证明如下:
h h h是存储 n n n个键的堆的高度。
由于在深度 d = 0 , … , h − 2 d=0,…,h−2 d=0,,h2至少有 2 d 2^d 2d个键,并且在深度 h − 1 h−1 h1至少有一个键,我们可以得到以下不等式:
n ≥ 1 + 2 + 4 + ⋯ + 2 h − 2 + 1 n≥1+2+4+⋯+2^{h−2}+1 n1+2+4++2h2+1
因此 n ≥ 2 h − 1 n ≥ 2^{h−1} n2h1
h ≤ l o g ( n + 1 ) h≤log(n+1) hlog(n+1)
在这里插入图片描述
因此在堆中进行搜索、插入和删除操作的时间复杂度都是对数级别的,即 O ( l o g n ) O(logn) O(logn)

1.2.5 相关操作

我们现在介绍用堆实现的优先队列的相关操作,这里会出现问题。
例如优先队列使用insertItem在优先队列的最后插入一个新的元素,但是这样就破坏了优先队列或者说是堆的排序,因此我们现在通过上浮(up-heap bubbling)来恢复堆的排序。

1.2.5.1 插入(insertion)与上浮(up-heap bubbling)

插入算法包含三个步骤:
1.找到插入节点:
找到插入节点的位置,这个节点将是新的最后一个节点。
2.存储并扩展为内部节点:
将新键存储在找到的位置,并将其扩展为内部节点。
3.恢复堆序性质:
通过比较新节点的键与其父节点的键,可能需要进行上浮(up-heap bubbling)操作来恢复堆的顺序性质。
下图展示了一个插入的例子。
在这里插入图片描述
上浮算法发生在插入的键破坏了堆序性质后。
通过沿着从插入节点向上的路径交换插入的新键 k k k来恢复堆序性质。
上浮过程在键 k k k到达根节点或到达一个父节点的键值小于或等于 k k k时终止。
由于堆的高度是 O ( l o g n ) O(logn) O(logn),其中 n n n是堆中元素的数量,上浮操作的时间复杂度也是 O ( l o g n ) O(logn) O(logn)
对于上面插入的例子,进行上浮的过程如下图所示。
在这里插入图片描述

1.2.5.2 删除(removeMin)与下浮(down-heap bubbling)

removeMin()是从优先队列中移除并返回具有最小键的元素,因此这里一定需要我们对堆进行重构,这里涉及的操作是下浮(down-heap bubbling)。
由于我们知道最后一个节点的索引,我们现在使用数组中最后一个节点的键替换根键,这样就可以快速地将最小元素从堆中移除,接下来就是进行下浮从而让堆能够维持堆序性质。
删除算法的步骤:
1.替换根键:
用数组中最后一个节点的键 w w w替换根键。
2.压缩节点:
将最后一个节点(现在包含最小元素)与其子节点压缩成一个叶子节点。
3.恢复堆序性质:
通过下浮(bubbling down)操作恢复堆的堆序性质。
下图展示了一个删除的例子。
在这里插入图片描述
下浮下浮(Down-Heap Bubbling)发生在删除算法之后。
下浮算法通过沿着从根节点向下的路径交换键 k k k来恢复堆序性质。这个过程涉及将新根节点与其子节点进行比较,并在必要时进行交换,以确保每个父节点的键值都满足堆序性质。
下浮过程在键 k k k到达叶子节点或到达一个节点时终止,该节点的子节点的键都大于或等于 k k k
同样地,由于堆的高度是 O ( l o g n ) O(logn) O(logn),其中 n n n是堆中元素的数量,因此下浮操作的时间复杂度也是 O ( l o g n ) O(logn) O(logn)
下图展示了下浮让前面例子恢复堆的堆序性质的过程。
在这里插入图片描述

1.2.6 堆排序

我们堆用堆实现的优先队列在排序中的应用进行总结。
使用堆实现的优先队列所需的空间是 O ( n ) O(n) O(n),其中 n n n是优先队列中元素的数量。
insertItem(插入元素)和 removeMin(删除最小元素)操作的时间复杂度是 O ( l o g n ) O(logn) O(logn)
size(获取元素数量)、isEmpty(检查是否为空)、minKey(获取最小键)和 minElement(获取最小元素)等方法的时间复杂度是 O(1)。
因此使用堆进行排序需要我们构建一个堆然后反复移除最大(或最小)元素来实现排序。由于每次移除和重构时间是 O ( l o g n ) O(logn) O(logn),所以堆排序的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)

这便是堆排序,利用堆的数据结构来实现排序,先构建一个堆然后再不断重复移除堆里的元素从而实现排序。

2. 分治法(Divide and Conquer)

分治法(Divide and Conquer)是一种解决算法问题的通用方法。
步骤如下:

  1. 分(Divide):
    如果输入数据的大小很小,可以直接解决这个问题;否则,将输入数据划分为两个或多个不相交的子集。
  2. 治(Recur):
    递归地解决与这些子集相关的子问题。
  3. 合(Conquer):
    将子问题的解决方案合并,形成原始问题的解决方案。

2.1 归并排序(MergeSort)

在排序问题中分治法的解决方案中最著名的便是归并排序(MergeSort)
归并排序算法对输入序列 S S S包含 n n n个元素,分为三个步骤:

  1. 分(Divide):将序列 S S S分成两个大约包含 n / 2 n/2 n/2个元素的子序列 S 1 S_1 S1 S 2 S_2 S2
  2. 治(Recur):递归地对 S 1 S_1 S1 S 2 S_2 S2进行排序。
  3. 合并(Conquer):将 S 1 S_1 S1 S 2 S_2 S2合并成一个唯一的有序序列。

其伪代码如下:

Algorithm mergeSort(S, C)
    Input sequence S with n elements, comparator C
    Output sequence S sorted according to C

    If S.size() > 1
        (S1, S2) ← partition(S, n/2)
        mergeSort(S1, C)
        mergeSort(S2, C)
        S ← merge(S1, S2)

图示如下:
在这里插入图片描述
下面通过一个例子展示这个过程。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这里对 8 个数字进行排序,首先对这些数字不断地进行分割从而变成 8 组序列,然后开始不断合并。

在这里插入图片描述
将左右两个序列的第一个元素进行对比,小的就先放进合并的序列中,当一个序列清空就将另一个序列的全部元素添加进合并的序列中。
在这里插入图片描述
在这里插入图片描述
不断重复这个过程我们就有了最后的结果。

2.1.1 归并排序的时间复杂度

归并排序树的高度是 O ( l o g n ) O(logn) O(logn),这里的 n n n是输入序列中的元素数量。这是因为归并排序在每次递归调用中都将序列分成两半,类似于一个二叉树的层级结构。
在深度 d d d的节点处,我们完成的总工作量是 O ( n ) O(n) O(n)。这是因为在深度 d d d处,我们处理的序列长度是 n / 2 d n/2^d n/2d,而我们需要合并两个这样的序列,每个序列大约需要 n / 2 d n/2^d n/2d的时间。
我们从根节点到叶子节点的每个深度都进行了 O ( n ) O(n) O(n)的工作,而深度是 O ( l o g n ) O(logn) O(logn),所以归并排序的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)
在这里插入图片描述
在这里插入图片描述

2.2 快速排序(Quick Sort)

快速排序的步骤如下:

  1. 分(Divide):
    选择一个随机元素作为基准(pivot)。
    将序列划分为三个部分:
    L L L:所有小于基准的元素。
    E E E:所有等于基准的元素。
    G G G:所有大于基准的元素。
  2. 治(Recur):
    递归地对小于基准的子序列 L L L和大于基准的子序列 G G G进行排序。
  3. 合并(Conquer):
    将排序好的小于基准的子序列 L L L和大于基准的子序列 G G G与基准元素合并,形成完整的有序序列。

图示如下:
在这里插入图片描述

2.2.1 快速排序的时间复杂度

平均情况:快速排序的平均时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn),其中 n n n是序列中的元素数量。这是因为每次划分大约将问题规模减半,递归的深度是 O ( l o g n ) O(logn) O(logn)
最坏情况:快速排序的最坏情况时间复杂度是 O ( n 2 ) O(n^2) O(n2),这通常发生在每次划分都非常不平衡时,例如选择的基准总是当前序列中的最小或最大元素,这就会导致运行 L L L G G G的大小为 n − 1 n-1 n1 0 0 0,因此总运行次数就是 n + ( n − 1 ) + . . . + 2 + 1 n+(n-1)+...+2+1 n+(n1)+...+2+1,所以时间复杂度是 O ( n 2 ) O(n^2) O(n2)

2.3 分治法(Divide-and-Conquer)的时间复杂度分析

我们现在回到分治法本身,如果我们遇到一个问题,我们使用分治法解决,那它的时间复杂度我们该怎么分析呢?
我们通常会使用递归关系去分析,因为分治法的核心思想也是将问题通过递归解决。

我们希望用一个方程来描述 T ( n ) T(n) T(n),这个方程将 T ( n ) T(n) T(n)与比 n n n小的问题规模的函数 T T T的值关联起来。
例子如下。
在这里插入图片描述

2.3.1 替代法(Substitution Method)

我们可以使用替代法(Substitution Method)来解决递归关系。
替代法通过迭代地将递归关系应用于自身,观察是否能找到一个模式。
对于前面的例子,使用代替法的过程如下。
T ( n ) = 2 T ( n / 2 ) + b n = 2 ( 2 T ( n / 2 2 ) + b ( n / 2 ) ) + b n = 2 2 T ( n / 2 2 ) + 2 b n = 2 3 T ( n / 2 3 ) + 3 b n = 2 4 T ( n / 2 2 ) + 4 b n = . . . = 2 i T ( n / 2 i ) + i b n T(n) = 2T(n/2)+bn= 2(2T(n/2^2)+b(n/2))+bn= 2^2T(n/2^2)+2bn = 2^3T(n/2^3)+3bn=2^4T(n/2^2)+4bn = ... = 2^iT(n/2^i)+ibn T(n)=2T(n/2)+bn=2(2T(n/22)+b(n/2))+bn=22T(n/22)+2bn=23T(n/23)+3bn=24T(n/22)+4bn=...=2iT(n/2i)+ibn
继续展开,直到达到基本情况 T ( n ) = b T(n)=b T(n)=b,这发生在 2 i = n 2^i =n 2i=n时,即 i = l o g n i=log n i=logn
所以 T ( n ) = b n + b n l o g n T(n)=bn+bnlogn T(n)=bn+bnlogn
因此 T ( n ) T(n) T(n)的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

2.3.2 主定理法(The Master Method)

它提供了一种标准化的公式,我们可以直接套用这个公式来求解递归关系的渐进问题。
下图是其适用于的递归关系的形式。
在这里插入图片描述
其中 d ≥ 1 , a > 0 , c > 0 , b > 1 d≥1,a>0,c>0,b>1 d1a>0c>0b>1
假设: a ≥ 1 a≥1 a1 b > 1 b>1 b>1是常数, f ( n ) f(n) f(n)是一个函数, T ( n ) T(n) T(n)通过递归关系定义 T ( n ) = a T ( n / b ) + f ( n ) T(n)=aT(n/b)+f(n) T(n)=aT(n/b)+f(n)在非负整数上.
其中 n / b n/b n/b可以解释为 ⌊ n / b ⌋ ⌊n/b⌋ n/b ⌈ n / b ⌉ ⌈n/b⌉ n/b
在这种假设下,主定理法告诉我们 T ( n ) T(n) T(n)会有三种渐进界。

  1. 如果 f ( n ) = O ( n l o g b ​ a − ϵ ) f(n)=O(n^{log_b​^{a−ϵ}}) f(n)=O(nlogbaϵ)对于某个常数 ϵ > 0 ϵ>0 ϵ>0,则 T ( n ) = Θ ( n l o g b ​ a ) T(n)=Θ(n^{log_b^​ a}) T(n)=Θ(nlogba)
  2. 如果 f ( n ) = Θ ( n l o g b ​ a ) f(n)=Θ(n^{log_b​^a}) f(n)=Θ(nlogba),则 T ( n ) = Θ ( n l o g b ​ a l g n ) T(n)=Θ(n^{log_b^​a}lgn) T(n)=Θ(nlogbalgn)
  3. 如果 f ( n ) = Ω ( n l o g b a + ϵ ) f(n)=Ω(n^{log_b^{a+ϵ}}) f(n)=Ω(nlogba+ϵ)对于某个常数 ϵ > 0 ϵ>0 ϵ>0,并且如果 a f ( n / b ) ≤ c f ( n ) af(n/b)≤cf(n) af(n/b)cf(n)对于某个常数 c < 1 c<1 c<1和所有足够大的 n n n,则 T ( n ) = Θ ( f ( n ) ) T(n)=Θ(f(n)) T(n)=Θ(f(n))

主定理根据 f ( n ) f(n) f(n)相对于 n l o g b ​ a n^{log_b^​ a} nlogba的增长速度,将问题分为三种情况,这三种情况对应的就是前面的渐进界:

  1. f ( n ) f(n) f(n)是多项式小于 n l o g b ​ a n^{log_b^​ a} nlogba
  2. f ( n ) f(n) f(n)渐进地接近 n l o g b ​ a n^{log_b^​ a} nlogba
  3. f ( n ) f(n) f(n)是多项式大于 n l o g b ​ a n^{log_b^​ a} nlogba
    多项式小于指的是 f ( n ) = O ( g ( n ) / n ϵ ) f(n)=O(g(n)/n^ϵ) f(n)=O(g(n)/nϵ),其中 ϵ > 0 ϵ>0 ϵ>0
    多项式大于指的是 f ( n ) = Ω ( g ( n ) n ϵ ) f(n)=Ω(g(n)n^ϵ) f(n)=Ω(g(n)nϵ),其中 ϵ > 0 ϵ>0 ϵ>0

也就是说我们通过比较 f ( n ) f(n) f(n) n l o g b ​ a n^{log_b^​ a} nlogba,得知谁占这里上升速度的主导关系,从而确定 T ( n ) T(n) T(n)的渐进界。
下面我们看几个例子。
例1: T ( n ) = 4 T ( n / 2 ) + n T(n)=4T(n/2)+n T(n)=4T(n/2)+n
解:这里 a = 4 , b = 2 , f ( n ) = n a=4,b=2,f(n)=n a=4,b=2,f(n)=n,计算得出 n l o g b a = n l o g 2 4 = n 2 n^{log_b^a}=n^{log_2^4}=n^2 nlogba=nlog24=n2
这里 f ( n ) = n f(n)=n f(n)=n,我们需要比较 n n n n 2 n^2 n2
根据主定理中的定义, f ( n ) f(n) f(n)是多项式小于 n l o g b a n^{log_b^a} nlogba,因为 f ( n ) = O ( n 2 − ϵ ) f(n)=O(n^{2−ϵ}) f(n)=O(n2ϵ)对于 ϵ = 1 ϵ=1 ϵ=1
因此根据主定理的情况1,我们得到 T ( n ) = Θ ( n l o g b a ) T(n)=Θ(n^{log_b^a}) T(n)=Θ(nlogba),所以 T ( n ) = Θ ( n 2 ) T(n)=Θ(n^2) T(n)=Θ(n2)

例2: T ( n ) = T ( 2 n / 3 ) + 1 T(n)=T(2n/3)+1 T(n)=T(2n/3)+1
解:这里 a = 1 , b = 3 / 2 , f ( n ) = 1 a=1,b=3/2,f(n)=1 a=1,b=3/2,f(n)=1,计算得出 n l o g b a = n l o g 3 / 2 1 = n 0 = 1 n^{log_b^a}=n^{log_3/2^1}=n^0=1 nlogba=nlog3/21=n0=1
因此符合主定理的情况2,我们得到 T ( n ) = Θ ( n l o g b a ) l g n T(n)=Θ(n^{log_b^a})lgn T(n)=Θ(nlogba)lgn,所以 T ( n ) = Θ ( l g n ) T(n)=Θ(lgn) T(n)=Θ(lgn)

例3: T ( n ) = T ( n / 3 ) + n T(n)=T(n/3)+n T(n)=T(n/3)+n
解:这里 a = 1 , b = 3 , f ( n ) = n a=1,b=3,f(n)=n a=1,b=3,f(n)=n,计算得出 n l o g b a = n l o g 3 1 = n 0 = 1 n^{log_b^a}=n^{log_3^1}=n^0=1 nlogba=nlog31=n0=1
根据主定理中的定义, f ( n ) f(n) f(n)是多项式大于 n l o g b a n^{log_b^a} nlogba,因为 f ( n ) = Ω ( n 0 + ϵ ) f(n)=Ω(n^{0+ϵ}) f(n)=Ω(n0+ϵ)对于 ϵ = 1 ϵ=1 ϵ=1
由于 f ( n ) f(n) f(n)是多项式大于 n l o g b a n^{log_b^a} nlogba,而且满足 a f ( n / b ) = ( 1 / 3 ) f ( n ) ≤ c f ( n ) af(n/b)=(1/3)f(n)≤cf(n) af(n/b)=(1/3)f(n)cf(n)对于某个常数 c < 1 c<1 c<1和所有足够大的 n n n,根据情况3,我们得到 T ( n ) = Θ ( f ( n ) ) T(n)=Θ(f(n)) T(n)=Θ(f(n))
因此 T ( n ) = Θ ( n ) T(n)=Θ(n) T(n)=Θ(n)

例4: T ( n ) = 2 T ( n / 2 ) + n l g n T(n)=2T(n/2)+nlgn T(n)=2T(n/2)+nlgn
解:这里 a = 2 , b = 2 , f ( n ) = n l g n a=2,b=2,f(n)=nlgn a=2,b=2,f(n)=nlgn,计算得出 n l o g b a = n l o g 2 2 = n n^{log_b^a}=n^{log_2^2}=n nlogba=nlog22=n
直观上, n l g n nlgn nlgn n n n大,因为 l g n lgn lgn n n n的对数函数,随着 n n n的增加而增加。但这里并不满足情况3,因为在这个例子中, f ( n ) = n l g n f(n)=nlgn f(n)=nlgn并不是多项式大于 n n n,因为 l g n lgn lgn的增长速度远慢于任何多项式函数。实际上, l g n lgn lgn是对数增长,而多项式增长比对数增长快得多。
或者我们可以依靠下面的计算: f ( n ) / n l o g b a = ( n l g n ) / n = l g n f(n)/n^{log_b^a}=(nlgn)/n=lgn f(n)/nlogba=(nlgn)/n=lgn,这是渐进小于 n ϵ n^ϵ nϵ对于 ϵ > 0 ϵ>0 ϵ>0
这个递归关系实际上适用于主定理的第二种情况,因此我们得到 T ( n ) = Θ ( n l g 2 n ) T(n)=Θ(nlg^2n) T(n)=Θ(nlg2n)

例5: T ( n ) = 2 T ( n 1 / 2 ) + n l g n T(n)=2T(n^{1/2})+nlgn T(n)=2T(n1/2)+nlgn
解:这里的递归关系其实不符合主定理的标准形式,因为这里括号里面 n n n有指数。
但我们可以使用还原去解决这个问题,我们令 k = l o g n k=logn k=logn,则原算式变为 T ( n ) = T ( 2 k ) = 2 T ( 2 k / 2 ) + k T(n)=T(2^k)=2T(2^{k/2})+k T(n)=T(2k)=2T(2k/2)+k
我们定义新的函数 S ( k ) = T ( 2 k ) S(k)=T(2^k) S(k)=T(2k),将其替换到算式中,得到 S ( k ) = 2 S ( k / 2 ) + k S(k)=2S(k/2)+k S(k)=2S(k/2)+k,这个新的递归关系现在符合主定理的标准形式,其中 a = 2 , b = 2 , f ( k ) = k a=2,b=2,f(k)=k a=2b=2f(k)=k
计算得出 n l o g b a = k l o g 2 2 = k n^{log_b^a}=k^{log_2^2}=k nlogba=klog22=k
所以其符合第二种情况,这意味着 S ( k ) = Θ ( k l o g k ) S(k)=Θ(klogk) S(k)=Θ(klogk) k = l o g n k=logn k=logn回代到 S ( k ) S(k) S(k)中,我们得到 T ( n ) = S ( l o g n ) = Θ ( ( l o g n ) l o g ( l o g n ) ) T(n)=S(logn)=Θ((logn)log(logn)) T(n)=S(logn)=Θ((logn)log(logn)),因此原算式的时间复杂度是 O ( l o g n l o g l o g n ) O(lognloglogn) O(lognloglogn)