【2023年】ArrayListとLinkedListの詳しい紹介と比較

1. 配列リスト

1。概要

        ArrayList は List インターフェイスを実装した動的配列であり、いわゆる動的配列とはサイズが可変であることを意味します。すべてのオプションのリスト操作が実装され、Null を含むすべての要素が許可されます。このクラスは、List インターフェイスの実装に加えて、リストを格納するために内部で使用される配列のサイズを操作するメソッドも提供します。

        各 ArrayList インスタンスには容量があり、これはリスト要素を格納するために使用される配列のサイズです。デフォルトの初期容量は 10 です。デフォルトの初期容量は 10 です。ArrayList 内の要素の数が増加すると、その容量は自動的に増加し続けます。要素が追加されるたびに、ArrayList は拡張操作が必要かどうかを確認します。拡張操作により、データが再び新しい配列にコピーされます。したがって、特定のビジネス データの量がわかれば、初期容量を指定できますこれにより、展開時のコピーの問題が軽減されます。もちろん、多数の要素を追加する前に、アプリケーションはensureCapacity オペレーションを使用して ArrayList インスタンスの容量を増やすこともでき、これにより増分再割り当ての量を減らすことができます。

2. ソースコード分析

ArrayList は List インターフェイスを実装し、最下層は配列を使用して実装されるため、その操作は基本的に配列の操作に基づいています。

2.1. コンストラクター

  • ArrayList(): デフォルトのコンストラクター。初期容量 10 の空のリストを提供します。
  • ArrayList(int initialCapacity): 指定された初期容量を持つ空のリストを構築します。
  • ArrayList(Collection<? extends E> c): 指定されたコレクションの要素を含むリストを、コレクションのイテレータが返す順序で並べて構築します。

    /**
     * 构造一个具有指定初始容量的空列表。
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * 构造一个初始容量为 10 的空列表
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;    //不会立刻创建数组,会在第一次add()时才会创建
    }

    /**
     * 构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 
     * 的迭代器返回它们的顺序排列的。
     */
    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

2.2 一般的な方法

 ArrayList は、add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(intindex, Collection<? extends E> c)、set(int index, E element ) を提供します。 ArrayList を実現する方法が 5 つ増えました。

  • add(): 単一の挿入データ
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 验证是否需要扩容
        elementData[size++] = e;   //往数组最后的位置赋值
        return true; 
    }




    public void add(int index, E element) {
        rangeCheckForAdd(index);    //判断索引位置是否正确

        ensureCapacityInternal(size + 1);  // 验证是否需要扩容
         /*
         * 对源数组进行复制处理(位移),从index + 1到size-index。
         * 主要目的就是空出index位置供数据插入,
         * 即向右移动当前位于该位置的元素以及所有后续元素。 
         */
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);  
        //给指定下标的位置赋值
        elementData[index] = element;
        size++;
    }

このメソッドの最も基本的なメソッドは System.arraycopy() メソッドです。このメソッドの基本的な目的は、新しいデータを挿入するためにインデックス位置を空けることです。ここでは配列データの右シフトが必要ですが、これは非常に面倒で、時間がかかるため、この方法で要素を追加することは通常はお勧めできません。指定した中間位置に多数の挿入 (中間挿入) を実行する必要がある場合は、LinkedList を使用することをお勧めします。

  • addAll(): データをバッチで挿入します
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();  //将集合c 转换成数组
        int numNew = a.length;  
        ensureCapacityInternal(size + numNew);  // 扩容(当前集合长度+c集合长度)

        //同上,主要是采用该方法把C集合转为数组后的数据进行复制在插入到当前集合的末尾
        System.arraycopy(a, 0, elementData, size, numNew);   
        size += numNew;
        return numNew != 0;
    }


    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);  //判断下标位置是否正确

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // 扩容(当前集合长度+c集合长度)
 
        int numMoved = size - index;
//       如果位置不是集合尾部,则需要先把数据向右移动指定长度(添加数据的长度)
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);
//       然后再把数据加入到指定下标位置
        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

        このメソッドは、System.arraycopy() メソッドを使用して、C コレクション内のデータ (最初に配列に変換されたもの) を elementData 配列にコピーすることに他なりません。このメソッドは以下で広範囲に使用されるため、ここで System.arraycopy() について簡単に紹介します。

        このメソッドのプロトタイプは、public static void  arraycopy (Object src, int srcPos, Object dest, int destPos, int length) です。その基本的な目的は、配列要素をコピーすることです。

        つまり、指定されたソース配列から、指定された位置から開始してターゲット配列の指定された位置で終了する配列をコピーします。ソース配列 src を srcPos 位置から dest 配列にコピーします。コピー長は length で、データは dest の destPos 位置から貼り付けられます。

  • get(): 指定された添え字を持つ要素を検索します。
        public E get(int index) {
            rangeCheck(index);
            checkForComodification();
            return ArrayList.this.elementData(offset + index);
        }

ArrayList は、ArrayList 内の要素を読み取るための get(int Index) を提供します。ArrayList は動的配列であるため、添字に従って ArrayList 内の要素を取得できるため、計算量は O(1) であり、格納/取得の速度は比較的高速です。ただし、検索の場合、比較のために結果を 1 つずつたどる必要があるため、要素の挿入と削除はあまり効率的ではありません。したがって、時間計算量は O(n) になります。

2.3. 拡張

        上記の新しく追加されたメソッドのソース コードでは、ArrayList の展開メソッドである ensureCapacity() の各メソッドにこのメソッドが存在することがわかりました。前述したように、ArrayList は要素を追加するたびに容量の検出と判定を行う必要があり、要素追加後の要素数が ArrayList の容量を超える場合には、新たに追加された要素のニーズを満たすために拡張操作が行われます。

        したがって、ビジネス データの量が明確にわかっている場合、または多数の要素を挿入する必要がある場合は、コレクションを再度作成するときに容量を直接指定するか、ensureCapacity を使用して ArrayList インスタンスの容量を手動で増やして、増分の数を減らすことができます。再割り当て。

  • ソース実装:
   //minCapacity :所需的最小容量
 private void grow(int minCapacity) {
        // 集合长度
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);   //增加原来的1.5倍
//        判断所需要的最小容量大于增加原来的1.5倍的长度,则容量扩大为minCapacity
        if (newCapacity - minCapacity < 0)  
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)  //判断是否会大于虚拟机的最大容量
            newCapacity = hugeCapacity(minCapacity);
        // 拷贝数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

拡張ロジックは次のとおりです。 

  • ArrayList() をパラメーターなしで呼び出す場合、初期容量は 0 です。パラメーターを使用して呼び出す場合は、指定したパラメーターのサイズになります。コレクションを呼び出す場合は、コレクションのサイズになります。
  • 要素が追加されると拡張が行われ、容量10の配列が拡張され、コレクションの要素数が10に達すると2回目の拡張が行われ、2回目の拡張で1.5倍に拡張されます。前の配列の長さ (基礎となるロジックは 15>>1=7+15=22) まず右にシフトしてから、元の配列の長さを加算します。

二、リンクリスト

1。概要

Linkedlist は、リンク リストの動的配列 (二重リンク リスト) に基づいています。

  • スタック (後入れ先出し)、キュー (先入れ先出し)、またはデキューとして操作できます。
  • データの追加と削除はポインタを変更するだけで効率的ですが、データへのアクセスの平均効率は低く、リンク リストを走査する必要があります。同期されておらず、スレッドセーフではありません。
  • null 要素をサポート、順序、要素を繰り返すことができます
  • LinkedList を走査するのに通常の for ループを使用せず、イテレータまたは foreach ループ (foreach ループの原理はイテレータです) を使用して LinkedList を走査します。
  • この方法では、アドレスに従ってデータを直接検索するため、LinkedList のトラバース効率が大幅に向上します。

2. 単連結リストと二重連結リスト

2.1、一方向リンクリスト

  • リンクリストの各要素をノード(Node)と呼びます。
  • 物理ストレージユニット上の非連続かつ非順次のストレージ構造

データ: データフィールド、データを格納

next: 次のノードの記憶場所を指すポインター フィールド

時間計算量の分析:

クエリ操作

  • ヘッド ノードにクエリを実行する場合リンク リストをトラバースする必要はなく、時間計算量は O(1) です。
  • 他のノードをクエリするには、リンク リストを走査する必要があり、時間計算量は O(n) です

