【问题标题】:Merge sort time and space complexity合并排序时间和空间复杂度
【发布时间】:2012-05-07 17:52:27
【问题描述】:

我们以这种合并排序的实现为例

void mergesort(Item a[], int l, int r) {
if (r <= l) return;
int m = (r+l)/2;
mergesort(a, l, m);   ------------(1)
mergesort(a, m+1, r); ------------(2)
merge(a, l, m, r);

a) 这个合并排序的时间复杂度是O(n lg(n))。并行化(1)和(2)会带来任何实际收益吗?从理论上讲,似乎在将它们并行化之后,您最终也会进入O(n lg(n))。但实际上我们能获得任何收益吗?

b) 这里合并排序的空间复杂度为O(n)。但是,如果我选择使用链表执行就地合并排序(不确定是否可以合理地使用数组),空间复杂度是否会变为O(lg(n)),因为您必须考虑递归堆栈帧的大小? 我们可以将O(lg(n)) 视为常数,因为它不能超过 64?我可能在几个地方误解了这一点。 64到底有什么意义?

c) Sorting Algorithms Compared - Cprogramming.com 说合并排序需要使用链表的常量空间。如何?他们对待O(lg(n)) 不变吗?

d) 为了更清晰而添加。 对于空间复杂度计算,假设输入数组或列表已经在内存中是否公平?当我进行复杂性计算时,我总是计算除了输入已经占用的空间之外我需要的“额外”空间。否则空间复杂度将始终为O(n) 或更差。

【问题讨论】:

标签: algorithm time-complexity mergesort space-complexity


【解决方案1】:

简单而聪明的思维。

总水平 (L) = log2(N)。 最后一级节点数=N。

第 1 步:让我们假设所有级别 (i) 的节点 = x(i)。

第二步:所以时间复杂度 = x1 + x2 + x3 + x4 + .... + x(L-1) + N(for i = L);

第三步:我们知道的事实,x1,x2,x3,x4...,x(L-1)

第 4 步:让我们考虑 x1=x2=x3=...=x(L-1)=N

第五步:所以时间复杂度 = (N+N+N+..(L)times)

时间复杂度 = O(N*L); 放 L = log(N);

时间复杂度 = O(N*log(N))

我们在合并时使用额外的数组,

空间复杂度:O(N)。

提示:大 O(x) 时间意味着,x 是我们可以肯定地说在平均情况下它永远不会超过 x 的最小时间

