【问题标题】:Time complexity of divide step in merge sort applied to linked list归并排序中除步的时间复杂度应用于链表
【发布时间】:2021-08-09 14:53:13
【问题描述】:

我一直在研究合并排序在链表中的应用。我看过的一些文章吹捧归并排序是对链表进行排序的最佳算法。对于分而治之策略中的征服部分来说,合并两个排序的链表是有意义的,因为最终节省了所需的内存(与数组相比)。但是,我不明白的是算法中除法步骤的时间复杂度。

对于数组,此步骤是通过利用随机访问并将数组拆分为更小的块来固定时间的。但是,对于一个链表,这不是需要额外的 O(n) 吗?我见过 Floyd 的算法(龟兔)用于找到链表的中点并将问题分成更小的块。我对除法步骤做了一些分析。假设链表的大小为n,那么刚刚划分问题所涉及的操作数如下,

n/2 + n/4 * 2 + n/8 * 4 + ... = n/2 * log(n)

从上面看来,与数组的情况相比,弗洛伊德的算法出现了一个额外的“n”因子。因此,最终的时间复杂度为 O(n^2 * log(n))。有人可以解释一下差异吗?

编辑:根据@Yves 的评论,我发现了错误,

当应该添加时,我在将已排序的块从下到上合并的同时增加了工作量。因此,净时间为:nlogn/2 + nlogn = O(nlogn),

这可能是对上述问题的最有效答案;其他答案有点间接/不提供解释

【问题讨论】:

  • 你的 n log(n)/2 如何变成 O(n² log(n)) ?
  • @Yves 谢谢!分析有误,望指正

标签: algorithm linked-list mergesort


【解决方案1】:

您的问题的问题是,为每个递归级别扫描半个子列表的额外 O(n/2) 时间复杂度转化为 O((0.5 n log(n) + 1.0 (n log(n)) = O(1.5 n log(n)),不是 O(n^2 (log(n))),而 O(1.5 (n log(n))) 转换为 O( n log(n)), 因为时间复杂度忽略了低阶项或常数. 但是在我对具有分散节点的大型列表的实际测试中, 大多数节点访问导致缓存未命中, 我的基准测试显示了递归与相对时间复杂度迭代为 O(1.4 n log(n)),使用基于计数的扫描来拆分列表,而不是龟兔方法。

对于递归版本,使用龟兔方法相对较慢,可以通过使用节点数来改进,如果链表容器不维护节点数,则可能需要一次扫描n个节点(例如 C++ std::list::size())。这减少了通过链表运行将单个指针推进一半(子计数/ 2)的开销。

示例 C/C++ 代码:

Time taken to sort numbers in Linked List

但是,在这种情况下(大列表,分散的节点),将数据从列表中复制到数组中,对数组进行排序,然后从排序后的数组中创建一个新的排序列表,会更快。这是因为数组中的元素是按顺序合并的(不是通过随机链表下一个指针),这是缓存友好的。

【讨论】:

  • 能否请您详细说明,不确定我是否理解“时间复杂度是一样的,但那是忽略常数因素”,和什么一样?
  • @SahilGupta - 我更新了我的答案,两者的时间复杂度都是 O(n log(n)),但自上而下比自下而上慢线性因子 O(n)。自上而下的递归级别数与自下而上使用的数组条目数大致相同,但自下而上没有扫描到中点,因此线性因子 O(n) 或 ~O 更快(n/2) 如果包括常量。
  • 您的回答/评论在理解合并排序在链表中的应用方面很有见地,但没有回答我原来的问题/我误解了
  • @SahilGupta - 我更新了我的答案。我希望它现在能回答你的问题。
【解决方案2】:

在自上而下的归并排序中,找到拆分列表的位置所花费的时间与要拆分的列表的长度成正比。在每个递归级别,这些总时间与所有子列表的长度成正比,即:与原始列表的长度成正比。递归级数为 log2(length(L)),因此拆分列表所涉及的总体时间复杂度为 O(length(L) * log2(length(L))) 与合并阶段的复杂性相同。

在自底向上合并排序中,列表一次拆分一个元素,这个单例要么存储到子列表数组的第一个元素中,要么与它合并以存储到第二个元素等。因此只拆分列表增加了线性时间,这就解释了为什么自下而上的合并通常比自上而下更快。影响实现效率的其他因素是缓存友好性,通常在自下而上和平衡长度中更好,而在自下而上的归并排序中更难实现。

以下是列表自下而上合并排序的简单示例:

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct list {
    struct list *next;
    int value;
} list;

list *list_merge(list *a, list *b) {
    list *head, **tailp = &head;
    while (a && b) {
        if (a->value <= b->value) {
            *tailp = a;
            a = a->next;
        } else {
            *tailp = b;
            b = b->next;
        }
        tailp = &(*tailp)->next;
    }
    *tailp = a ? a : b;
    return head;
}

list *list_mergesort(list *p) {
    list *a[sizeof(size_t) * CHAR_BIT];
    list *e;
    size_t i, top = 0;

    while (p) {
        e = p;
        p = p->next;
        e->next = NULL;
        for (i = 0;; i++) {
            if (i == top) {
                a[top++] = e;
                break;
            }
            if (a[i] == NULL) {
                a[i] = e;
                break;
            }
            e = list_merge(a[i], e);
            a[i] = NULL;
        }
    }
    e = NULL;
    for (i = 0; i < top; i++) {
        e = list_merge(a[i], e);
    }
    return e;
}

list *list_append(list **headp, int value) {
    list *e = malloc(sizeof *e);
    if (e) {
        e->next = NULL;
        e->value = value;
        while (*headp)
            headp = &(*headp)->next;
        *headp = e;
    }
    return e;
}

void list_print(list *e) {
    while (e) {
        printf("%d%c", e->value, " \n"[!e->next]);
        e = e->next;
    }
}

void list_free(list *head) {
    while (head) {
        list *e = head;
        head = head->next;
        free(e);
    }
}

int main() {
    list *head = NULL;
    for (int i = 0; i < 10; i++)
        list_append(&head, rand());
    head = list_mergesort(head);
    list_print(head);
    list_free(head);
    return 0;
}

【讨论】:

    猜你喜欢
    • 2015-07-22
    • 2023-01-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-03
    • 2014-08-02
    • 1970-01-01
    • 2016-03-03
    相关资源
    最近更新 更多