二分查找 - worst/avg 个案例
Binary search - worst/avg case
我发现很难理解 why/how 使用二进制搜索在 array/list 中搜索键的最坏和平均情况是 O(log(n))。
log(1,000,000) 只有 6。log(1,000,000,000) 只有 9 - 我明白了,但我不明白其中的解释。如果没有测试,我们怎么知道 avg/worst 的情况实际上是 log(n)?
我希望你们明白我想说的。如果没有,请告诉我,我会尝试以不同的方式解释。
最坏情况
每次二进制搜索代码做出决定时,它都会从考虑中消除一半的剩余元素。因此,您将每个决定的元素数量除以 2。
在您只剩下一个元素之前,您可以除以 2 多少次?如果n是元素的起始个数,x是你除以2的次数,我们可以这样写:
n / (2 * 2 * 2 * ... * 2) = 1 ['2' 重复 x 次]
或者,等价地,
n / 2^x = 1
或者,等价地,
n = 2^x
所以 n 的以 2 为底的对数给你 x,这是正在做出的决定的数量。
最后,你可能会问,如果我使用log base 2,为什么像你那样写成log base 10 也可以?基数无关紧要,因为差异是 only a constant factor,按大 O 表示法是 "ignored"。
平均情况
我看到你也问了平均情况。考虑:
- 数组中只有一个元素可以在第一次尝试时找到。
- 第二次尝试只能找到两个元素。 (因为第一次尝试后,我们选择了右半边或左半边。)
- 第三次尝试只能找到四个元素。
您可以看到模式:1, 2, 4, 8, ... , n/2。表示相同的模式向另一个方向发展:
- 一半的元素采取最大数量的决定来找到。
- 四分之一的元素少了一次查找决定。
- 等等
由于一半的元素花费的时间最长,因此其他元素花费的时间少多少并不重要。我们可以假设所有元素都花费了最多的时间,即使其中一半实际上花费了 0 时间,无论真实平均值是多少,我们的假设都不会超过两倍。我们可以忽略 "double" 因为它是一个常数因子。因此,就大 O 符号而言,平均情况与最坏情况相同。
对于二分查找,数组应该按升序或降序排列。
- 在每一步中,算法都会将搜索键值与数组中间元素的键值进行比较。
- 如果键匹配,则找到匹配元素并返回其索引或位置。
- 否则,如果搜索关键字小于中间元素的关键字,则算法对中间元素左侧的子数组重复其操作。
- 或者,如果搜索关键字更大,则算法对右侧的子数组重复其操作。
- 如果要查找的剩余数组为空,则在数组中找不到key,返回一个特殊的"not found"指示。
所以,二分查找是 dichotomic divide and conquer search algorithm
。因此,执行搜索操作需要对数时间,因为在每次迭代中元素都减少了一半。
对于我们可以进行二进制搜索的排序列表,二进制搜索生成的每个 "decision" 都会将您的键与中间元素进行比较,如果大于则取列表的右半部分,如果小于则取右半部分取列表的左半部分(如果匹配,它将 return 该位置的元素)对于每个产生 O(logn) 的决定,您有效地将列表减半。
然而,二进制搜索仅适用于排序列表。对于未排序的列表,您可以从第一个元素开始直接搜索,产生 O(n) 的复杂度。
O(logn) < O(n)
虽然这完全取决于您要进行的搜索次数、您的输入等,但您的最佳方法是什么。
对于二进制搜索,先决条件是将排序数组作为输入。
• 列表排序后:
• 当然,我们不必查字典中的每个单词来查找单词。
• 一个基本策略是反复将搜索范围减半,直到找到值。
• 例如,在 9 #s below.v = 1 1 3 5 8 10 18 33 42
的列表中查找 5
• 我们首先从中间开始:8
• 由于 5<8,我们知道我们可以只看前半部分:1 1 3 5
• 再看中间的 #,缩小到 3 5
• 然后当我们减少到一个时我们停止 #: 5
需要多少比较:4 =log(base 2)(9-1)=O(log(base2)n)
int binary_search (vector<int> v, int val) {
int from = 0;
int to = v.size()-1;
int mid;
while (from <= to) {
mid = (from+to)/2;
if (val == v[mid])
return mid;
else if (val > v[mid])
from = mid+1;
else
to = mid-1;
}
return -1;
}
我发现很难理解 why/how 使用二进制搜索在 array/list 中搜索键的最坏和平均情况是 O(log(n))。
log(1,000,000) 只有 6。log(1,000,000,000) 只有 9 - 我明白了,但我不明白其中的解释。如果没有测试,我们怎么知道 avg/worst 的情况实际上是 log(n)?
我希望你们明白我想说的。如果没有,请告诉我,我会尝试以不同的方式解释。
最坏情况
每次二进制搜索代码做出决定时,它都会从考虑中消除一半的剩余元素。因此,您将每个决定的元素数量除以 2。
在您只剩下一个元素之前,您可以除以 2 多少次?如果n是元素的起始个数,x是你除以2的次数,我们可以这样写:
n / (2 * 2 * 2 * ... * 2) = 1 ['2' 重复 x 次]
或者,等价地,
n / 2^x = 1
或者,等价地,
n = 2^x
所以 n 的以 2 为底的对数给你 x,这是正在做出的决定的数量。
最后,你可能会问,如果我使用log base 2,为什么像你那样写成log base 10 也可以?基数无关紧要,因为差异是 only a constant factor,按大 O 表示法是 "ignored"。
平均情况
我看到你也问了平均情况。考虑:
- 数组中只有一个元素可以在第一次尝试时找到。
- 第二次尝试只能找到两个元素。 (因为第一次尝试后,我们选择了右半边或左半边。)
- 第三次尝试只能找到四个元素。
您可以看到模式:1, 2, 4, 8, ... , n/2。表示相同的模式向另一个方向发展:
- 一半的元素采取最大数量的决定来找到。
- 四分之一的元素少了一次查找决定。
- 等等
由于一半的元素花费的时间最长,因此其他元素花费的时间少多少并不重要。我们可以假设所有元素都花费了最多的时间,即使其中一半实际上花费了 0 时间,无论真实平均值是多少,我们的假设都不会超过两倍。我们可以忽略 "double" 因为它是一个常数因子。因此,就大 O 符号而言,平均情况与最坏情况相同。
对于二分查找,数组应该按升序或降序排列。
- 在每一步中,算法都会将搜索键值与数组中间元素的键值进行比较。
- 如果键匹配,则找到匹配元素并返回其索引或位置。
- 否则,如果搜索关键字小于中间元素的关键字,则算法对中间元素左侧的子数组重复其操作。
- 或者,如果搜索关键字更大,则算法对右侧的子数组重复其操作。
- 如果要查找的剩余数组为空,则在数组中找不到key,返回一个特殊的"not found"指示。
所以,二分查找是 dichotomic divide and conquer search algorithm
。因此,执行搜索操作需要对数时间,因为在每次迭代中元素都减少了一半。
对于我们可以进行二进制搜索的排序列表,二进制搜索生成的每个 "decision" 都会将您的键与中间元素进行比较,如果大于则取列表的右半部分,如果小于则取右半部分取列表的左半部分(如果匹配,它将 return 该位置的元素)对于每个产生 O(logn) 的决定,您有效地将列表减半。
然而,二进制搜索仅适用于排序列表。对于未排序的列表,您可以从第一个元素开始直接搜索,产生 O(n) 的复杂度。
O(logn) < O(n)
虽然这完全取决于您要进行的搜索次数、您的输入等,但您的最佳方法是什么。
对于二进制搜索,先决条件是将排序数组作为输入。
• 列表排序后:
• 当然,我们不必查字典中的每个单词来查找单词。
• 一个基本策略是反复将搜索范围减半,直到找到值。
• 例如,在 9 #s below.v = 1 1 3 5 8 10 18 33 42
的列表中查找 5
• 我们首先从中间开始:8
• 由于 5<8,我们知道我们可以只看前半部分:1 1 3 5
• 再看中间的 #,缩小到 3 5
• 然后当我们减少到一个时我们停止 #: 5
需要多少比较:4 =log(base 2)(9-1)=O(log(base2)n)
int binary_search (vector<int> v, int val) {
int from = 0;
int to = v.size()-1;
int mid;
while (from <= to) {
mid = (from+to)/2;
if (val == v[mid])
return mid;
else if (val > v[mid])
from = mid+1;
else
to = mid-1;
}
return -1;
}