簡體   English   中英

為什么鏈表刪除和插入操作的復雜度為 O(1) ? 不應該是 O(n)

[英]Why does linked list delete and insert operation have complexity of O(1) ? shouldn't it be of O(n)

據說 LinkedList 刪除和添加操作的復雜度是O(1) ArrayList情況下,它是O(n)

計算大小為“M”的 ArrayList :如果我想刪除第 N 個位置的元素,那么我可以使用索引一次性直接轉到第 N 個位置(我不必遍歷到第 N 個索引)然后我可以刪除元素,直到此時,復雜度為 O(1),然后我將不得不移動其余元素(MN 移動),因此我的復雜度將是線性的,即 O(M-N+1)。 因此在最后刪除或插入會給我最好的性能(如 N ~ M),而在開始時刪除或插入將是最差的(如 N ~ 1)。

現在是大小為 "M" 的 LisnkedList :因為我們不能直接到達 LinkedList 中的第 N 個元素,為了訪問第 N 個元素我們必須遍歷 N 個元素,所以 LinkedList 中的搜索比 ArrayList 更昂貴......但是刪除並且在 LinkedList 的情況下,添加操作被稱為 O(1),因為在 LinkedList 中不涉及 Shift,但是涉及 rigth 的遍歷操作? 所以復雜度應該是 O(n),其中最差的性能將出現在尾節點,而最佳性能將出現在頭節點。

誰能解釋一下為什么我們在計算 LinkedList 刪除操作的復雜性時不考慮遍歷成本?

所以我想了解它在 java.util 包中是如何工作的。 如果我想在 C 或 C++ 中實現相同的功能,我將如何實現 O(1) 以在 LinkedList 中進行隨機刪除和插入?

刪除和附加操作在所述的情況下,是O(1)的LinkedList ,因為在LinkedList的移位不參與,但有右涉及橫動動作?

添加到鏈表的任一端不需要遍歷,只要您保留對鏈表兩端的引用。 這就是 Java 為其addaddFirst / addLast方法addFirst

無參數removeremoveFirst / removeLast方法也是如此——它們在列表端操作。

另一方面, remove(int)remove(Object)操作不是 O(1)。 它們需要遍歷,因此您正確地將它們的成本確定為 O(n)。

刪除的復雜性被認為是您已經擁有指向要刪除的元素的正確位置的指針......

不考慮您為找到它所花費的成本

Information on this topic is now available on Wikipedia at: Search data structure

    +----------------------+----------+------------+----------+--------------+
    |                      |  Insert  |   Delete   |  Search  | Space Usage  |
    +----------------------+----------+------------+----------+--------------+
    | Unsorted array       | O(1)     | O(1)       | O(n)     | O(n)         |
    | Value-indexed array  | O(1)     | O(1)       | O(1)     | O(n)         |
    | Sorted array         | O(n)     | O(n)       | O(log n) | O(n)         |
    | Unsorted linked list | O(1)*    | O(1)*      | O(n)     | O(n)         |
    | Sorted linked list   | O(n)*    | O(1)*      | O(n)     | O(n)         |
    | Balanced binary tree | O(log n) | O(log n)   | O(log n) | O(n)         |
    | Heap                 | O(log n) | O(log n)** | O(n)     | O(n)         |
    | Hash table           | O(1)     | O(1)       | O(1)     | O(n)         |
    +----------------------+----------+------------+----------+--------------+

 * The cost to add or delete an element into a known location in the list (i.e. if you have an iterator to the location) is O(1). If you don't know the location, then you need to traverse the list to the location of deletion/insertion, which takes O(n) time. 
** The deletion cost is O(log n) for the minimum or maximum, O(n) for an arbitrary element.

是的,如果您一次性考慮兩個操作(索引和插入),您是正確的。 在這種情況下,這是不正確的,因為當您在鏈表中間插入一個節點時,所采取的假設是您已經在必須插入該節點的地址處。

訪問節點的時間復雜度為 O(n),而僅插入節點的時間復雜度為 O(1)。

在頭部插入需要您添加元素並更新頭部指針。

newnode->next = head;
head = newnode;

在尾部插入需要你保留一個指向尾部元素的指針,在尾部添加元素並更新尾部指針。

