データ構造とアルゴリズム (2): 連結リスト

概要

意味

コンピューター サイエンスでは、リンク リストはデータ要素の線形コレクションであり、それぞれが次の要素を指しており、要素のストレージは連続的ではありません。

コンピューター サイエンスでは、リンク リストはデータ要素の線形コレクションであり、その順序はメモリ内の物理的な配置によって決まりません。代わりに、各要素は次の要素を指します。

[^5] として分類できます。

  • 単一リンクリスト。各要素は次の要素のみを認識します。

[外部リンク画像の転送に失敗しました。ソース サイトには盗難防止リンク メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-DJ6m944t-1679589982443)(.\imgs\image-20221110083407176.png)]

  • 二重リンクリスト。各要素は前の要素と次の要素を認識します。

[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-SZhvWbP1-1679589982443)(.\imgs\image-20221110083427372.png)]

  • 循環リンク リストでは、通常のリンク リストの末尾ノード tail は null を指しますが、循環リンク リストの末尾はヘッド ノード head を指します。

[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-0v0nt7lE-1679589982443)(.\imgs\image-20221110083538273.png)]

リンク リストにはセンチネル ノードと呼ばれる特別なノードもあります。これはダミー ノードとも呼ばれます。これはデータを保存せず、通常は境界判断を簡略化するために先頭と末尾として使用されます (以下の図を参照)。

[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-RUvDkzMz-1679589982444)(.\imgs\image-20221110084611550.png)]

ランダムアクセス性能

インデックス、時間計算量に基づく検索O ( n ) O(n)O ( n )

パフォーマンスの挿入または削除

  • 開始位置: O ( 1 ) O(1)
  • 終了位置: 末尾ノードがO ( 1 ) O(1)であることがわかっている場合O ( 1 ) 、末尾ノードがO(n) O(n)であるかわかりませんO ( n )
  • 中央の位置: インデックスに基づく検索時間 + O ( 1 ) O(1)

単一リンクリスト

一方向リンクリストの定義に従い、まず値と次のポインタを格納するクラスNodeと、先頭ノードを記述する参照を定義します。

public class SinglyLinkedList {
    
    
    
    private Node head; // 头部节点
    
    private static class Node {
    
     // 节点类
        int value;
        Node next;

        public Node(int value, Node next) {
    
    
            this.value = value;
            this.next = next;
        }
    }
}
  • Node は実装の詳細を外部から隠すための内部クラスとして定義されておりクラスのユーザーは Node の構造を気にする必要はありません。
  • Nodeを SinglyLinkedList インスタンスに関連付ける必要がなく、複数の SinglyLinkedList インスタンスが Node クラス定義を共有できるため、静的内部クラスとして定義されます。

頭が追加されました

public class SinglyLinkedList {
    
    
     /**
     * 头部添加一个节点
     * @param value
     */
    public void addFirst(int value) {
    
    
		this.head = new Node(value, this.head);
    }
}
  • this.head == null の場合、新しいノードは null を指し、新しい this.head として機能します。
  • this.head != null の場合、新しいノードは元の this.head を指し、新しい this.head として機能します。
    • 代入操作は右から左に実行されることに注意してください。

横断中

public class SinglyLinkedList {
    
    
    // ...
    public void loop() {
    
    
        Node curr = this.head;
        while (curr != null) {
    
    
            // 做一些事
            curr = curr.next;
        }
    }
}

以下を使用して、トラバース時に実行したい機能を渡すことができますConsumer

  • Consumer のルールはパラメータであり、戻り値はないため、System.out::println のようなメソッドは Consumer です。
  • Consumer を呼び出すときは、現在のノード curr.value をパラメータとして渡します。

コードは以下のように表示されます:

/**
 * while遍历
 */
public void whileLoop(Consumer<Integer> consumer) {
    
    
    Node curr = this.head;
    while (curr != null) {
    
    
        // 做一些事
        consumer.accept(curr.value);
        curr = curr.next;
    }
}

テスト呼び出しでは、走査された値が出力されます。

public class SinglyLinkedList {
    
    
  
    public static void main(String[] args) {
    
    
        Ds02SinglyLinkedList singlyLinkedList = new Ds02SinglyLinkedList();
        singlyLinkedList.addFirst(4);
        singlyLinkedList.addFirst(3);
        singlyLinkedList.addFirst(2);
        singlyLinkedList.addFirst(1);
				// 使用lambda表达式
        singlyLinkedList.whileLoop(System.out::println);
    }
}

トラバース用

/**
* for遍历
*/
public void forLoop(Consumer<Integer> consumer) {
    
    
    for (Node curr = this.head; curr != null ; curr = curr.next) {
    
    
        // 做一些事
        consumer.accept(curr.value);
    }
}

イテレータのトラバーサル

「Iterable 接口,实现其iterator」メソッドを継承し、イテレータを返します。

