第7章BigO在日常代码中的使用
在之前的章节中,你学习了如何使用大O表示法来表达代码的时间复杂度。正如你所看到的,大O分析涉及许多细节。在本章中,我们将运用你至今所学的知识,分析现实世界代码库中可能出现的实际代码的效率。
确定代码的效率是优化代码的第一步。毕竟,如果我们不知道代码有多快,我们怎么知道我们的修改能否使其更快呢?
此外,一旦我们知道代码在大O表示法中的分类,我们就可以判断它是否需要优化。例如,时间复杂度为O(N^2)的算法通常被认为是“慢”的算法。因此,如果我们确定我们的算法属于这样的类别,我们应该停下来想一想是否有优化的方法。
当然,对于某个特定问题,O(N^2)可能是我们能做到的最好的了。然而,知道我们的算法被认为是慢的,可以提示我们深入研究,并分析是否有更快的替代方案。
在本书的后续章节中,你将学习到许多优化代码速度的技巧。但优化的第一步是能够确定我们的代码当前有多快。
所以,让我们开始吧。
偶数的平均值
以下的 Ruby 方法接受一个数字数组,并返回其中所有偶数的平均值。我们如何用大O表示法来表示其效率呢?
def average_of_even_numbers(array)
# The mean average of even numbers will be defined as the sum of
# the even numbers divided by the count of even numbers, so we
# keep track of both the sum and the count:
sum = 0.0
count_of_even_numbers = 0
# We iterate through each number in the array, and if we encounter
# an even number, we modify the sum and the count:
array.each do |number|
if number.even?
sum += number
count_of_even_numbers += 1
end
end
# We return the mean average:
return sum / count_of_even_numbers
end
下面是如何分解代码以确定其效率的步骤。
记住,大O表示法关键问题在于回答:如果有N个数据元素,算法需要多少步骤?因此,我们首先要确定“N”数据元素是什么。
在这种情况下,算法正在处理传递给该方法的数字数组。这些数字数组,就是“N”数据元素,其中N是数组的大小。
接下来,我们需要确定算法处理这些N个值需要多少步骤。
我们可以看到,算法的核心是对传入数组中的每个数字进行迭代的循环,因此我们首先要分析这个循环。由于循环遍历了N个元素中的每一个,我们知道算法至少需要N步。
不过,在循环内部,我们可以看到在每一轮循环中会发生不同数量的步骤。对于每一个数字,我们检查它是否为偶数。然后,如果数字是偶数,我们执行两个额外的步骤:修改sum变量和修改count_of_even_numbers变量。因此,对于偶数,我们执行的步骤比对于奇数多三步。
正如你所学到的,大O主要关注最坏情况。在我们的情况下,最坏情况是所有数字都是偶数,在这种情况下,我们在每一轮循环中执行三个步骤。因此,我们可以说对于N个数据元素,我们的算法需要3N步。也就是说,对于N个数字中的每一个,我们的算法执行三步。
现在,我们的方法在循环之外执行了一些其他步骤。在循环之前,我们初始化了两个变量并将它们设置为0。从技术上讲,这是两个步骤。在循环之后,我们执行另一个步骤:sum / count_of_even_numbers的除法运算。因此,从技术上讲,除了3N步之外,我们的算法还需要执行三个额外的步骤,所以总步数是3N + 3。
但是,你也学到了大O表示法忽略常数,所以我们不会称我们的算法为O(3N + 3),而是简单地称之为O(N)。
词汇构建器
以下是该算法的JavaScript实现。让我们看看它的大O效率:
function wordBuilder(array) {
let collection = [];
for(let i = 0; i < array.length; i++) {
for(let j = 0; j < array.length; j++) {
if (i !== j) {
collection.push(array[i] + array[j]);
}
}
}
return collection;
}
在这里,我们使用了一个嵌套的循环。外部循环遍历数组中的每个字符,通过索引i进行跟踪。对于每个索引i,我们运行一个内部循环,再次使用索引j遍历相同数组中的每个字符。在这个内部循环中,我们将i和j位置的字符连接在一起,除非i和j指向同一个索引。
为了确定我们的算法的效率,我们再次需要确定N个数据元素是什么。在我们的情况下,就像前面的例子一样,N是传递给函数的数组中的项数。
接下来,我们需要确定我们的算法相对于N个数据元素需要多少步骤。在我们的情况下,外部循环遍历了所有N个元素,对于每个元素,内部循环再次遍历了所有N个元素,这相当于N步乘以N步。这是O(N^2)的典型情况,通常是嵌套循环算法的结果。
现在,如果我们修改算法来计算三字符字符串的每个组合会发生什么呢?也就是说,对于我们的例子数组[“a”, “b”, “c”, “d”],我们的函数将返回以下数组:
[
‘abc’, ‘abd’, ‘acb’,
‘acd’, ‘adb’, ‘adc’,
‘bac’, ‘bad’, ‘bca’,
‘bcd’, ‘bda’, ‘bdc’,
‘cab’, ‘cad’, ‘cba’,
‘cbd’, ‘cda’, ‘cdb’,
‘dab’, ‘dac’, ‘dba’,
‘dbc’, ‘dca’, ‘dcb’
]]
以下是使用三个嵌套循环的实现。它的时间复杂度是多少呢?
function wordBuilder(array) {
let collection = [];
for(let i = 0; i < array.length; i++) {
for(let j = 0; j < array.length; j++) {
for(let k = 0; k < array.length; k++) {
if (i !== j && j !== k && i !== k) {
collection.push(array[i] + array[j] + array[k]);
}
}
}
}
return collection;
}
在这个算法中,对于N个数据元素,我们有i循环的N步乘以j循环的N步乘以k循环的N步。这是N * N * N,即N³步,用大O表示为O(N³)。
如果我们有四个或五个嵌套循环,分别会得到O(N⁴)和O(N⁵)的算法。让我们看看这些如何在第99页的图表上呈现。
![[Pasted image 20231216170049.png]]
将代码从O(N³)的速度优化到O(N^2)将是一个巨大的进步,因为代码会呈指数级加速。
数组抽样
以下是一个获取数组样本的函数示例。我们期望处理非常大的数组,所以我们的样本只包括数组的第一个元素、中间元素和最后一个元素。
def sample(array):
first = array[0]
middle = array[int(len(array) / 2)]
last = array[-1]
return [first, middle, last]
在这种情况下,传递给这个函数的数组是主要的数据,因此我们可以说N是这个数组中的元素数量。
然而,无论N是多少,我们的函数最终都会执行相同数量的步骤。从数组的开始、中间和末尾索引读取每个元素都只需要一步,不论数组的大小如何。同样地,获取数组的长度并将其除以2也只需要一步。
由于步骤数量是恒定的——也就是说,不管N是多少,它都保持不变——这个算法被认为是O(1)。
平均摄氏度读数
这里有另一个涉及平均温度的示例。假设我们正在开发天气预报软件。为了确定一个城市的温度,我们从城市中的许多温度计中获取温度读数,然后计算这些温度的平均值。
我们希望以华氏度和摄氏度显示温度,但是我们最初只以华氏度获取读数。为了获得摄氏度的平均温度,我们的算法执行两件事情:首先,它将所有读数从华氏度转换为摄氏度。然后,它计算所有摄氏度数字的平均值。
以下是一些 Ruby 代码来实现这一目标。它的大O表示是多少呢?
def average_celsius(fahrenheit_readings)
# 收集摄氏度数字:
celsius_numbers = []
# 将每个读数转换为摄氏度并添加到数组中:
fahrenheit_readings.each do |fahrenheit_reading|
celsius_conversion = (fahrenheit_reading - 32) / 1.8
celsius_numbers.push(celsius_conversion)
end
# 获取所有摄氏度数字的总和:
sum = 0.0
celsius_numbers.each do |celsius_number|
sum += celsius_number
end
# 返回平均值:
return sum / celsius_numbers.length
end
首先,我们可以说N是传递到我们方法中的fahrenheit_readings的数量。
在方法内部,我们运行了两个循环。第一个循环将读数转换为摄氏度,第二个循环计算所有摄氏度数字的总和。由于我们有两个循环,每个循环都要迭代所有N个元素,因此是N + N,也就是2N(加上一些常数步骤)。由于大O符号忽略常数,这被简化为O(N)。不要被早期“Word Builder”示例中的两个循环导致O(N^2)的效率所迷惑。在那里,循环是嵌套的,导致了N个步骤乘以N个步骤。然而,在我们的情况下,我们只是有两个循环,一个接着一个。这是N个步骤加上N个步骤(2N),只是简单的O(N)。
服装标签
我们的代码接受一个新生产的服装项目数组(以字符串形式存储),并为我们需要的每个可能标签创建文本。
特别是,我们的标签应包含商品名称以及尺码,尺码范围从 1 到 5。例如,如果我们有数组 [“Purple Shirt”, “Green Shirt”],我们希望为这些衬衫生成以下标签文本:
[
“Purple Shirt Size: 1”,
“Purple Shirt Size: 2”,
“Purple Shirt Size: 3”,
“Purple Shirt Size: 4”,
“Purple Shirt Size: 5”,
“Green Shirt Size: 1”,
“Green Shirt Size: 2”,
“Green Shirt Size: 3”,
“Green Shirt Size: 4”,
“Green Shirt Size: 5”
]
以下是用 Python 编写的代码,可以为整个服装项目数组创建此文本:
def mark_inventory(clothing_items):
clothing_options = []
for item in clothing_items:
for size in range(1, 6): # 对于 1 到 5 的尺码(Python 范围是取到第二个数字,但不包括它):
clothing_options.append(item + " Size: " + str(size))
return clothing_options
让我们确定这个算法的效率。clothing_items 是正在处理的主要数据,所以 N 是 clothing_items 的数量。
这段代码包含了嵌套循环,所以很容易宣称这个算法的时间复杂度为 O(N^2)。然而,我们需要更仔细地分析一下。虽然包含嵌套循环的代码通常是 O(N^2),但在这种情况下并非如此。
导致 O(N^2) 的嵌套循环是每个循环都围绕着 N 进行的。然而,在我们的情况下,外部循环运行了 N 次,而内部循环运行了恒定的五次。也就是说,无论 N 是多少,内部循环总是运行五次。
因此,虽然外部循环运行了 N 次,但内部循环针对每个 N 字符串运行了五次。虽然这意味着我们的算法运行了 5N 次,但根据大 O 表示法,这被简化为 O(N),因为大 O 表示法忽略常数项。
统计1的数量
这是另一个算法,其时间复杂度与一开始看起来的不同。该函数接受一个包含 1 和 0 的数组组成的数组,然后返回其中有多少个 1。对于这个示例输入:
[
[0, 1, 1, 1, 0],
[0, 1, 0, 1, 0, 1],
[1, 0]
]
我们的函数会返回 7,因为有七个 1。以下是Python中的函数:
def count_ones(outer_array):
count = 0
for inner_array in outer_array:
for number in inner_array:
if number == 1:
count += 1
return count
这个算法的时间复杂度是多少呢?
再次强调,很容易注意到嵌套循环并得出它是 O(N^2) 的结论。然而,这两个循环遍历了完全不同的内容。
外层循环遍历内部数组,而内层循环遍历实际的数字。最终,我们的内部循环只运行了总数字的数量。
因此,可以说 N 代表了有多少个数字。由于我们的算法只是简单地处理每个数字,所以函数的时间复杂度是 O(N)。
回文检查器
回文是指无论是正着读还是倒着读都相同的单词或短语。一些例子包括“racecar”、“kayak”和“deified”。
这是一个判断字符串是否为回文的 JavaScript 函数:
function isPalindrome(string) {
// 左边界从索引0开始:
let leftIndex = 0;
// 右边界从数组最后一个索引开始:
let rightIndex = string.length - 1;
// 循环直到左边界达到数组的中间位置:
while (leftIndex < string.length / 2) {
// 如果左边的字符不等于右边的字符,则字符串不是回文:
if (string[leftIndex] !== string[rightIndex]) {
return false;
}
// 左边界向右移动一位:
leftIndex++;
// 右边界向左移动一位:
rightIndex--;
}
// 如果整个循环中没有发现任何不匹配,则字符串必定是回文:
return true;
}
让我们来确定这个算法的时间复杂度。
在这种情况下,N 是传递给此函数的字符串的大小。
算法的核心部分发生在 while 循环内。现在,这个循环有点有趣,因为它仅运行到达字符串中点为止。这意味着循环运行了 N / 2 步。
然而,大 O 表示法忽略常数。因此,我们去掉除以 2,所以我们的算法是 O(N)。
获取所有产品
我们的下一个示例是一个算法,接受一个数字数组,并返回两个数字的所有组合乘积。
例如,如果我们传入数组 [1, 2, 3, 4, 5],该函数返回:
[2, 3, 4, 5, 6, 8, 10, 12, 15, 20]
这是因为我们首先将1乘以2、3、4和5。然后将2乘以3、4和5。接着将3乘以4和5。最后将4乘以5。
注意一个有趣的地方:当我们将2乘以其他数字时,我们只需要将它乘以其右侧的数字。我们不需要回头再将2乘以1,因为当我们将1乘以2时已经包含了这部分。因此,每个数字只需要与其右侧的剩余数字相乘。
以下是该算法的 JavaScript 实现:
function twoNumberProducts(array) {
let products = [];
// 外层数组循环:
for(let i = 0; i < array.length - 1; i++) {
// 内层数组循环,其中 j 总是从 i 的右侧一索引开始:
for(let j = i + 1; j < array.length; j++) {
products.push(array[i] * array[j]);
}
}
return products;
}
让我们来分析一下。N 是传递给此函数的数组中的项目数量。
外层循环运行了 N 次。(实际上运行了 N - 1 次,但我们将忽略该常数。)不过,内层循环不同。因为 j 总是从 i 的右侧一索引开始,所以内层循环的步数会随着每次外层循环的启动而减少。
让我们看看内层循环对我们的示例数组(包含五个元素)运行了多少次:
当 i 为 0 时,内层循环在 j 为 1、2、3 和 4 时运行。当 i 为 1 时,内层循环在 j 为 2、3 和 4 时运行。当 i 为 2 时,内层循环在 j 为 3 和 4 时运行。当 i 为 3 时,内层循环在 j 为 4 时
运行。当所有操作完成时,内层循环运行了:
4 + 3 + 2 + 1 次。
将这个用 N 来表示,我们可以说内层循环大致运行了:
N + (N - 1) + (N - 2) + (N - 3) … + 1 次。
这个公式总是计算出大约是 N^2 / 2。我们可以在第105页的图表中进行可视化展示。为了图表的目的,我们假设 N 是 8,所以有 8^2,或者说 64 个方块。
![[Pasted image 20231216171938.png]]
如果你从顶部一行逐步向下观察,你会发现顶部一行有 N 个方块被涂成灰色。接下来一行有 N - 1 个方块被涂成灰色,再下一行有 N - 2 个灰色方块。这个模式一直持续到底部,底部只有一个方块被涂成灰色。
你也可以一眼看出有一半的方块被涂成灰色。这表明了 N + (N - 1) + (N - 2) + (N - 3)… + 1 这个模式相当于 N^2 / 2。
于是我们推断出,内部循环运行了 N^2 / 2 步。但是因为大 O 忽略了常数,所以我们将其表示为 O(N^2)。
处理多个数据集
现在,如果我们不是计算单个数组中每两个数的乘积,而是计算一个数组中的每个数与另一个数组中的每个数的乘积会发生什么呢?
例如,如果我们有数组 [1, 2, 3] 和数组 [10, 100, 1000],我们将计算出的乘积为:
[10, 100, 1000, 20, 200, 2000, 30, 300, 3000]
我们的代码与之前的片段类似,稍作修改:
function twoNumberProducts(array1, array2) {
let products = [];
for(let i = 0; i < array1.length; i++) {
for(let j = 0; j < array2.length; j++) {
products.push(array1[i] * array2[j]);
}
}
return products;
}
让我们分析一下这个函数的时间复杂度。
首先,什么是 N?这是第一个难题,因为现在我们有两个数据集,也就是两个数组。
把所有东西都放在一起,说 N 是两个数组中所有项目的总数是很诱人的。然而,这是有问题的,原因如下:
这是两种情况的故事。在情景1中,有两个大小为5的数组。在情景2中,有一个大小为9的数组和另一个大小为1的数组。
在这两种情况下,我们都会说 N 是10,因为 5 + 5 = 10 和 9 + 1 = 10。然而,两种情况的效率却大不相同。
在情景1中,我们的代码需要25(5 * 5)步。因为 N 是10,这相当于 (N / 2)² 步。
然而,在情景2中,我们的代码只需要 9(9 * 1)步,接近于 N 步。这比情景1快得多!
所以,我们不想把 N 视为两个数组中整数的总数,因为我们永远无法根据大 O 表示法确定效率,因为它会根据不同的情况而变化。
我们现在有点为难。我们别无选择,只能将时间复杂度表示为 O(N * M),其中 N 是一个数组的大小,而 M 是另一个数组的大小。
这是一个新的概念:每当我们有两个不同的数据集需要通过乘法相互交互时,在用大O表示法描述效率时,我们必须分别识别这两个数据源。虽然这是用大O表示法表达算法的正确方式,但它比其他大O表示法的表达略显不够有用。将O(N * M)算法与仅含有N(而非M)的算法进行比较有点像比较苹果和橙子。
然而,我们知道O(N * M)存在于特定的范围内。也就是说,如果N和M相同,它相当于O(N^2)。如果它们不相同,我们会任意将较小的数赋值给M,即使M只有1,最终得到O(N)。从某种意义上讲,O(N * M)可以被理解为介于O(N)和O(N^2)之间的范围。
这很棒吗?不,但这是我们能做到的最好的描述方式。
密码破解器
你是一个黑客(当然是道德的),试图破解某人的密码。你决定采用穷举法,并编写了一些代码来生成给定长度的所有可能字符串。以下是你编写的代码:
def every_password(n)
(("a" * n)..("z" * n)).each do |str|
puts str
end
end
这段代码的工作方式是你将一个数字传递给这个函数,它将成为变量n。
例如,如果n为3,代码中的 “a” * n 会产生字符串 “aaa”。然后代码在 “aaa” 到 “zzz” 的所有可能字符串范围内设置一个循环。运行这段代码将打印:
aaa
aab
aac
aad
aae
...
zzx
zzy
zzz
如果n为4,你的代码将打印长度为4的所有可能字符串:
aaaa
aaab
aaac
aaad
aaae
...
zzzx
zzzy
zzzz
如果你尝试运行这段代码,即使只是长度为5的情况,你可能需要等待相当长的时间才能完成。这是一个很慢的算法!但是我们如何用大O表示法来表达它呢?
让我们来分解一下。
如果我们只是简单地打印出字母表中的每个字母一次,那就需要26步。
当我们打印每个两个字符的组合时,最终会有26个字符乘以26个字符。
当打印每个三个字符的组合时,最终会有26 * 26 * 26个组合。
你看到规律了吗?
![[Pasted image 20231216182801.png]]
如果我们用N来表示这个问题,就会发现,如果N是每个字符串的长度,那么组合的数量就是26的N次方。
因此,在大O表示法中,我们将其表示为O(26N)。这是一个极其缓慢的算法!事实上,即使是一个“仅仅”是O(2N)的算法也是非常慢的。让我们来看看与迄今为止我们见过的一些其他算法相比,它在图表上的表现:
![[Pasted image 20231216182909.png]]
正如你所看到的,O(2^N) 在某个点上甚至比O(N^3) 还要慢。
从某种意义上说,O(2^N) 是O(log N)的对立面。对于O(log N)的算法(比如二分搜索),每次数据加倍时,算法只需要额外一步。而对于O(2^N)的算法,每次增加一个数据元素,算法的步骤数量就会加倍!在我们的密码破解器中,每当我们将N增加一次,步骤数就会乘以26。这需要非常长的时间,这就是为什么穷举法是破解密码的一种低效方式。
总结
恭喜!你现在是一个大O表示法的专家。你可以分析各种算法并对其时间复杂度进行分类。拥有这些知识,你将能够系统地优化你的代码以提高速度。
说到优化,下一章中我们将发现一种全新的数据结构,它是加速算法最有用、最常见的工具之一。我在谈论一些真正的速度提升。
练习
-
使用大O表示法描述以下函数的时间复杂度。该函数在数组是“100-Sum Array”时返回true,在不是时返回false。一个“100-Sum Array”满足以下条件:
- 第一个和最后一个数字的和为100。
- 第二个和倒数第二个数字的和为100。
- 第三个和倒数第三个数字的和为100,依此类推。
以下是函数代码:
def one_hundred_sum?(array) left_index = 0 right_index = array.length - 1 while left_index < array.length / 2 if array[left_index] + array[right_index] != 100 return false end left_index += 1 right_index -= 1 end return true end
-
使用大O表示法描述以下函数的时间复杂度。它将两个已排序的数组合并成一个新的已排序数组,包含来自两个数组的所有值:
def merge(array_1, array_2) new_array = [] array_1_pointer = 0 array_2_pointer = 0 # Run the loop until we've reached end of both arrays: while array_1_pointer < array_1.length || array_2_pointer < array_2.length # If we already reached the end of the first array, # add item from second array: if !array_1[array_1_pointer] new_array << array_2[array_2_pointer] array_2_pointer += 1 # If we already reached the end of the second array, # add item from first array: elsif !array_2[array_2_pointer] new_array << array_1[array_1_pointer] array_1_pointer += 1 # If the current number in first array is less than current # number in second array, add from first array: elsif array_1[array_1_pointer] < array_2[array_2_pointer] new_array << array_1[array_1_pointer] array_1_pointer += 1 # If the current number in second array is less than or equal # to current number in first array, add from second array: else new_array << array_2[array_2_pointer] array_2_pointer += 1 end end return new_array end
-
使用大O表示法描述以下函数的时间复杂度。该函数解决了一个著名的问题,被称为“在草堆中找针”。
针(needle)和草堆(haystack)都是字符串。例如,如果针是 “def” 而草堆是 “abcdefghi”,那么针就包含在草堆中,因为 “def” 是 “abcdefghi” 的子字符串。但是,如果针是 “dd”,那么它无法在 “abcdefghi” 的草堆中找到。
该函数根据针是否能在草堆中找到返回 true 或 false:def find_needle(needle, haystack) needle_index = 0 haystack_index = 0 while haystack_index < haystack.length if needle[needle_index] == haystack[haystack_index] found_needle = true while needle_index < needle.length if needle[needle_index] != haystack[haystack_index + needle_index] found_needle = false break end needle_index += 1 end return true if found_needle needle_index = 0 end haystack_index += 1 end return false end
-
使用大O表示法描述以下函数的时间复杂度。该函数从给定数组中找出三个数的最大乘积:
def largest_product(array) largest_product_so_far = array[0] * array[1] * array[2] i = 0 while i < array.length j = i + 1 while j < array.length k = j + 1 while k < array.length if array[i] * array[j] * array[k] > largest_product_so_far largest_product_so_far = array[i] * array[j] * array[k] end k += 1 end j += 1 end i += 1 end return largest_product_so_far end
-
我曾经看过一个针对人力资源(HR)的笑话:“想立即从招聘流程中淘汰最倒霉的人吗?只需将你桌上的简历一半扔进垃圾桶。”
如果我们要编写一款软件,不断减少一堆简历直到只剩下一个,它可能会采取交替抛弃顶部一半和底部一半的方法。也就是说,它首先会消除一半顶部的简历,然后继续消除剩余部分的一半底部。它不断交替消除顶部和底部,直到只剩下一份幸运的简历,那就是我们要招聘的人选!
用大O表示法描述这个函数的效率:
def pick_resume(resumes)
eliminate = "top"
while resumes.length > 1
if eliminate == "top"
resumes = resumes[resumes.length / 2, resumes.length - 1]
eliminate = "bottom"
elsif eliminate == "bottom"
resumes = resumes[0, resumes.length / 2]
eliminate = "top"
end
end
return resumes[0]
end
答案
-
这里,N是数组的大小。我们的循环运行了N / 2次,因为每轮循环处理两个值。然而,这被表示为O(N),因为我们舍弃了常数部分。
-
在这种情况下,定义N稍微有些棘手,因为我们处理了两个不同的数组。然而,算法只处理每个值一次,所以我们可以决定将N称为两个数组之间的总值数量,时间复杂度将为O(N)。如果我们想要更字面一些,将一个数组称为N,另一个称为M,我们可以选择用O(N + M)来表达效率。然而,因为我们只是简单地将N和M相加,所以更简单的方式是使用N来表示两个数组总共的数据元素数量,并将其称为O(N)。
-
在最坏的情况下,该算法的运行次数为“needle”中的字符数乘以“haystack”中的字符数。由于needle和haystack可能具有不同数量的字符,所以时间复杂度是O(N * M)。
-
N是数组的大小,时间复杂度是O(N^3),因为它经过三层嵌套循环处理。实际上,中间的循环运行了N / 2次,最内层循环运行了N / 4次,所以是N * (N / 2) * (N / 4),这是N^3 / 8步骤。但我们舍弃了常数部分,留下了O(N^3)。
-
N是resumes数组的大小。由于每轮循环我们都消除了一半的简历,因此我们有一个O(log N)的算法。