web-dev-qa-db-ja.com

リンクリストの削除と挿入の操作がO(1)の複雑さを持っているのはなぜですか?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よりもコストがかかりますが、削除します。そして、追加操作はO(1)であると言われます。LinkedListの場合、LinkedListにはシフトは含まれませんが、トラバース操作は厳密に含まれるため、複雑さはorder O(n)ここで、最悪のパフォーマンスはテールノードにあり、最高のパフォーマンスはヘッドノードにあります。

LinkedListの削除操作の複雑さを計算する際に、トラバースコストを考慮しないのはなぜでしょうか。

したがって、Java.utilパッケージでどのように機能するかを理解したいと思います。 CまたはC++で同じように実装したい場合、LinkedListでランダムに削除および挿入するためにO(1)をどのように実現しますか?

13
Aditya Agarwal

削除および追加操作は、O(1)の場合と呼ばれます。LinkedListの場合、LinkedListにはシフトは含まれませんが、トラバース操作が含まれますか?

リンクリストの両端への参照を保持している限り、リンクリストの両端に追加する必要はありません。これは、Javaが add および addFirst / addLast メソッドに対して行うことです。

パラメータなしの remove および removeFirst / removeLast メソッドについても同様です-リストの末尾で動作します。

一方、 remove(int) および remove(Object) 操作は、O(1)ではありません。それらはトラバーサルを必要とするため、コストをO(n)として正しく識別しました。

21
dasblinkenlight

削除の複雑さは、削除したい要素の正しい位置へのポインタをすでに持っていると考えられています...

それを見つけるためにかかったコストとは見なされません

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.
6
Rafael Lima

はい、2つの操作(インデックス作成と挿入)を1回で行うと考えると、間違いです。リンクリストの途中にノードを挿入する場合、ノードを挿入する必要があるアドレスにすでにいることが前提となるため、この場合は当てはまりません。

ノードにアクセスする時間の複雑さは、O(n))ですが、ノードの挿入のみがO(1)です。

先頭に挿入するには、要素を追加して、ヘッドポインタを更新する必要があります。

newnode->next = head;
head = newnode;

尾に挿入するには、尾の要素へのポインタを保持し、要素を尾に追加して、尾のポインタを更新する必要があります。

tail->next = newnode;
tail = newnode;

ヘッド要素を削除するには、ヘッドを更新し、以前のヘッド要素を削除する必要があります。

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

上記はすべて簡単な操作であり、リンクされたリスト内の要素の数に依存しません。したがって、これらはO(1)です。

ただし、テールエレメントを削除すると、O(n)操作になります。これは、テールポインターがある場合でも、新しいテールノードとして設定される最後から2番目のノードが必要になるためです。 (テールポインターを更新し、ノードの次のメンバーをNULLに設定することにより)。これを行うには、リンクリスト全体を走査する必要があります。

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

ArrayListは、サイズ変更可能な配列と 実際のストレージへの「参照」または「ポインタ」を格納 を提供します。配列が割り当てられたサイズを超えて拡張された場合、この参照の配列を再作成する必要があります。つまり、最初にノードを挿入するには、既存のすべての要素を1つ上に移動するか、割り当てられたサイズを超えている場合はリスト全体を再割り当てする必要があります。そのため、挿入は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)です。

1
Raman Sahasi

配列では、C言語の観点から実装を見ると、明確な理解が得られます。配列で要素を一定の時間に追加および削除できます。つまり、配列全体を走査する必要はありません。例:配列が[1,2,3,4]の場合、5を直接Arr [arr。 length] postion、同様に、一定の時間に要素を削除できます。削除する位置はArr [arr.length-1]ですが、宣言した配列サイズが4で、さらに1つの要素を追加する場合のシナリオを見てみましょう追加する要素のスペースがないことがはっきりとわかります。したがって、サイズの新しい配列を作成するには、前の配列の2倍、つまりサイズ8としましょう。前の配列のすべての要素は、ここにコピーされ、新しい要素が5番目の位置、つまりArr [arr.length]に追加されます。したがって、挿入時間の複雑さはO(n)=前の配列の要素数に正比例するためです。持っています。

リンクリストに来ると、固定サイズではありません(ヒープメモリに動的に割り当てられます)。先頭と末尾のポインターで最初と最後の位置を追跡できるため、リンクリストのサイズに関係なく、ヘッドのポインターを変更する必要があります。テール、時間の複雑さをO(1)にして、最後に追加するために新しいノードを作成するには、現在のテールのリンク部分をこの新しいノードアドレスに変更し、この新しいノードをテールとして作成します。最初に挿入して、新しいノードを作成し、このノードのリンク部分を現在のヘッドアドレスとして設定し、最後にこの新しいノードをヘッドとして作成し、1つのノードを変更するだけで1番目の位置に要素を追加するため、時間の複雑さがO(1 )。

0
Kunal Hazard