Trouver l’élément central d’une liste chaînée

Trouver l'élément central d'une liste chaînée

1. Vue d'ensemble

Dans ce didacticiel, nous allons expliquer comment trouver l'élément central d'une liste liée en Java.

Nous présenterons les principaux problèmes dans les sections suivantes et présenterons différentes approches pour les résoudre.

2. Garder une trace de la taille

Ce problème peut être facilement résolu parkeeping track of the size when we add new elements to the list. Si nous connaissons la taille, nous savons également où se trouve l'élément central, la solution est donc triviale.

Voyons un exemple utilisant l’implémentation Java d’unLinkedList:

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

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

Si nous vérifions le code interne de la classeLinkedList, nous pouvons voir que dans cet exemple, nous parcourons simplement la liste jusqu'à ce que nous atteignions l'élément du milieu:

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. Trouver le milieu sans connaître la taille

Il est très courant que nous rencontrions des problèmes oùwe only have the head node of a linked list, et nous devons trouver l'élément du milieu. Dans ce cas, nous ne connaissons pas la taille de la liste, ce qui rend ce problème plus difficile à résoudre.

Nous montrerons dans les sections suivantes plusieurs approches pour résoudre ce problème, mais d'abord, nous devons créer une classe pour représenter un nœud de la liste.

Créons une classeNode, qui stocke les valeursString:

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;
    }
}

De plus, nous utiliserons cette méthode d'assistance dans nos cas de test pour créer une liste liée individuellement en utilisant uniquement nos nœuds:

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. Trouver d'abord la taille

Pour résoudre ce problème, l’approche la plus simple consiste à trouver d’abord la taille de la liste, puis à suivre la même approche que celle que nous avons utilisée auparavant: itérer jusqu’à l’élément central.

Voyons cette solution en action:

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());
}

Comme nous pouvons le voir,this code iterates through the list twice. Therefore, this solution has a poor performance and it’s not recommended.

3.2. Recherche de l'élément central en une seule passe de manière itérative

Nous allons maintenant améliorer la solution précédente en recherchant l'élément du milieu avec une seule itération sur la liste.

Pour faire cela de manière itérative, nous avons besoin de deux pointeurs pour parcourir la liste en même temps. One pointer will advance 2 nodes in each iteration, and the other pointer will advance only one node per iteration.

Lorsque le pointeur le plus rapide atteint la fin de la liste, le pointeur le plus lent se trouve au milieu:

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());
}

Nous pouvons tester cette solution avec un simple test unitaire en utilisant des listes avec un nombre pair et impair d'éléments:

@Test
public void whenFindingMiddleFromHead1PassIteratively_thenMiddleFound() {

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

3.3. Recherche récursive de l'élément central en un seul passage

Un autre moyen de résoudre ce problème en un seul passage consiste à utiliser la récursivité. Nous pouvons parcourir jusqu'à la fin de la liste pour connaître la taille et,in the callbacks, we just count until the half of the size.

Pour ce faire en Java, nous allons créer une classe auxiliaire pour conserver les références de la taille de la liste et de l'élément du milieu lors de l'exécution de tous les appels récursifs:

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

Maintenant, implémentons la méthode récursive:

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--;
}

Et enfin, créons une méthode qui appelle la méthode récursive:

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());
}

Encore une fois, nous pouvons le tester de la même manière que précédemment:

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

4. Conclusion

Dans cet article, nous avons présenté le problème de la recherche de l'élément central d'une liste chaînée en Java, et nous avons montré différentes façons de le résoudre.

Nous sommes partis de l'approche la plus simple où nous avons suivi la taille, puis nous avons continué avec les solutions pour trouver l'élément du milieu à partir du nœud principal de la liste.

Comme toujours, le code source complet des exemples est disponibleover on GitHub.