/**
* 迭代器遍历
*/
@Override
public Iterator<Integer> iterator() {
    
    
    // 使用匿名内部类
    return new Iterator<Integer>() {
    
    
        Node p = head;
        @Override
        public boolean hasNext() {
    
    
          return p != null;
        }

        @Override
        public Integer next() {
    
    
            int v = p.value;
            p = p.next;
            return v;
        }
    };
}
  • hasNext は、next を呼び出す必要があるかどうかを判断するために使用されます。
  • 次に 2 つのことを行います
    • 現在のノードの値を返します
    • 次のノードを指す

テストコードは次のとおりです。

public class SinglyLinkedList {
    
    
  
    public static void main(String[] args) {
    
    
        Ds02SinglyLinkedList singlyLinkedList = new Ds02SinglyLinkedList();
        singlyLinkedList.addFirst(4);
        singlyLinkedList.addFirst(3);
        singlyLinkedList.addFirst(2);
        singlyLinkedList.addFirst(1);
				for (Integer integer : singlyLinkedList) {
    
    
            System.out.println(integer);
        }
    }
}

再帰的走査

public class SinglyLinkedList implements Iterable<Integer> {
    
    
    // ...
    public void recursionLoop() {
    
    
        recursion(this.head);
    }

    private void recursion(Node curr) {
    
    
        if (curr == null) {
    
    
            return;
        }
        // 前面做些事
        recursion(curr.next);
        // 后面做些事
    }
}

最後に追加

末尾に要素を追加するには、まず末尾のノードを見つけてから、末尾ノードの後に​​新しいノードを挿入する必要があります。

最後のノードを検索するメソッドを作成するfindLast()と、そのメソッドは後で再利用できます。

public class SinglyLinkedList {
    
    
        /**
     * 寻找最后一个节点
     * @return
     */
    private Node findLast() {
    
    
        if (this.head == null) {
    
    
            return null;
        }
        Node curr;
        for (curr = this.head; curr.next != null; ) {
    
    
            curr = curr.next;
        }
        return curr;
    }

    /**
     * 在最后一个节点添加元素
     * @param value
     */
    public void addLast(int value) {
    
    
        Node last = findLast();
        // 如果未找到最后一个元素,说明链表为空,在头部添加即可
        if (last == null) {
    
    
            addFirst(value);
            return;
        }
        // 找到尾部元素后
        last.next = new Node(value, null);
    }
}
  • 最後のノードを見つけるための終了条件は curr.next == null であることに注意してください。

最後に複数を追加します

public class SinglyLinkedList {
    
    
    // ...
	public void addLast(int first, int... rest) {
    
    
        
        Node sublist = new Node(first, null);
        Node curr = sublist;
        for (int value : rest) {
    
    
            curr.next = new Node(value, null);
            curr = curr.next;
        }
        
        Node last = findLast();
        if (last == null) {
    
    
            this.head = sublist;
            return;
        }
        last.next = sublist;
    }
}
  • サブリストの文字列への最初の文字列
  • それから全体として追加します

インデックスで取得する

public class SinglyLinkedList {
    
    
      /**
     * 寻找指定索引位置的节点
     * @param index
     * @return
     */
    private Node findNode(int index) {
    
    
        int i = 0;
        for (Node curr = this.head; curr != null; curr = curr.next, i++) {
    
    
            if (index == i) {
    
    
                return curr;
            }
        }
        return null;
    }

    private IllegalArgumentException illegalIndex(int index) {
    
    
        return new IllegalArgumentException(String.format("index [%d] 不合法%n", index));
    }

    /**
     * 获取指定位置的索引
     * @param index
     * @return
     */
    public int get(int index) {
    
    
        Node node = findNode(index);
        if (node != null) {
    
    
            return node.value;
        }
        throw illegalIndex(index);
    }
}
  • 同様に、サブメソッドも再利用できます

入れる

public class SinglyLinkedList {
    
    
       /**
     * 指定位置插入节点
     * @param index
     * @param value
     */
    public void insert(int index, int value) {
    
    

        if (index == 0) {
    
    
            addFirst(value);
            return;
        }
        // 找到上一个节点
        Node prev = findNode(index - 1); 
        if (prev == null) {
    
     // 找不到
            throw illegalIndex(index);
        }
        prev.next = new Node(value, prev.next);
    }
}
  • 挿入 (以下の削除を含む) では、前のノードを検索する必要があります

消去