tail->next = newnode;
tail = newnode;

刪除 head 元素需要更新 head 並刪除之前的 head 元素。

temp = head;
head = head->next;
delete temp; /* or free(temp); */

以上都是微不足道的操作,不依賴於鏈表中元素的數量。 因此,它們是 O(1)

然而,刪除尾元素將是一個 O(n) 操作,因為即使您可能有尾指針,您仍然需要將設置為新尾節點的倒數第二個節點(通過更新尾指針並設置節點的下一個成員為 NULL)。 為此,您需要遍歷整個鏈表。

penultimate_el = find_penultimate_el(head); /* this is O(n) operation */
delete tail; /* or free(tail) */
tail = penultimate_el;
tail->next = NULL;

ArrayList 提供可調整大小的數組並將“引用”或“指針”存儲到實際存儲 如果數組擴展到超出其分配的大小,則必須重新創建此引用數組。 換句話說,在開頭插入一個節點將需要將所有現有元素向上移動一個,或者如果整個列表超出分配的大小,則需要重新分配整個列表。 這就是為什么插入是O(n)

LinkedList 由一系列節點組成; 每個節點是分開分配的。 因此在插入時,沒有必要遍歷所有節點。 這就是為什么它的復雜度為O(1) 但是,如果您在末尾插入並且您只有第一個節點的引用,那么您可能必須遍歷整個列表,因此在這種情況下的復雜性將為 O(n)。

編輯

如果查看java.util.LinkedList的源代碼,您會發現LinkedList始終跟蹤最后一個元素

以下是來自實際java.util.LinkedList類的一些代碼片段。

package java.util;

...

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;

    /**
     * Pointer to first node.
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     */
    transient Node<E> last;


    ...
    ...


    /**
     * Appends the specified element to the end of this list.
     */
    public boolean add(E e) {
        linkLast(e);
        return true;
    }



    ...
    ...


    /**
     * Links e as last element.
     */
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }


    ...
    ...

}

請特別參閱linkLast()方法。 它不會遍歷整個列表。 它只是在列表的末尾插入元素,這就是時間復雜度為O(1)

在數組中,如果我們從 C 語言的角度來看實現,那么它會讓我們有一個清晰的理解。 雖然我們可以在數組中以恆定時間添加和刪除元素,即我們不需要遍歷整個數組 例如:如果數組是 [1,2,3,4],則 5 可以直接添加到 Arr[arr。 length]位置,同樣我們可以在固定時間刪除元素,要刪除的位置是Arr[arr.length-1],但是讓我們看看你聲明的數組大小是4的場景,然后我們想再添加一個元素我們可以清楚地看到沒有空間可以添加元素,因此我們需要做的是創建一個大小為前一個數組的兩倍的新數組,即大小為 8 ,那么前一個數組的所有元素都必須是復制到此處,新元素添加到第 5 個位置,即 Arr[arr.length] ,因此插入時間復雜度為 O(n),因為它與前一個數組的元素數成正比。

現在來到鏈表,它沒有固定大小(它在堆內存中動態分配)聲明我們可以通過頭和尾指針跟蹤第一個和最后一個位置,因此無論鏈表大小如何,我們只需要更改頭指針和尾,時間復雜度為 O(1) ,最后添加創建一個新節點,將當前尾的鏈接部分更改為這個新節點地址,並將這個新節點作為尾。 對於首先插入我們需要創建新節點, 將此節點的鏈接部分設置為當前頭地址,並最終將此新節點設為頭,因此只需更改一個節點即可在第一個位置添加一個元素,因此時間復雜度為 O(1)。

所有其他答案似乎都是正確的。 我還有一點要補充。 我認為在雙向鏈表中,如果您有該節點的引用,則刪除任何節點的時間復雜度將為 O(1)。 由於它是雙向鏈表並且您手頭有節點的引用,因此您可以直接更新前一個節點的指針,因此刪除帶有引用的節點將只花費 O(1) 時間。 由於雙向鏈表屬性,您不需要遍歷,這就是為什么使用引用刪除節點的時間復雜度為 O(1)。

您可以在此處閱讀更多相關信息: 雙向鏈表元素移除的時間復雜度?

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM