什么会导致算法具有 O(log n) 复杂度?
我对大 O 的了解是有限的,当对数项出现在方程中时,它让我更加困惑。
有人可以简单地向我解释一下O(log n)
算法是什么吗?对数从何而来?
当我试图解决这个期中练习题时,特别出现了这个问题:
设 X(1..n) 和 Y(1..n) 包含两个整数列表,每个列表均按非递减顺序排序。给出一个 O(log n) 时间算法来查找所有 2n 个组合元素的中值(或第 n 个最小整数)。例如,X = (4, 5, 7, 8, 9) 且 Y = (3, 5, 8, 9, 10),则 7 是组合列表 (3, 4, 5, 5, 7) 的中位数、8、8、9、9、10)。 [提示:使用二分搜索的概念]
My knowledge of big-O is limited, and when log terms show up in the equation it throws me off even more.
Can someone maybe explain to me in simple terms what a O(log n)
algorithm is? Where does the logarithm come from?
This specifically came up when I was trying to solve this midterm practice question:
Let X(1..n) and Y(1..n) contain two lists of integers, each sorted in nondecreasing order. Give an O(log n)-time algorithm to find the median (or the nth smallest integer) of all 2n combined elements. For ex, X = (4, 5, 7, 8, 9) and Y = (3, 5, 8, 9, 10), then 7 is the median of the combined list (3, 4, 5, 5, 7, 8, 8, 9, 9, 10). [Hint: use concepts of binary search]
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
我必须承认,当你第一次看到 O(log n) 算法时,这很奇怪……这个对数到底是从哪里来的?然而,事实证明,有几种不同的方法可以让对数项以大 O 表示法显示。以下是一些:
重复除以一个常数
取任意数 n;比如说,16。在得到小于或等于 1 的数字之前,你能将 n 除以 2 多少次?对于 16,我们注意到
这最终需要四个步骤才能完成。有趣的是,我们还有 log2 16 = 4。嗯……128 怎么样?
这花了七步,log2 128 = 7。这是巧合吗?没有!这是有充分理由的。假设我们将数字 n 除以 2 i 次。然后我们得到数字 n / 2i。如果我们想求解 i 的值,其中该值最多为 1,我们得到
换句话说,如果我们选择一个整数 i 使得 i ≥ log2 n,那么将 n 除以 i 次之后,我们将得到一个最多为 1 的值。保证这一点的最小 i 大约为 log2 n,所以如果我们有一个算法除以 2 直到数字变得足够小,然后我们可以说它以 O(log n) 步终止。
一个重要的细节是,n 除以什么常数并不重要(只要它大于 1);如果除以常数 k,则需要 logk n 步才能达到 1。因此,任何重复将输入大小除以某个分数的算法都需要 O(log n) 次迭代才能终止。这些迭代可能需要大量时间,因此净运行时间不必是 O(log n),但步骤数将是对数。
那么这是从哪里出现的呢?一个典型的例子是二分搜索,一种用于搜索排序的快速算法一个值的数组。该算法的工作原理如下:
例如,要在数组中搜索 5
我们首先查看中间元素:
因为 7 > > 5,并且由于数组已排序,我们知道数字 5 不可能位于数组的后半部分,因此我们可以将其丢弃。现在
我们看看中间的元素:
因为 3 < 5,我们知道5不能出现在数组的前半部分,所以我们可以把前半部分数组扔掉。
我们再看看这个数组的中间部分:
因为这正是我们要找的数字,我们可以报告 5 确实在数组中。
那么这有多有效呢?好吧,在每次迭代中,我们都会丢弃至少一半的剩余数组元素。一旦数组为空或者我们找到了我们想要的值,算法就会停止。在最坏的情况下,元素不存在,因此我们不断将数组的大小减半,直到用完元素。这需要多长时间?好吧,由于我们一遍又一遍地将数组切成两半,所以我们最多会在 O(log n) 次迭代中完成,因为在运行之前我们不能将数组切成两半超过 O(log n) 次超出数组元素。
遵循分而治之一般技术的算法(切割出于同样的原因,你不能将某个对象切成两半超过 O(log n)次。您可能想看看合并排序作为一个很好的例子。
一次处理一位数字
以 10 为基数的数字 n 有多少位数字?好吧,如果数字中有 k 位数字,那么最大的数字就是 10k 的某个倍数。最大的 k 位数字是 999...9,k 次,这等于 10k + 1 - 1。因此,如果我们知道 n 有 k 位数字,那么我们知道 n 的值最多为 10k + 1 - 1。如果我们想用 n 来求解 k,我们得到
由此我们得出 k 大约是 n 的以 10 为底的对数。换句话说,n 中的位数是 O(log n)。
例如,让我们考虑一下将两个大数字相加的复杂性,这两个数字太大而无法放入机器字中。假设这些数字以 10 为基数表示,我们将这些数字称为 m 和 n。添加它们的一种方法是通过小学方法 - 一次写出一位数字,然后从右到左进行计算。例如,要添加 1337 和 2065,我们首先将数字写出如下:
我们添加最后一位数字并进位 1:
然后我们添加倒数第二个(“倒数第二个”)数字并进位 1
: ,我们添加倒数第三个(“antepenultimate”)数字:
最后,我们添加倒数第四个(“preantepenultimate”......我喜欢英语)数字:
现在,我们做了多少工作?我们对每个数字总共执行 O(1) 次工作(即恒定量的工作),需要处理的数字总数为 O(max{log n, log m})。这总共给出了 O(max{log n, log m}) 复杂度,因为我们需要访问两个数字中的每个数字。
许多算法通过在某个基数中一次处理一位数字来获得 O(log n) 项。一个典型的例子是基数排序,它将整数按一位数字排序时间。基数排序有多种形式,但它们通常运行时间为 O(n log U),其中 U 是要排序的最大可能整数。原因是每次排序都需要 O(n) 时间,并且总共需要 O(log U) 次迭代来处理正在排序的最大数字的每个 O(log U) 数字。许多高级算法,例如 Gabow 的最短路径算法 或Ford-Fulkerson 最大流量算法,其复杂性有一个对数项,因为它们一次只处理一位数字。
至于关于如何解决该问题的第二个问题,您可能需要查看此相关问题< /a> 探索了更高级的应用程序。鉴于此处描述的问题的一般结构,当您知道结果中存在对数项时,您现在可以更好地了解如何思考问题,因此我建议您在给出答案之前不要查看答案一些想法。
I have to agree that it's pretty weird the first time you see an O(log n) algorithm... where on earth does that logarithm come from? However, it turns out that there's several different ways that you can get a log term to show up in big-O notation. Here are a few:
Repeatedly dividing by a constant
Take any number n; say, 16. How many times can you divide n by two before you get a number less than or equal to one? For 16, we have that
Notice that this ends up taking four steps to complete. Interestingly, we also have that log2 16 = 4. Hmmm... what about 128?
This took seven steps, and log2 128 = 7. Is this a coincidence? Nope! There's a good reason for this. Suppose that we divide a number n by 2 i times. Then we get the number n / 2i. If we want to solve for the value of i where this value is at most 1, we get
In other words, if we pick an integer i such that i ≥ log2 n, then after dividing n in half i times we'll have a value that is at most 1. The smallest i for which this is guaranteed is roughly log2 n, so if we have an algorithm that divides by 2 until the number gets sufficiently small, then we can say that it terminates in O(log n) steps.
An important detail is that it doesn't matter what constant you're dividing n by (as long as it's greater than one); if you divide by the constant k, it will take logk n steps to reach 1. Thus any algorithm that repeatedly divides the input size by some fraction will need O(log n) iterations to terminate. Those iterations might take a lot of time and so the net runtime needn't be O(log n), but the number of steps will be logarithmic.
So where does this come up? One classic example is binary search, a fast algorithm for searching a sorted array for a value. The algorithm works like this:
For example, to search for 5 in the array
We'd first look at the middle element:
Since 7 > 5, and since the array is sorted, we know for a fact that the number 5 can't be in the back half of the array, so we can just discard it. This leaves
So now we look at the middle element here:
Since 3 < 5, we know that 5 can't appear in the first half of the array, so we can throw the first half array to leave
Again we look at the middle of this array:
Since this is exactly the number we're looking for, we can report that 5 is indeed in the array.
So how efficient is this? Well, on each iteration we're throwing away at least half of the remaining array elements. The algorithm stops as soon as the array is empty or we find the value we want. In the worst case, the element isn't there, so we keep halving the size of the array until we run out of elements. How long does this take? Well, since we keep cutting the array in half over and over again, we will be done in at most O(log n) iterations, since we can't cut the array in half more than O(log n) times before we run out of array elements.
Algorithms following the general technique of divide-and-conquer (cutting the problem into pieces, solving those pieces, then putting the problem back together) tend to have logarithmic terms in them for this same reason - you can't keep cutting some object in half more than O(log n) times. You might want to look at merge sort as a great example of this.
Processing values one digit at a time
How many digits are in the base-10 number n? Well, if there are k digits in the number, then we'd have that the biggest digit is some multiple of 10k. The largest k-digit number is 999...9, k times, and this is equal to 10k + 1 - 1. Consequently, if we know that n has k digits in it, then we know that the value of n is at most 10k + 1 - 1. If we want to solve for k in terms of n, we get
From which we get that k is approximately the base-10 logarithm of n. In other words, the number of digits in n is O(log n).
For example, let's think about the complexity of adding two large numbers that are too big to fit into a machine word. Suppose that we have those numbers represented in base 10, and we'll call the numbers m and n. One way to add them is through the grade-school method - write the numbers out one digit at a time, then work from the right to the left. For example, to add 1337 and 2065, we'd start by writing the numbers out as
We add the last digit and carry the 1:
Then we add the second-to-last ("penultimate") digit and carry the 1:
Next, we add the third-to-last ("antepenultimate") digit:
Finally, we add the fourth-to-last ("preantepenultimate"... I love English) digit:
Now, how much work did we do? We do a total of O(1) work per digit (that is, a constant amount of work), and there are O(max{log n, log m}) total digits that need to be processed. This gives a total of O(max{log n, log m}) complexity, because we need to visit each digit in the two numbers.
Many algorithms get an O(log n) term in them from working one digit at a time in some base. A classic example is radix sort, which sorts integers one digit at a time. There are many flavors of radix sort, but they usually run in time O(n log U), where U is the largest possible integer that's being sorted. The reason for this is that each pass of the sort takes O(n) time, and there are a total of O(log U) iterations required to process each of the O(log U) digits of the largest number being sorted. Many advanced algorithms, such as Gabow's shortest-paths algorithm or the scaling version of the Ford-Fulkerson max-flow algorithm, have a log term in their complexity because they work one digit at a time.
As to your second question about how you solve that problem, you may want to look at this related question which explores a more advanced application. Given the general structure of problems that are described here, you now can have a better sense of how to think about problems when you know there's a log term in the result, so I would advise against looking at the answer until you've given it some thought.
当我们谈论大哦描述时,我们通常谈论的是解决给定大小的问题所需的时间。通常,对于简单问题,该大小仅由输入元素的数量来表征,通常称为 n 或 N。(显然这并不总是正确的 - 图的问题通常用顶点数、V 和边数,E;但现在,我们将讨论对象列表,列表中有 N 个对象。)
我们说问题“是(N 的某个函数)的大哦”如果并且仅当:
对于所有N>一些任意的 N_0,有一些常数 c,这样算法的运行时间小于该常数 c 倍(N 的某个函数)。
换句话说,不要考虑其中的小问题设置问题的“持续开销”很重要,思考大问题。当考虑大问题时,(N 的某个函数)的 big-Oh 意味着运行时间仍然总是小于该函数的某个常数倍。总是。
简而言之,该函数是一个上限,直到一个常数因子。
因此,“log(n) 的 big-Oh”与我上面所说的意思相同,只是“N 的某个函数”被替换为“log(n)”。
所以,你的问题告诉你考虑二分搜索,所以让我们考虑一下。假设您有一个按升序排列的 N 个元素的列表。您想查明该列表中是否存在某个给定的数字。一种不是二分搜索的方法是扫描列表中的每个元素,看看它是否是您的目标数字。您可能会很幸运,第一次尝试就找到了它。但在最坏的情况下,你会检查 N 次不同的时间。这不是二分搜索,也不是 log(N) 的大哦,因为没有办法强制它符合我们上面列出的标准。
您可以选择任意常数 c=10,如果您的列表有 N=32 个元素,那就没问题:10*log(32) = 50,这大于 32 的运行时间。但是如果 N=64 , 10*log(64) = 60,小于 64 的运行时间。您可以选择 c=100,或 1000,或无数,您仍然可以找到一些违反该要求的 N 个。换句话说,不存在N_0。
但是,如果我们进行二分搜索,我们会选择中间的元素并进行比较。然后我们扔掉一半的数字,然后一遍又一遍地重复,依此类推。如果你的N=32,你只能做大约5次,即log(32)。如果你的 N=64,你只能这样做大约 6 次,等等。现在你可以选择任意常数 c,这样对于大的 N 值总是满足要求
。所有这些背景,O(log(N)) 通常意味着你有某种方法可以做一件简单的事情,这可以将你的问题大小减少一半。就像上面的二分查找一样。一旦你把问题切成两半,你就可以一次又一次地把它切成两半。但是,至关重要的是,您不能做的是一些需要比 O(log(N)) 时间更长的预处理步骤。例如,你不能将两个列表打乱成一个大列表,除非你也能找到一种方法在 O(log(N)) 时间内做到这一点。
(注意:几乎总是,Log(N) 表示以二为底的对数,这就是我上面的假设。)
When we talk about big-Oh descriptions, we are usually talking about the time it takes to solve problems of a given size. And usually, for simple problems, that size is just characterized by the number of input elements, and that's usually called n, or N. (Obviously that's not always true-- problems with graphs are often characterized in numbers of vertices, V, and number of edges, E; but for now, we'll talk about lists of objects, with N objects in the lists.)
We say that a problem "is big-Oh of (some function of N)" if and only if:
For all N > some arbitrary N_0, there is some constant c, such that the runtime of the algorithm is less than that constant c times (some function of N.)
In other words, don't think about small problems where the "constant overhead" of setting up the problem matters, think about big problems. And when thinking about big problems, big-Oh of (some function of N) means that the run-time is still always less than some constant times that function. Always.
In short, that function is an upper bound, up to a constant factor.
So, "big-Oh of log(n)" means the same thing that I said above, except "some function of N" is replaced with "log(n)."
So, your problem tells you to think about binary search, so let's think about that. Let's assume you have, say, a list of N elements that are sorted in increasing order. You want to find out if some given number exists in that list. One way to do that which is not a binary search is to just scan each element of the list and see if it's your target number. You might get lucky and find it on the first try. But in the worst case, you'll check N different times. This is not binary search, and it is not big-Oh of log(N) because there's no way to force it into the criteria we sketched out above.
You can pick that arbitrary constant to be c=10, and if your list has N=32 elements, you're fine: 10*log(32) = 50, which is greater than the runtime of 32. But if N=64, 10*log(64) = 60, which is less than the runtime of 64. You can pick c=100, or 1000, or a gazillion, and you'll still be able to find some N that violates that requirement. In other words, there is no N_0.
If we do a binary search, though, we pick the middle element, and make a comparison. Then we throw out half the numbers, and do it again, and again, and so on. If your N=32, you can only do that about 5 times, which is log(32). If your N=64, you can only do this about 6 times, etc. Now you can pick that arbitrary constant c, in such a way that the requirement is always met for large values of N.
With all that background, what O(log(N)) usually means is that you have some way to do a simple thing, which cuts your problem size in half. Just like the binary search is doing above. Once you cut the problem in half, you can cut it in half again, and again, and again. But, critically, what you can't do is some preprocessing step that would take longer than that O(log(N)) time. So for instance, you can't shuffle your two lists into one big list, unless you can find a way to do that in O(log(N)) time, too.
(NOTE: Nearly always, Log(N) means log-base-two, which is what I assume above.)
在以下解决方案中,所有具有递归调用的行都在
X 和 Y 子数组给定大小的一半。
其他线路在恒定时间内完成。
递归函数为T(2n)=T(2n/2)+c=T(n)+c=O(lg(2n))=O(lgn)。
您从 MEDIAN(X, 1, n, Y, 1, n) 开始。
In the following solution, all the lines with a recursive call are done on
half of the given sizes of the sub-arrays of X and Y.
Other lines are done in a constant time.
The recursive function is T(2n)=T(2n/2)+c=T(n)+c=O(lg(2n))=O(lgn).
You start with MEDIAN(X, 1, n, Y, 1, n).
Log 项在算法复杂度分析中经常出现。以下是一些解释:
1.如何表示一个数字?
让我们以数字 X = 245436 为例。“245436”的这种表示法包含隐含的信息。明确该信息:
这是数字的十进制扩展。因此,表示这个数字所需的最少信息量是6位。这并非巧合,因为任何小于 10^d 的数字都可以用 d 位数字表示。
那么需要多少位数字来表示X呢?这等于 X 中 10 的最大指数加 1。
另请注意,这是表示此范围内的数字的最简洁方法。任何减少都会导致信息丢失,因为丢失的数字可以映射到其他 10 个数字。例如:12* 可以映射到 120, 121, 122, …, 129。
2.如何在 (0, N - 1) 中搜索数字?
取 N = 10^d,我们使用最重要的观察结果:
这意味着,当要求在整数行上搜索范围从 0 到 N - 1 的数字时,我们需要至少 log(N) 尝试找到它。为什么?任何搜索算法在搜索号码时都需要选择一个又一个数字。
它需要选择的最少位数是log(N)。因此,在大小为 N 的空间中搜索数字所需的最少操作次数是 log(N)。
你能猜出二分查找、三元查找或十进制查找的顺序复杂度吗?
O(log(N))!
<强>3。如何对一组数字进行排序?
当要求将一组数字 A 排序到数组 B 中时,它看起来像这样 ->
置换元素
原始数组中的每个元素都必须映射到排序数组中对应的索引。因此,对于第一个元素,我们有 n 个位置。为了正确找到 0 到 n - 1 范围内的相应索引,我们需要…log(n) 次操作。
下一个元素需要 log(n-1) 次操作,下一个元素需要 log(n-2) 次操作,依此类推。总数为:
这可以近似到nlog(n) - n。
这是 O(n*log(n))!
因此我们得出结论,没有比 O(n*log(n)) 做得更好的排序算法。具有这种复杂性的一些算法是流行的归并排序和堆排序!
这些是我们在算法复杂性分析中经常看到 log(n) 出现的一些原因。同样可以扩展到二进制数。我在这里制作了一个视频。
为什么在算法复杂度分析中log(n)出现得如此频繁?
干杯!
The Log term pops up very often in algorithm complexity analysis. Here are some explanations:
1. How do you represent a number?
Lets take the number X = 245436. This notation of “245436” has implicit information in it. Making that information explicit:
Which is the decimal expansion of the number. So, the minimum amount of information we need to represent this number is 6 digits. This is no coincidence, as any number less than 10^d can be represented in d digits.
So how many digits are required to represent X? Thats equal to the largest exponent of 10 in X plus 1.
Also note that this is the most concise way to denote the number in this range. Any reduction will lead to information loss, as a missing digit can be mapped to 10 other numbers. For example: 12* can be mapped to 120, 121, 122, …, 129.
2. How do you search for a number in (0, N - 1)?
Taking N = 10^d, we use our most important observation:
This implies that, when asked to search for a number on the integer line, ranging from 0 to N - 1, we need at least log(N) tries to find it. Why? Any search algorithm will need to choose one digit after another in its search for the number.
The minimum number of digits it needs to choose is log(N). Hence the minimum number of operations taken to search for a number in a space of size N is log(N).
Can you guess the order complexities of binary search, ternary search or deca search?
Its O(log(N))!
3. How do you sort a set of numbers?
When asked to sort a set of numbers A into an array B, here’s what it looks like ->
Permute Elements
Every element in the original array has to be mapped to it’s corresponding index in the sorted array. So, for the first element, we have n positions. To correctly find the corresponding index in this range from 0 to n - 1, we need…log(n) operations.
The next element needs log(n-1) operations, the next log(n-2) and so on. The total comes to be:
This can be approximated to nlog(n) - n.
Which is O(n*log(n))!
Hence we conclude that there can be no sorting algorithm that can do better than O(n*log(n)). And some algorithms having this complexity are the popular Merge Sort and Heap Sort!
These are some of the reasons why we see log(n) pop up so often in the complexity analysis of algorithms. The same can be extended to binary numbers. I made a video on that here.
Why does log(n) appear so often during algorithm complexity analysis?
Cheers!
当解决方案基于 n 次迭代时,我们将时间复杂度称为 O(log n),其中每次迭代中完成的工作是前一次迭代的一小部分,因为算法致力于解决方案。
We call the time complexity O(log n), when the solution is based on iterations over n, where the work done in each iteration is a fraction of the previous iteration, as the algorithm works towards the solution.
还不能发表评论……它是死灵!
Avi Cohen 的答案不正确,请尝试:
没有一个条件为真,因此 MEDIAN(X, p, q, Y, j, k) 将削减两个五。这些是非递减序列,并非所有值都是不同的。
还可以尝试使用不同值的偶数长度示例:
现在 MEDIAN(X, p, q, Y, j+1, k) 将削减四个。
相反,我提供这个算法,用 MEDIAN(1,n,1,n) 调用它:
Can't comment yet... necro it is!
Avi Cohen's answer is incorrect, try:
None of the conditions are true, so MEDIAN(X, p, q, Y, j, k) will cut both the fives. These are nondecreasing sequences, not all values are distinct.
Also try this even-length example with distinct values:
Now MEDIAN(X, p, q, Y, j+1, k) will cut the four.
Instead I offer this algorithm, call it with MEDIAN(1,n,1,n):