Найти средний элемент связанного списка

Найти средний элемент связанного списка

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, и нам нужно найти средний элемент. В этом случае мы не знаем размер списка, что усложняет решение этой проблемы.

В следующих разделах мы покажем несколько подходов к решению этой проблемы, но сначала нам нужно создать класс, представляющий узел списка.

Давайте создадим классNode, в котором хранятся значенияString:

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. Итерационный поиск среднего элемента за один проход

Теперь мы собираемся улучшить предыдущее решение, найдя средний элемент всего за одну итерацию по списку.

Чтобы сделать это итеративно, нам нужно два указателя, чтобы перебирать список одновременно. 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. Рекурсивный поиск среднего элемента за один проход

Другой способ решить эту проблему за один проход - использовать рекурсию. Мы можем выполнить итерацию до конца списка, чтобы узнать размер и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.