【讨论】:

    【解决方案2】:

    对于最好和最坏的情况,复杂度都是 O(nlog(n)) 。 虽然每一步都需要额外的 N 大小的数组,所以 空间复杂度是 O(n+n) 是 O(2n),因为我们移除了计算复杂度的常数值,所以它是 O(n)

    【讨论】:

      【解决方案3】:

      合并排序空间复杂度为O(nlogn),考虑到它最多可以进行O(logn) 递归,这是非常明显的,并且对于每个递归,还有O(n) 的额外空间用于存储需要合并的数组重新分配。 对于那些说O(n) 的人,请不要忘记它是O(n) 用于到达堆栈帧深度。

      【讨论】:

      • 在每个递归步骤之后,合并的数组不会被垃圾收集吗?它应该是 O(n) 空间复杂度而不是 O(nlogn) 那么
      【解决方案4】:

      归并排序的最坏情况:O(n log n), 合并排序的最佳情况:O(n log n) 通常,O(n) 自然变体, 归并排序的平均性能:O(n log n), 归并排序的最坏情况空间复杂度:О(n) 总,O(n) 辅助

      【讨论】:

        【解决方案5】:

        MergeSort 时间复杂度是 O(nlgn),这是一个基础知识。 合并排序空间复杂度将始终为 O(n),包括数组。 如果你把空间树画出来,看起来空间复杂度是 O(nlgn)。但是,由于代码是深度优先代码,您将始终只沿着树的一个分支扩展,因此,所需的总空间使用将始终以 O(3n) = O(n) 为界。

        比如把空间树画出来,好像是O(nlgn)

                                     16                                 | 16
                                    /  \                              
                                   /    \
                                  /      \
                                 /        \
                                8          8                            | 16
                               / \        / \
                              /   \      /   \
                             4     4    4     4                         | 16
                            / \   / \  / \   / \
                           2   2 2   2.....................             | 16
                          / \  /\ ........................
                         1  1  1 1 1 1 1 1 1 1 1 1 1 1 1 1              | 16
        

        其中树的高度为 O(logn) => 空间复杂度为 O(nlogn + n) = O(nlogn)。 但是,实际代码中并非如此,因为它不是并行执行的。例如,在 N = 16 的情况下,这就是合并排序代码的执行方式。 N = 16。

                                   16
                                  /
                                 8
                                /
                               4
                             /
                            2
                           / \
                          1   1
        

        注意使用的空间数是 32 = 2n = 2*16

        然后向上合并

                                   16
                                  /
                                 8
                                /
                               4
                             /  \
                            2    2
                                / \                
                               1   1
        

        即 34

                                   16
                                  /
                                 8
                                / \
                               4   4
                                  /
                                 2
                                / \ 
                               1   1
        

        36

        然后向上合并

                                   16
                                  / \
                                 8  8
                                   / \
                                  4   4
                                     / \
                                    2   2
                                        /\
                                       1  1
        

        16 + 16 + 14 = 46

        在更大的情况下,n = 64

                             64
                            /  \
                           32  32
                               / \
                              16  16
                                  / \
                                 8  8
                                   / \
                                  4   4
                                     / \
                                    2   2
                                        /\
                                       1  1
        

        这是 64*3

        您可以通过对一般情况的归纳来证明这一点。

        因此,空间复杂度总是以 O(3n) = O(n) 为界,即使您使用数组实现,只要您在合并后清理已用空间并且不是并行执行代码而是顺序执行代码。

        我的实现示例如下:

        templace<class X> 
        void mergesort(X a[], int n) // X is a type using templates
        {
            if (n==1)
            {
                return;
            }
            int q, p;
            q = n/2;
            p = n/2;
            //if(n % 2 == 1) p++; // increment by 1
            if(n & 0x1) p++; // increment by 1
                // note: doing and operator is much faster in hardware than calculating the mod (%)
            X b[q];
        
            int i = 0;
            for (i = 0; i < q; i++)
            {
                b[i] = a[i];
            }
            mergesort(b, i);
            // do mergesort here to save space
            // http://stackoverflow.com/questions/10342890/merge-sort-time-and-space-complexity/28641693#28641693
            // After returning from previous mergesort only do you create the next array.
            X c[p];
            int k = 0;
            for (int j = q; j < n; j++)
            {
                c[k] = a[j];
                k++;
            }
            mergesort(c, k);
            int r, s, t;
            t = 0; r = 0; s = 0;
            while( (r!= q) && (s != p))
            {
                if (b[r] <= c[s])
                {
                    a[t] = b[r];
                    r++;
                }
                else
                {
                    a[t] = c[s];
                    s++;
                }
                t++;
            }
            if (r==q)
            {
                while(s!=p)
                {
                    a[t] = c[s];
                    s++;
                    t++;
                }
            }
            else
            {
                while(r != q)
                {
                    a[t] = b[r];
                    r++;
                    t++;
                }
            }
            return;
        }
        

        希望这会有所帮助!=)

        宋志龙,

        多伦多大学

        【讨论】:

        • @CheeLoongSoon 你从哪里得到 3n?
        【解决方案6】:

        a) 是的 - 在一个完美的世界中,您必须进行 log n 大小为 n、n/2、n/4 ... 的合并(或者更好的说法是 1、2、3 ... n/4, n/2, n - 它们不能并行化),这给出了 O(n)。它仍然是 O(n log n)。在不太完美的世界中,您没有无限数量的处理器,上下文切换和同步会抵消任何潜在收益。

        b) 空间复杂度始终为 Ω(n),因为您必须将元素存储在某处。额外的空间复杂度在使用数组的实现中可能是 O(n),在链表实现中可能是 O(1)。在实践中,使用列表的实现需要额外的空间来存放列表指针,因此除非您已经在内存中拥有列表,否则这无关紧要。

        编辑 如果你计算堆栈帧,那么它是 O(n)+ O(log n) ,所以在数组的情况下仍然是 O(n) 。如果是列表,则需要 O(log n) 额外内存。

        c) 列表只需要在合并过程中更改一些指针。这需要不断增加内存。

        d) 这就是为什么在合并排序复杂性分析中人们会提到“额外的空间要求”或类似的东西。很明显,您必须将元素存储在某个地方,但最好提及“附加内存”以阻止纯粹主义者。

        【讨论】:

        • 我们是否需要考虑输入数组或列表已经占用的空间?请参阅我的 (d) 部分。此外,您为什么不考虑将在每个递归调用上分配的堆栈帧大小。这将解释 O(lg(n))
        • @FrankQ。从纯粹主义者的角度来看,是的。什么时候可以从上下文中推断是没有必要的,但是如果有人因为没有提及它而在考试中被扣分,我不会感到惊讶。确实是堆栈帧,它们通常被省略,但数量很多,但在数组实现中仍然是 O(n)。
        • 你能解释一下堆栈帧的大小req是O(log n)吗?在每个级别,没有。递归调用的次数是 2^j,所以堆栈帧的数量不应该是 1 + 2 + 4 + .. + 2^((log n)+1) 吗?我知道我错过了一些东西,无法弄清楚。类似的是我对额外数组空间的怀疑。在级别 0,我们合并到大小为 n 的数组,在级别 1,我们合并两个大小为 n/2 的数组,因此总分配 = 2*n/2 = n。那么如果我们这样分析,应该是 b n + n (log n 次) = (n(log n)) 我的这个分析有什么缺陷?
        • @soulcheck 有时,您的意思是在特定的递归调用中?而且由于以后可以使用相同的内存,我们只查看一次分配的最大大小(即递归树的根)?我无法理解答案的另一部分。有 O(logn) 级别,但每个级别都会进行 2^(levelnumber) 递归调用,对吗? root 将需要 1 个堆栈帧,但由于它对每一半进行递归调用,因此两半都需要存储在堆栈帧中,从而在第 1 级要求 2^1 等最后一级,它将是 2^logn ?
        • @AyushChaudhary 对不起,你是对的。不得不再次把我的头绕过去。无论如何,它仍然是 O(n)。我会更正答案。
        【解决方案7】:

        a) 是的,当然,并行化归并排序是非常有益的。它仍然是 nlogn,但您的常数应该会显着降低。

        b) 链表的空间复杂度应该是 O(n),或者更具体地说是 O(n) + O(logn)。请注意,这是一个 +,而不是 *。在进行渐近分析时不要太在意常数。

        c) 在渐近分析中,只有方程中的主导项很重要,所以我们有一个 + 而不是 * 的事实使它成为 O(n)。如果我们全部复制子列表,我相信这将是 O(nlogn) 空间 - 但是基于智能链表的合并排序可以共享列表的区域。

        【讨论】:

        • 对于 (b) 链表的空间复杂度不是 O(n),合并过程中不需要额外的空间进行排序,可以移动指针并就地合并?
        • 你必须将列表的n个元素存储在某处
        • 使用链表时不一定。
        猜你喜欢
        • 2015-07-22
        • 2023-01-18
        • 1970-01-01
        • 2012-08-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-07-23
        • 1970-01-01
        相关资源
        最近更新 更多