リンクリストの中央の要素を探す

リンクリストの中間要素を見つける

1. 概要

このチュートリアルでは、Javaでリンクリストの中央の要素を見つける方法を説明します。

次のセクションでは主な問題を紹介し、それらを解決するためのさまざまなアプローチを示します。

2. サイズを追跡する

この問題は、keeping track of the size when we add new elements to the listだけで簡単に解決できます。 サイズがわかれば、中間要素の位置もわかるので、解は簡単です。

LinkedListのJava実装を使用した例を見てみましょう。

public static Optional findMiddleElementLinkedList(
  LinkedList linkedList) {
    if (linkedList == null || linkedList.isEmpty()) {
        return Optional.empty();
    }

    return Optional.of(linkedList.get(
      (linkedList.size() - 1) / 2));
}

LinkedListクラスの内部コードを確認すると、この例では、中央の要素に到達するまでリストをトラバースしていることがわかります。

Node node(int index) {
    if (index < (size >> 1)) {
        Node x = first;
        for (int i = 0; i < index; i++) {
            x = x.next;
        }
        return x;
    } else {
        Node x = last;
        for (int i = size - 1; i > index; i--) {
            x = x.prev;
        }
        return x;
    }
}

3. サイズを知らずに中間を見つける

we only have the head node of a linked list,で問題が発生し、中央の要素を見つける必要があることはよくあることです。 この場合、リストのサイズがわからないため、この問題の解決が難しくなります。

次のセクションでは、この問題を解決するためのいくつかのアプローチを示しますが、最初に、リストのノードを表すクラスを作成する必要があります。

Stringの値を格納するNodeクラスを作成しましょう。

public static class Node {

    private Node next;
    private String data;

    // constructors/getters/setters

    public boolean hasNext() {
        return next != null;
    }

    public void setNext(Node next) {
        this.next = next;
    }

    public String toString() {
        return this.data;
    }
}

また、テストケースでこのヘルパーメソッドを使用して、ノードのみを使用して単一リンクリストを作成します。

private static Node createNodesList(int n) {
    Node head = new Node("1");
    Node current = head;

    for (int i = 2; i <= n; i++) {
        Node newNode = new Node(String.valueOf(i));
        current.setNext(newNode);
        current = newNode;
    }

    return head;
}

3.1. 最初にサイズを見つける

この問題に取り組むための最も簡単なアプローチは、最初にリストのサイズを見つけ、その後、前に使用したのと同じアプローチに従い、中央の要素まで繰り返すことです。

このソリューションの実際を見てみましょう。

public static Optional findMiddleElementFromHead(Node head) {
    if (head == null) {
        return Optional.empty();
    }

    // calculate the size of the list
    Node current = head;
    int size = 1;
    while (current.hasNext()) {
        current = current.next();
        size++;
    }

    // iterate till the middle element
    current = head;
    for (int i = 0; i < (size - 1) / 2; i++) {
        current = current.next();
    }

    return Optional.of(current.data());
}

ご覧のとおり、this code iterates through the list twice. Therefore, this solution has a poor performance and it’s not recommendedです。

3.2. ワンパスで中間要素を繰り返し見つける

次に、リストを1回繰り返すだけで中央の要素を見つけることにより、前のソリューションを改善します。

これを繰り返し行うには、リストを同時に反復処理する2つのポインターが必要です。 One pointer will advance 2 nodes in each iteration, and the other pointer will advance only one node per iteration

速いポインターがリストの最後に到達すると、遅いポインターが中央になります。

public static Optional findMiddleElementFromHead1PassIteratively(Node head) {
    if (head == null) {
        return Optional.empty();
    }

    Node slowPointer = head;
    Node fastPointer = head;

    while (fastPointer.hasNext() && fastPointer.next().hasNext()) {
        fastPointer = fastPointer.next().next();
        slowPointer = slowPointer.next();
    }

    return Optional.ofNullable(slowPointer.data());
}

奇数と偶数の両方の要素を持つリストを使用した単純な単体テストで、このソリューションをテストできます。

@Test
public void whenFindingMiddleFromHead1PassIteratively_thenMiddleFound() {

    assertEquals("3", MiddleElementLookup
      .findMiddleElementFromHead1PassIteratively(
        createNodesList(5)).get());
    assertEquals("2", MiddleElementLookup
      .findMiddleElementFromHead1PassIteratively(
        reateNodesList(4)).get());
}

3.3. 1つのパスで中間要素を再帰的に見つける

1つのパスでこの問題を解決する別の方法は、再帰を使用することです。 リストの最後まで繰り返して、サイズとin the callbacks, we just count until the half of the size.を知ることができます。

Javaでこれを行うには、すべての再帰呼び出しの実行中にリストサイズと中間要素の参照を保持する補助クラスを作成します。

private static class MiddleAuxRecursion {
    Node middle;
    int length = 0;
}

それでは、再帰メソッドを実装しましょう。

private static void findMiddleRecursively(
  Node node, MiddleAuxRecursion middleAux) {
    if (node == null) {
        // reached the end
        middleAux.length = middleAux.length / 2;
        return;
    }
    middleAux.length++;
    findMiddleRecursively(node.next(), middleAux);

    if (middleAux.length == 0) {
        // found the middle
        middleAux.middle = node;
    }

    middleAux.length--;
}

最後に、再帰的なメソッドを呼び出すメソッドを作成しましょう。

public static Optional findMiddleElementFromHead1PassRecursively(Node head) {

    if (head == null) {
        return Optional.empty();
    }

    MiddleAuxRecursion middleAux = new MiddleAuxRecursion();
    findMiddleRecursively(head, middleAux);
    return Optional.of(middleAux.middle.data());
}

繰り返しますが、以前と同じ方法でテストできます。

@Test
public void whenFindingMiddleFromHead1PassRecursively_thenMiddleFound() {
    assertEquals("3", MiddleElementLookup
      .findMiddleElementFromHead1PassRecursively(
        createNodesList(5)).get());
    assertEquals("2", MiddleElementLookup
      .findMiddleElementFromHead1PassRecursively(
        createNodesList(4)).get());
}

4. 結論

この記事では、Javaでリンクリストの中間要素を見つける問題を紹介し、それを解決するさまざまな方法を示しました。

サイズを追跡する最も単純なアプローチから始め、その後、リストのヘッドノードから中央の要素を見つけるためのソリューションを続けました。

いつものように、例の完全なソースコードはover on GitHubで入手できます。