public class SinglyLinkedList {
    
    
      /**
     * 删除指定位置的节点
     * @param index
     */
    public void remove(int index) {
    
    
        // 删除头节点
        if (index == 0) {
    
    
            if (this.head != null) {
    
    
                this.head = this.head.next;
                return;
            } else {
    
    
                throw illegalIndex(index);
            }
        }
        // 找到前一个节点
        Node prev = findNode(index - 1);
        Node curr;
        if (prev != null && (curr = prev.next) != null) {
    
    
            // 把前一个节点的下一个节点设置为下一个节点
            prev.next = curr.next;
        } else {
    
    
            throw illegalIndex(index);
        }
    }
}
  • 最初の if ブロックは、removeFirst のケースに対応します。
  • 最後の if ブロックは、少なくとも 2 つのノードの場合に対応します
    • 前のノードが空でないと判断するだけでなく、現在のノードが空でないことも確認します

単一リンクリスト (センチネル付き)

以前、一方向リンクリストの実装を観察したところ、ほとんどすべてのメソッドに先頭かどうかを判断するコードが含まれていることがわかりました。

データストレージに参加しない特別なノードをセンチネルとして使用します。これは一般にセンチネルまたはダミーと呼ばれ、センチネルノードとのリンクされたリストはリーダーリンクリストと呼ばれます

public class SinglyLinkedListSentinel {
    
    
    // ...
    private Node head = new Node(Integer.MIN_VALUE, null);
}
  • その値は使用されないため、どのような値が保存されていても問題ありません。

センチネル ノードに参加すると、コードは比較的シンプルになります。最初にいくつかのツールとメソッドを確認します。

public class SinglyLinkedListSentinel {
    
    
    // ...
    
    // 根据索引获取节点
    private Node findNode(int index) {
    
    
        int i = -1;
        for (Node curr = this.head; curr != null; curr = curr.next, i++) {
    
    
            if (i == index) {
    
    
                return curr;
            }
        }
        return null;
    }
    
    // 获取最后一个节点
    private Node findLast() {
    
    
        Node curr;
        for (curr = this.head; curr.next != null; ) {
    
    
            curr = curr.next;
        }
        return curr;
    }
}
  • findNode は以前と似ていますが、 i の初期値がセンチネルに対応する -1 に設定され、渡される実際のインデックスも[ − 1 , ∞ ) [-1, \infty)である点が異なります。[ 1
  • findLast は決して null を返しません。他のノードがない場合でも、最後のノードとして Sentinel を返します。

このようにして、コードは次のように簡素化されます。

public class SinglyLinkedListSentinel {
    
    
    // ...
    
    public void addLast(int value) {
    
    
        Node last = findLast();
        /*
        改动前
        if (last == null) {
            this.head = new Node(value, null);
            return;
        }
        */
        last.next = new Node(value, null);
    }
    
    public void insert(int index, int value) {
    
    
        /*
        改动前
        if (index == 0) {
            this.head = new Node(value, this.head);
            return;
        }
        */
        // index 传入 0 时,返回的是哨兵
        Node prev = findNode(index - 1);
        if (prev != null) {
    
    
            prev.next = new Node(value, prev.next);
        } else {
    
    
            throw illegalIndex(index);
        }
    }
    
    public void remove(int index) {
    
    
        /*
        改动前
        if (index == 0) {
            if (this.head != null) {
                this.head = this.head.next;
                return;
            } else {
                throw illegalIndex(index);
            }
        }
        */
        // index 传入 0 时,返回的是哨兵
        Node prev = findNode(index - 1);
        Node curr;
        if (prev != null && (curr = prev.next) != null) {
    
    
            prev.next = curr.next;
        } else {
    
    
            throw illegalIndex(index);
        }
    }
    
    public void addFirst(int value) {
    
    
        /*
        改动前
        this.head = new Node(value, this.head);
        */
		this.head.next = new Node(value, this.head.next);
        // 也可以视为 insert 的特例, 即 insert(0, value);
    }
}
  • 削除については、前に [最後の if ブロックは少なくとも 2 つのノードが取得される状況に対応する] と述べましたが、センチネルがあるので、2 つのノードで十分です。

二重リンクリスト (センチネル付き)

public class DoublyLinkedListSentinel implements Iterable<Integer> {
    
    

    private final Node head;
    private final Node tail;

    public DoublyLinkedListSentinel() {
    
    
        head = new Node(null, 666, null);
        tail = new Node(null, 888, null);
        head.next = tail;
        tail.prev = head;
    }

    private Node findNode(int index) {
    
    
        int i = -1;
        for (Node p = head; p != tail; p = p.next, i++) {
    
    
            if (i == index) {
    
    
                return p;
            }
        }
        return null;
    }

    public void addFirst(int value) {
    
    
        insert(0, value);
    }

    public void removeFirst() {
    
    
        remove(0);
    }

    public void addLast(int value) {
    
    
        Node prev = tail.prev;
        Node added = new Node(prev, value, tail);
        prev.next = added;
        tail.prev = added;
    }

    public void removeLast() {
    
    
        Node removed = tail.prev;
        if (removed == head) {
    
    
            throw illegalIndex(0);
        }
        Node prev = removed.prev;
        prev.next = tail;
        tail.prev = prev;
    }

    public void insert(int index, int value) {
    
    
        Node prev = findNode(index - 1);
        if (prev == null) {
    
    
            throw illegalIndex(index);
        }
        Node next = prev.next;
        Node inserted = new Node(prev, value, next);
        prev.next = inserted;
        next.prev = inserted;
    }

    public void remove(int index) {
    
    
        Node prev = findNode(index - 1);
        if (prev == null) {
    
    
            throw illegalIndex(index);
        }
        Node removed = prev.next;
        if (removed == tail) {
    
    
            throw illegalIndex(index);
        }
        Node next = removed.next;
        prev.next = next;
        next.prev = prev;
    }

    private IllegalArgumentException illegalIndex(int index) {
    
    
        return new IllegalArgumentException(
                String.format("index [%d] 不合法%n", index));
    }

    @Override
    public Iterator<Integer> iterator() {
    
    
        return new Iterator<Integer>() {
    
    
            Node p = head.next;

            @Override
            public boolean hasNext() {
    
    
                return p != tail;
            }

            @Override
            public Integer next() {
    
    
                int value = p.value;
                p = p.next;
                return value;
            }
        };
    }

    static class Node {
    
    
        Node prev;
        int value;
        Node next;

        public Node(Node prev, int value, Node next) {
    
    
            this.prev = prev;
            this.value = value;
            this.next = next;
        }
    }
}

リングリンクリスト(センチネル付き)

センチネル付きの双方向リング チェーン ウォッチ。このときセンチネルは頭と尾の両方の役割を果たします。

[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-Q0Mf8Opu-1679589982444)(./imgs/image-20221229144232651.png)]