時間計算量の追加と削除

  • ヘッド ノードを追加および削除する場合にのみ、リンク リストをたどる必要はなく、時間計算量は O(1) です。
  • 他のノードを追加または削除するには、ノードを追加または削除する前にリンク リストを走査して対応するノードを見つける必要があります

 2.2. 二重リンクリスト

二重リンク リストは、その名前が示すように、2 つの方向をサポートします。

  • 各ノードには、後続ノードを指す複数の後続ポインタがあります。
  • 前のノードを指す先行ポインタ prev があります

前のノードを取得する必要がある場合は、 prev を呼び出すだけでよく、次のノードを呼び出すには next ポインターを呼び出すだけです。

一方向リンクリストを比較する:

  • 二重リンク リストには、後続ノードと先行ノードのアドレスを格納するために 2 つの追加スペースが必要です。
  • 双方向トラバーサルをサポートし、二重リンクリスト操作をより柔軟にします。

時間計算量の分析:

お問い合わせ:

  • 先頭ノードと末尾ノードのクエリの時間計算量は O(1) です。
  • クエリ時間の平均複雑さは O(n) です
  • 特定のノードの先行ノードを見つける時間計算量は O(1) です。

追加と削除:

  • 先頭ノードと末尾ノードの追加と削除の時間計算量は O(1) です。
  • 他のノードの追加と削除の時間計算量は O(n) です
  • 特定のノードの追加と削除の時間計算量は O(1) です。

3. ArrayList と LinkedList の違い

1. 基礎となるデータストレージ構造の比較

  •  ArrayList は動的配列に基づくデータ構造であり、連続メモリとして保存されます。
  • LinkedList は二重リンク リストに基づくデータ構造であり、そのメモリ ストレージは非連続です。

2. 運用データの効率化:

  • 検索: 検索の比較 ArrayList 基底層は配列の連続性に基づいているため、RandomAccess と呼ばれるインターフェイスが実装されており、検索時にアドレス指定式を介して添字に基づいて検索されますが、LinkedList の基底層は二重リンクリストの場合、このインターフェースは実装されていません。要素を検索するときは、 next() イテレータを使用して 1 つずつ反復処理することしかできません。
  • 追加と削除: ArrayList の末尾の挿入パフォーマンスは速くなりますが、先頭位置に挿入する場合は各配列要素を後方に移動する必要があるため、他の部分の挿入パフォーマンスは遅くなりますが、LinkedList は先頭と末尾に挿入されます。リンクされたリスト構造 削除する場合、検索して見つける必要はありません。削除および追加する場合、他の要素は関与しません。必要なのはポインターの指す位置を変更するだけです。すべてが高速ですが、中間の削除と変更の場合は、要素を使用する場合は、最初に変更する要素の位置を特定する必要があり、その位置決めには時間がかかり、すべてのパフォーマンスが低下します。
  • ただし、未知のインデックスを探している場合は、ArrayList も走査する必要があり、時間計算量は O(n) になります。

3. 占有メモリ空間:

  • ArrayList の最下層は配列であるため、メモリは連続的であり、メモリを節約します。
  • LinkedList は二重リンク リストであり、データと 2 つのポインターを保存する必要があり、より多くのメモリを消費します。
  • さらに、ArrayList は CPU バッファーの局所性原理を利用でき、ロード時に隣接するすべての要素が一度に CPU バッファーにロードされ、メモリの読み取りを最初に CPU バッファーで読み取ることができるため、効果的に実行を向上させることができます。 LinkedList はリンクされたリストであるため、要素はポインタによって示されるだけであり、隣接することはできないため、この機能を有効に活用することはできません。

4. スレッドの安全性:

  • ArrayList も LinkedList もスレッドセーフではありません
  • スレッドセーフを実現する必要がある場合は、Collections.synchronizedList() を通じてパッケージを作成できます。
    • Collections.synchronizedList(new LinkedList<>())
    • Collections.synchronizedList(new ArrayList<>())
 
 
 
 
 
 

おすすめ

転載: blog.csdn.net/weixin_52315708/article/details/131853639