[外部リンク画像の転送に失敗しました。ソース サイトには盗難防止リンク メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-qIVfR2uc-1679589982444)(./imgs/image-20221229143756065.png)]

[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-f6tHpFnn-1679589982444)(./imgs/image-20221229153338425.png)]

[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-f5k2mR5K-1679589982444)(./imgs/image-20221229154248800.png)]

リファレンス実装

public class DoublyLinkedListSentinel implements Iterable<Integer> {
    
    

    @Override
    public Iterator<Integer> iterator() {
    
    
        return new Iterator<>() {
    
    
            Node p = sentinel.next;

            @Override
            public boolean hasNext() {
    
    
                return p != sentinel;
            }

            @Override
            public Integer next() {
    
    
                int value = p.value;
                p = p.next;
                return value;
            }
        };
    }

    static class Node {
    
    
        Node prev;
        int value;
        Node next;

        public Node(Node prev, int value, Node next) {
    
    
            this.prev = prev;
            this.value = value;
            this.next = next;
        }
    }

    private final Node sentinel = new Node(null, -1, null); // 哨兵

    public DoublyLinkedListSentinel() {
    
    
        sentinel.next = sentinel;
        sentinel.prev = sentinel;
    }

    /**
     * 添加到第一个
     * @param value 待添加值
     */
    public void addFirst(int value) {
    
    
        Node next = sentinel.next;
        Node prev = sentinel;
        Node added = new Node(prev, value, next);
        prev.next = added;
        next.prev = added;
    }

    /**
     * 添加到最后一个
     * @param value 待添加值
     */
    public void addLast(int value) {
    
    
        Node prev = sentinel.prev;
        Node next = sentinel;
        Node added = new Node(prev, value, next);
        prev.next = added;
        next.prev = added;
    }
    
    /**
     * 删除第一个
     */
    public void removeFirst() {
    
    
        Node removed = sentinel.next;
        if (removed == sentinel) {
    
    
            throw new IllegalArgumentException("非法");
        }
        Node a = sentinel;
        Node b = removed.next;
        a.next = b;
        b.prev = a;
    }

    /**
     * 删除最后一个
     */
    public void removeLast() {
    
    
        Node removed = sentinel.prev;
        if (removed == sentinel) {
    
    
            throw new IllegalArgumentException("非法");
        }
        Node a = removed.prev;
        Node b = sentinel;
        a.next = b;
        b.prev = a;
    }

    /**
     * 根据值删除节点
     * <p>假定 value 在链表中作为 key, 有唯一性</p>
     * @param value 待删除值
     */
    public void removeByValue(int value) {
    
    
        Node removed = findNodeByValue(value);
        if (removed != null) {
    
    
            Node prev = removed.prev;
            Node next = removed.next;
            prev.next = next;
            next.prev = prev;
        }
    }

    private Node findNodeByValue(int value) {
    
    
        Node p = sentinel.next;
        while (p != sentinel) {
    
    
            if (p.value == value) {
    
    
                return p;
            }
            p = p.next;
        }
        return null;
    }

}

おすすめ

転載: blog.csdn.net/qq_43745578/article/details/129742653