Der 4D-Langartikel entführt Sie in die Welt der Datenstrukturen

Was ist die Datenstruktur?

Programm = Datenstruktur + Algorithmus

Ja, der obige Satz ist sehr klassisch. Programme bestehen aus Datenstrukturen und Algorithmen. Natürlich ergänzen sich Datenstrukturen und Algorithmen und können nicht völlig unabhängig voneinander betrachtet werden. Dieser Artikel konzentriert sich jedoch auf diese häufig verwendeten Datenstrukturen.

Was ist die Datenstruktur?

Zunächst einmal, was sind Daten? Daten stellen symbolische Darstellungen objektiver Sachverhalte dar . In der Informatik bezeichnet man damit alle Symbole, die in einen Computer eingegeben und von einem Computerprogramm verarbeitet werden können. Warum dann das Wort "Struktur" hinzufügen?

Datenelemente sind die Grundeinheiten von Daten , und in keinem Problem existieren Datenelemente unabhängig voneinander. Es besteht immer eine Beziehung zwischen ihnen. Diese Beziehung zwischen Datenelementen wird als Struktur bezeichnet .

Daher haben wir folgende Definitionen:

Eine Datenstruktur ist die Art und Weise, wie ein Computer Daten speichert und organisiert . Eine Datenstruktur ist eine Sammlung von Datenelementen , die eine oder mehrere spezifische Beziehungen zueinander haben . Häufig kann eine gut gewählte Datenstruktur zu einer höheren Betriebs- oder Speichereffizienz führen . Datenstrukturen werden häufig mit effizienten Abrufalgorithmen und Indizierungstechniken in Verbindung gebracht .

Einfach ausgedrückt ist eine Datenstruktur eine Möglichkeit, Daten zu organisieren, zu verwalten und zu speichern. Obwohl theoretisch alle Daten gemischt oder gemischt oder ohne Nahrung gespeichert werden können, streben Computer nach hoher Effizienz.Wenn wir die Datenstruktur verstehen können, finden wir eine Datenstruktur, die für das aktuelle Problemszenario besser geeignet ist, und drücken dies aus Beziehung zwischen den Daten In Bezug auf die Speicherung kann der adaptive Algorithmus effizienter bei der Berechnung verwendet werden, sodass die Laufeffizienz des Programms definitiv verbessert wird.

Die vier häufig verwendeten Datenstrukturen sind:

  • Mengen: nur Beziehungen, die zur selben Menge gehören, keine anderen Beziehungen
  • Lineare Struktur: Zwischen den Datenelementen in der Struktur besteht eine Eins-zu-eins-Beziehung
  • Baumstruktur: Zwischen den Datenelementen in der Struktur besteht eine Eins-zu-Viele-Beziehung
  • Graphenartige Struktur oder netzartige Struktur: Graphenartige Struktur oder netzartige Struktur

Was ist die logische Struktur und Speicherstruktur?

Die logische Beziehung zwischen Datenelementen wird als logische Struktur bezeichnet, das heißt, wir definieren eine mathematische Beschreibung des Operationsobjekts. Aber wir müssen auch wissen, wie man es im Computer darstellt. Die Darstellung der Datenstruktur im Computer (auch Abbild genannt) wird als physikalische Struktur der Daten, auch Speicherstruktur genannt, bezeichnet .

Die Beziehung vor den Datenelementen hat zwei unterschiedliche Darstellungsmethoden im Computer: sequentielles Bild und nicht sequentielles Bild , und daraus werden zwei verschiedene Speicherstrukturen erhalten: sequentielle Speicherstruktur und Kettenspeicherstruktur , wie wir sequentielle Speicherstruktur wollen Bei der Darstellung komplexer Zahlen z1 =3.0 - 2.3i kann die logische Beziehung zwischen Datenelementen direkt durch die relative Position der Elemente im Speicher dargestellt werden:

Die Kettenstruktur verwendet Zeiger , um die logische Beziehung zwischen Datenelementen darzustellen. Suchen Sie in ähnlicher Weise z1 =3.0 - 2.3i zuerst das nächste 100, das eine Adresse ist, und finden Sie die echten Daten gemäß der Adresse -2.3i:

bisschen

Die kleinste Einheit, die Informationen in einem Computer darstellt, ist ein Bit in einer Binärzahl, das als Bit bezeichnet wird . Das heißt, wir sehen gewöhnlich 01010101010Daten wie diese: Die unterste Schicht des Computers besteht aus allen Arten von Transistoren und Leiterplatten, also, egal um welche Daten es sich handelt, sogar Bilder, Töne und Summen unten 0. 1Wenn es acht Schaltkreise gibt, dann hat jeder Stromkreis sein eigenes Der geschlossene Zustand von , es gibt 8eine 2Multiplikation, 2^8^, was 256ein anderes Signal ist.

Aber im Allgemeinen müssen wir negative Zahlen darstellen, das heißt, das höchste Bit stellt das Vorzeichenbit dar, 0stellt positive Zahlen dar, 1stellt negative Zahlen dar, das heißt, der maximale Wert von 8 Bit ist 01111111, das heißt 127.

Es ist erwähnenswert, dass es in der Computerwelt mehr Konzepte von Originalcode, inversem Code und komplementärem Code gibt:

  • Ursprünglicher Code: Verwenden Sie das erste Bit, um das Symbol darzustellen, und die restlichen Bits, um den Wert darzustellen
  • Komplementcode: Das Komplement der positiven Zahl ist es selbst, das Komplement der negativen Zahl ist, dass das Vorzeichenbit unverändert bleibt und die verbleibenden Bits invertiert werden.
  • Zweierkomplement: Das Komplement einer positiven Zahl ist es selbst, das Komplement einer negativen Zahl basiert auf ihrem Komplement + 1

Warum brauchen Sie die Umkehrung und das Komplement des ursprünglichen Codes?

Wir wissen, dass Addition und Subtraktion Hochfrequenzoperationen sind. Menschen können die Plus- und Minuszeichen intuitiv sehen und sie können sie sofort berechnen. Wenn der Computer jedoch verschiedene Symbole unterscheidet, wird die Addition und Subtraktion komplizierter, z positive + positive Zahlen, positive Zahlen - positive Zahlen, positive Zahlen - negative Zahlen, negative Zahlen + negative Zahlen ... etc. Daher möchten einige Leute denselben Operator (Plus-Operator) verwenden, um alle Additions- und Subtraktionsberechnungen zu lösen, wodurch viele komplexe Schaltungen und der Overhead verschiedener Symbolumwandlungen reduziert werden können und die Berechnung effizienter ist.

Wir können sehen, dass das Ergebnis der folgenden negativen Zahlen, die an der Operation teilnehmen, ebenfalls den Komplementregeln entspricht:

        00100011		35
 +      11011101	   -35
-------------------------
        00000000       0
        00100011		35
 + 	    11011011	   -37
-------------------------
        11111110       -2

Wenn das Berechnungsergebnis den Bereich überschreitet, den die Anzahl der Ziffern darstellen kann, ist es natürlich ein Überlauf, was bedeutet, dass mehr Ziffern benötigt werden, um korrekt dargestellt zu werden.

Im Allgemeinen sollten diejenigen, die Bitoperationen verwenden können, versuchen, Bitoperationen zu verwenden, da dies effizienter ist.

  • ~: bitweise Negation
  • &: als UND-Verknüpfung
  • |: bitweise ODER-Verknüpfung
  • ^: bitweises exklusives ODER
  • <<: Linksverschiebung mit Vorzeichen, zum Beispiel 35(00100011), eine Linksverschiebung ist ein bisschen 70(01000110), -35(11011101)eine Linksverschiebung ist ein bisschen-70(10111010)
  • >>: Vorzeichenbehaftete Rechtsverschiebung, zum Beispiel 35(00100011), eine Rechtsverschiebung ist ein bisschen 17(00010001), -35(11011101)eine Linksverschiebung ist ein bisschen-18(11101110)
  • <<<: Vorzeichenlose Linksverschiebung, zum Beispiel 35(00100011), ist eine Linksverschiebung70(01000110)
  • >>>: vorzeichenlose Rechtsverschiebung, zum Beispiel -35(11011101), ist eine Rechtsverschiebung110(01101110)
  • x ^= y; y ^= x; x ^= y;:Austausch
  • s &= ~(1 << k):te kStelle 0

Um darüber zu sprechen, wo Bit-Operationen verwendet werden, ist es eher klassisch, als Bloom-Filter zu zählen , siehe für Details: http://aphysia.cn/archives/cachebloomfilter

Was ist ein Bloom-Filter?

Der Bloom-Filter ( Bloom Filter) wurde 1970 von Bloom ( ) vorgeschlagen Burton Howard Bloomund besteht tatsächlich aus einem langen binären Vektor und einer Reihe zufälliger Hash-Mapping-Funktionen (um es klar auszudrücken, es ist ein binäres Array zum Speichern von Datenmerkmalen). Es kann verwendet werden, um festzustellen, ob ein Element in der Sammlung vorhanden ist. Seine Vorteile sind eine hohe Abfrageeffizienz und wenig Platz. Der Nachteil ist, dass es bestimmte Fehler gibt, und wenn wir Elemente entfernen möchten, können sie sich gegenseitig beeinflussen.

Das heißt, wenn ein Element der Sammlung durch mehrere hashFunktionen hinzugefügt wird, wird das Element keinem Punkt im Bitarray zugeordnet, der auf gesetzt ist 1.

Der Punkt ist, dass es mehrere Hash-Funktionen gibt, die Daten in verschiedene Bits hashen können, und nur wenn diese Bits alle 1 sind, können wir beurteilen, dass die Daten bereits vorhanden sind

Angenommen, es gibt drei hashFunktionen, dann verwenden verschiedene Elemente drei hashFunktionen hashan drei Positionen.

Angenommen, es gibt einen anderen Zhang San, dann wird er, hashwenn er dort ist, auch hashzu den folgenden Positionen gehen, alle Positionen sind 1, wir können sagen, dass Zhang San bereits darin existiert.

Besteht also die Möglichkeit einer Fehleinschätzung? Dies ist möglich.Zum Beispiel gibt es jetzt nur noch Zhang San, Li Si, Wang Wu und Cai Ba. Die hashMapping-Werte sind wie folgt:

Chen Liu kam später, aber leider wurden hashdie Bits aus dem Hash seiner drei Funktionen nur hashdurch andere Elemente geändert, und 1es wurde geurteilt, dass es bereits existierte, aber tatsächlich existierte Chen Liu nicht.

Die obige Situation ist eine Fehleinschätzung, und der Bloom-Filter wird unweigerlich zu einer Fehleinschätzung führen. Aber es hat den Vorteil, dass beim Bloom-Filter die Elemente, die existieren, möglicherweise nicht existieren, aber die Elemente, die nicht existieren, nicht existieren dürfen. , weil das Fehlen eines Urteils bedeutet, dass mindestens eines von hashihnen falsch ist.

Dies liegt auch daran, dass mehrere Elemente hashzusammenkommen können, aber ein Datenwert aus der Sammlung geworfen wird und wir das zugeordnete Bit auf setzen möchten, 0was dem Löschen der Daten entspricht. Zu diesem Zeitpunkt sind andere Elemente betroffen, und die von anderen Elementen zugeordneten Positionen können auf gesetzt werden 0. Aus diesem Grund können Bloomfilter nicht entfernt werden.

Reihe

Die lineare Darstellung ist die am häufigsten verwendete und einfachste Datenstruktur, eine lineare Darstellung einer endlichen Folge von n Datenelementen mit den folgenden Eigenschaften:

  • Es gibt ein eindeutiges erstes Datenelement
  • Es gibt ein eindeutiges Datenelement namens last
  • Mit Ausnahme des ersten hat jedes Element in der Menge einen Vorgänger
  • Mit Ausnahme des letzten Elements hat jedes Datenelement in der Sammlung ein Nachfolgeelement

Lineare Tabellen umfassen Folgendes:

  • Arrays: schnelles Abfragen/Aktualisieren, langsames Suchen/Löschen
  • verknüpfte Liste
  • Warteschlange
  • Stapel

Ein Array ist eine Art lineare Tabelle, und die Reihenfolge der linearen Tabelle bedeutet, dass die Datenelemente der linearen Tabelle sequentiell in einer Gruppe von Speichereinheiten mit aufeinanderfolgenden Adressen gespeichert werden :

JavaAngegeben als :

int[] nums = new int[100];
int[] nums = {1,2,3,4,5};

Object[] Objects = new Object[100];

ist C++vertreten in:

int nums[100];

Array ist eine lineare Struktur, im Allgemeinen ein kontinuierlicher Raum auf der untersten Ebene, der die gleiche Art von Daten speichert.Aufgrund der kontinuierlichen kompakten Struktur und der Unterstützung natürlicher Indizes ist die Effizienz der Abfragedaten hoch:

aAngenommen, wir wissen, dass der erste Wert des Arrays die Adresse 296ist und der darin enthaltene Datentyp 2eine , dann können wir, wenn wir erwarten, den fünften Wert zu erhalten: 296+(5-1)*2 = 304, O(1)die Zeitkomplexität von erhalten.

Die Essenz des Updates besteht auch darin, zuerst das Element zu finden, dann können Sie mit dem Update beginnen:

Wenn Sie jedoch Daten einfügen möchten, müssen Sie die folgenden Daten verschieben, z. B. das folgende Array, Elemente einfügen 6. Das Schlimmste ist, alle Elemente zu verschieben, da die Zeit komplex istO(n)

Bild-20220104225524289

Das Löschen eines Elements erfordert das Verschieben der folgenden Daten nach vorne, und die größte Zeitkomplexität ist auch O(n):

Java-Code implementiert das Hinzufügen, Löschen, Ändern und Überprüfen von Arrays:

package datastruction;

import java.util.Arrays;

public class MyArray {
    private int[] data;

    private int elementCount;

    private int length;

    public MyArray(int max) {
        length = max;
        data = new int[max];
        elementCount = 0;
    }

    public void add(int value) {
        if (elementCount == length) {
            length = 2 * length;
            data = Arrays.copyOf(data, length);
        }
        data[elementCount] = value;
        elementCount++;
    }

    public int find(int searchKey) {
        int i;
        for (i = 0; i < elementCount; i++) {
            if (data[i] == searchKey)
                break;
        }
        if (i == elementCount) {
            return -1;
        }
        return i;
    }

    public boolean delete(int value) {
        int i = find(value);
        if (i == -1) {
            return false;
        }
        for (int j = i; j < elementCount - 1; j++) {
            data[j] = data[j + 1];
        }
        elementCount--;
        return true;
    }

    public boolean update(int oldValue, int newValue) {
        int i = find(oldValue);
        if (i == -1) {
            return false;
        }
        data[i] = newValue;
        return true;
    }
}

// 测试类
public class Test {
    public static void main(String[] args) {
        MyArray myArray = new MyArray(2);
        myArray.add(1);
        myArray.add(2);
        myArray.add(3);
        myArray.delete(2);
        System.out.println(myArray);
    }
}

verknüpfte Liste

Im obigen Beispiel sehen wir, dass das Array fortlaufend Platz benötigt. Wenn der Platz nur groß ist, muss er erweitert werden 2, wenn er im 3ten wird. Außerdem müssen die Elemente kopiert werden. Einige Lösch- und Einfügevorgänge verursachen weitere Datenverschiebungsvorgänge.

Eine verkettete Liste, das heißt eine verkettete Datenstruktur, hat nicht die Nachteile sequentieller Speicherstrukturen, da sie nicht erfordert, dass logisch benachbarte Datenelemente in ihrer physischen Position benachbart sind, aber gleichzeitig verliert sie auch die direkte Suche durch Index-Indizes Vorteile von Elementen.

Wichtig: Die verknüpfte Liste ist im Speicher des Computers nicht fortlaufend, aber der vorherige Knoten speichert den Zeiger (Adresse) des nächsten Knotens, und der letztere Knoten wird über die Adresse gefunden.

Das Folgende ist die Struktur einer einfach verketteten Liste:

In der Regel werden wir manuell einen Front-Node vor die einfach verkettete Liste setzen, der auch als Head-Node bezeichnet werden kann, aber nicht absolut ist:

Die allgemeine verkettete Listenstruktur ist in die folgenden Typen unterteilt:

  • Einfach verkettete Liste : Jeder Knoten in der verketteten Liste hat einen und nur einen Zeiger auf den nächsten Knoten, und der letzte Knoten zeigt auf null.
  • Doppelt verknüpfte Liste : Jeder Knoten hat zwei Zeiger (der Einfachheit halber nennen wir ihn einen vorderen Zeiger und einen hinteren Zeiger ), die auf den vorherigen Knoten bzw. den nächsten Knoten zeigen, der vordere Zeiger des ersten Knotens zeigt auf und der hintere Zeiger NULLdes letzten Knotens zeigt auf Zeiger aufNULL
  • Zirkulär verkettete Liste : Der Zeiger jedes Knotens zeigt auf den nächsten Knoten und der Zeiger des letzten Knotens zeigt auf den ersten Knoten (obwohl es sich um eine kreisförmig verkettete Liste handelt, ist es notwendig, den Kopf- oder Endknoten zu identifizieren, wenn dies erforderlich ist Endlosschleife vermeiden)
  • Komplexe verkettete Liste : Jede verkettete Liste hat einen Rückwärtszeiger, der auf den nächsten Knoten zeigt, und einen Zufallszeiger, der auf einen beliebigen Knoten zeigt.

Zeitkomplexität der verknüpften Listenoperation:

  • Abfrage: O(n), müssen die verknüpfte Liste durchlaufen
  • Einfügen: O(1), ändern Sie den Zeiger davor und danach
  • Löschen: O(1), das gleiche ist der Zeiger vor und nach der Änderung
  • Änderung: Wenn keine Abfrage O(1)erforderlich ist, muss sie abgefragt werdenO(n)

Wie stellt man den Strukturcode einer verketteten Liste dar?

Das Folgende stellt nur die einfach verkettete Listenstruktur dar, das C++heißt:

// 结点
typedef struct LNode{
  // 数据
  ElemType data;
  // 下一个节点的指针
  struct LNode *next;
}*Link,*Position;

// 链表
typedef struct{
  // 头结点,尾节点
  Link head,tail;
  // 长度
  int len;
}LinkList;

JavaDer Code sagt:

    public class ListNode {
        int val;
        ListNode next = null;

        ListNode(int val) {
            this.val = val;
        }
    }

Implementieren Sie selbst eine einfache verknüpfte Liste und implementieren Sie die Funktion zum Hinzufügen, Löschen, Ändern und Überprüfen:

class ListNode<T> {
    T val;
    ListNode next = null;

    ListNode(T val) {
        this.val = val;
    }
}

public class MyList<T> {
    private ListNode<T> head;
    private ListNode<T> tail;
    private int size;

    public MyList() {
        this.head = null;
        this.tail = null;
        this.size = 0;
    }

    public void add(T element) {
        add(size, element);
    }

    public void add(int index, T element) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("超出链表长度范围");
        }
        ListNode current = new ListNode(element);
        if (index == 0) {
            if (head == null) {
                head = current;
                tail = current;
            } else {
                current.next = head;
                head = current;
            }
        } else if (index == size) {
            tail.next = current;
            tail = current;
        } else {
            ListNode preNode = get(index - 1);
            current.next = preNode.next;
            preNode.next = current;
        }
        size++;
    }

    public ListNode get(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("超出链表长度");
        }
        ListNode temp = head;
        for (int i = 0; i < index; i++) {
            temp = temp.next;
        }
        return temp;
    }

    public ListNode delete(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("超出链表节点范围");
        }
        ListNode node = null;
        if (index == 0) {
            node = head;
            head = head.next;
        } else if (index == size - 1) {
            ListNode preNode = get(index - 1);
            node = tail;
            preNode.next = null;
            tail = preNode;
        } else {
            ListNode pre = get(index - 1);
            pre.next = pre.next.next;
            node = pre.next;
        }
        size--;
        return node;
    }

    public void update(int index, T element) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("超出链表节点范围");
        }
        ListNode node = get(index);
        node.val = element;
    }

    public void display() {
        ListNode temp = head;
        while (temp != null) {
            System.out.print(temp.val + " -> ");
            temp = temp.next;
        }
        System.out.println("");
    }
}

Der Testcode lautet wie folgt:

public class Test {
    public static void main(String[] args) {
        MyList myList = new MyList();
        myList.add(1);
        myList.add(2);
        // 1->2
        myList.display();

        // 1
        System.out.println(myList.get(0).val);

        myList.update(1,3);
        // 1->3
        myList.display();

        myList.add(4);
        // 1->3->4
        myList.display();

        myList.delete(1);
        // 1->4
        myList.display();
    }
}

Ausgabeergebnis:

1 -> 2 -> 
1
1 -> 3 -> 
1 -> 3 -> 4 -> 
1 -> 4 ->

Die Suche und Aktualisierung der einfach verketteten Liste ist relativ einfach. Schauen wir uns den konkreten Vorgang des Einfügens eines neuen Knotens an (hier wird nur das Einfügen an der mittleren Position gezeigt, und das Einfügen von Kopf und Ende ist relativ einfach):

Wie lösche ich einen Zwischenknoten? Das Folgende ist der spezifische Prozess:

Bild-20220108114627633

Vielleicht werden Sie neugierig sein, der a5Knoten hat einfach keinen Zeiger, also wohin geht er?

Wenn es sich um ein JavaProgramm handelt, sammelt der Garbage Collector solche nicht referenzierten Knoten und hilft uns, diesen Teil des Speichers zu recyceln, aber um die Garbage Collection zu beschleunigen, müssen wir im Allgemeinen die nicht benötigten Knoten node = nullleeren im C++Programm , dann müssen Sie manuell recyceln, da es sonst leicht zu Problemen wie Speicherlecks kommen kann.

Die Funktionsweise der komplexen verketteten Liste wird hier kurz erwähnt. Später werde ich die Datenstruktur und die gemeinsamen Algorithmen der verketteten Liste separat teilen. Dieser Artikel spricht hauptsächlich über das Gesamtbild der Datenstruktur.

Tabelle überspringen

Wir können oben beobachten, dass es sehr mühsam ist, wenn die verknüpfte Liste durchsucht wird. Wenn dieser Knoten am Ende ist, muss er alle Knoten durchlaufen, um ihn zu finden. Die Sucheffizienz ist zu gering. Gibt es einen guten Weg?

Es gibt immer mehr Lösungen als Probleme, aber 多快好省es gibt kein absolutes "". Es gibt etwas zu geben. Die Computerwelt ist voller philosophischer Aromen. Da es ein Problem mit der Sucheffizienz gibt, können wir die verknüpfte Liste genauso gut sortieren. Die sortierte verkettete Liste kennt immer noch nur die Kopf- und Endknoten und den Bereich in der Mitte, aber um den mittleren Knoten zu finden, müssen Sie immer noch den alten Weg der Traversierung gehen. Was ist, wenn wir Zwischenknoten speichern? Speichern Sie es, wir wissen wirklich, dass die Daten in der ersten Hälfte oder in der zweiten Hälfte sind. Um beispielsweise zu finden 7, muss es beim mittleren Knoten beginnen. Wenn Sie suchen 4, müssen Sie von vorne beginnen, und wenn Sie im schlimmsten Fall den Zwischenknoten erreichen, brechen Sie die Suche ab.

Ganz gelöst ist das Problem aber noch nicht, da die verlinkte Liste sehr lang ist und nur die beiden Teile vorher und nachher durchsucht werden können. Es ist besser, zum Prinzip zurückzukehren: 空间和时间,我们选择时间,那就要舍弃一部分空间Wir fügen jedem Knoten einen Zeiger hinzu, und jetzt gibt es zwei Ebenen von Zeigern (Anmerkung: Es gibt nur eine Kopie des Knotens, die alle derselbe Knoten sind, nur der Einfachheit halber des Aussehens habe ich zwei Kopien erstellt, die eigentlich derselbe Knoten sind. Es gibt zwei Zeiger, sagen wir 1, die sowohl auf 2 als auch auf 5 zeigen ):

Zwei Ebenen von Zeigern, das Problem besteht immer noch, dann fügen Sie weitere Ebenen hinzu, fügen Sie beispielsweise eine Ebene für jeweils zwei Knoten hinzu:

Dies ist die Skip-Tabelle. Die Definition der Skip-Tabelle lautet wie folgt:

Die Skip-Liste (SkipList, der vollständige Name der Skip-Liste) ist eine Datenstruktur, die für die schnelle Suche und die Suche nach geordneten Elementsequenzen verwendet wird. Die Skip-Liste ist eine randomisierte Datenstruktur, die im Wesentlichen eine geordnete verkettete Liste ist, die eine binäre Suche durchführen kann . Die Sprungliste fügt der ursprünglich geordneten verketteten Liste einen mehrstufigen Index hinzu und verwendet den Index, um eine schnelle Suche zu erreichen. Das Überspringen von Tabellen kann nicht nur die Suchleistung verbessern, sondern auch die Leistung von Einfüge- und Löschvorgängen. Es ist in der Leistung mit Rot-Schwarz-Baum und AVL-Baum vergleichbar, aber das Prinzip der Sprungtabelle ist sehr einfach und die Implementierung ist viel einfacher als Rot-Schwarz-Baum.

Das Hauptprinzip besteht darin, Raum gegen Zeit auszutauschen, was fast die Effizienz der binären Suche erreichen kann.Tatsächlich 1 + 2 + 4 + ... + n = 2n-1verdoppelt sich der verbrauchte Raum fast, wenn man annimmt,dass alle zwei Schichten hinzugefügt werden. Glaubst du, es sieht aus wie das Inhaltsverzeichnis eines Buches, ein Verzeichnis der ersten Ebene, ein Verzeichnis der zweiten Ebene, ein Verzeichnis der dritten Ebene...

Wenn wir weiterhin Daten in die Skip-Tabelle einfügen, kann es vorkommen, dass in einem bestimmten Segment zu viele Knoten vorhanden sind. Zu diesem Zeitpunkt müssen wir den Index dynamisch aktualisieren. Zusätzlich zum Einfügen von Daten müssen wir ihn auch einfügen in die verknüpfte Liste der vorherigen Schicht, um die Abfrageeffizienz sicherzustellen.

redisUm dies zu erreichen, wird die Skip-Tabelle verwendet zset, redisund ein Zufallsalgorithmus wird verwendet, um die Ebene zu berechnen und wie viele Ebenen von Indizes für jeden Knoten berechnet werden.Obwohl der Vergleich nicht absolut garantiert werden kann, ist die Effizienz grundsätzlich garantiert, und es ist effizienter als diese balancierten Bäume und Rot-Schwarz-Bäume.Der Algorithmus ist einfacher.

Stapel

Ein Stack ist eine Datenstruktur, in Javader StackKlassen verkörpert sind. Seine Essenz ist First-in, Last-out , wie ein Eimer, der nur kontinuierlich darauf gestellt werden kann, und wenn er herausgenommen wird, können nur die obersten Daten kontinuierlich entnommen werden. Wenn Sie die Daten unten entnehmen möchten, können Sie dies nur tun, wenn die Daten oben entnommen sind. Wenn es einen solchen Bedarf gibt, verwenden wir natürlich im Allgemeinen eine Zwei-Wege-Warteschlange.

Das Folgende ist eine Demonstration der Eigenschaften des Stapels:

Wofür wird die unterste Schicht des Stapels verwendet? Tatsächlich können Sie eine verknüpfte Liste oder ein Array verwenden, aber JDKder zugrunde liegende Stack wird mit einem Array implementiert. Nach der Kapselung kann APInur das letzte Element manipuliert werden. Der Stack wird häufig verwendet, um rekursive Funktionen zu implementieren. Wenn Sie den Stack oder andere Sammlungsimplementierungsanalysen darin verstehen möchten Java, können Sie sich diese Artikelserie ansehen: http://aphysia.cn/categories/collection

Elemente werden dem Stapel hinzugefügt (gepusht) und Elemente werden aus dem Stapel genommen, und das oberste Element des Stapels ist das letzte Element, das auf den Stapel gelegt wurde.

Verwenden Sie ein Array, um einen einfachen Stack zu implementieren (beachten Sie, dass dies nur für Referenztests dient, es wird tatsächlich Threadsicherheit und andere Probleme geben):

import java.util.Arrays;

public class MyStack<T> {
    private T[] data;
    private int length = 2;
    private int maxIndex;

    public MyStack() {
        data = (T[]) new Object[length];
        maxIndex = -1;
    }

    public void push(T element) {
        if (isFull()) {
            length = 2 * length;
            data = Arrays.copyOf(data, length);
        }
        data[maxIndex + 1] = element;
        maxIndex++;
    }

    public T pop() {
        if (isEmpty()) {
            throw new IndexOutOfBoundsException("栈内没有数据");
        } else {
            T[] newdata = (T[]) new Object[data.length - 1];
            for (int i = 0; i < data.length - 1; i++) {
                newdata[i] = data[i];
            }
            T element = data[maxIndex];
            maxIndex--;
            data = newdata;
            return element;
        }
    }

    private boolean isFull() {
        return data.length - 1 == maxIndex;
    }

    public boolean isEmpty() {
        return maxIndex == -1;
    }

    public void display() {
        for (int i = 0; i < data.length; i++) {
            System.out.print(data[i]+" ");
        }
        System.out.println("");
    }
}

Testcode:

public class MyStackTest {
    public static void main(String[] args) {
        MyStack<Integer> myStack = new MyStack<>();
        myStack.push(1);
        myStack.push(2);
        myStack.push(3);
        myStack.push(4);
        myStack.display();

        System.out.println(myStack.pop());

        myStack.display();

    }
}

Die Ausgabe sieht wie erwartet wie folgt aus:

1 2 3 4 
4
1 2 3 

Die Eigenschaft des Stacks ist First-In, Last-Out, aber wenn die vorherigen Daten zufällig entnommen werden müssen, ist die Effizienz relativ gering und sie müssen geräumt werden Java.

Warteschlange

Da wir vor uns eine First-In-Last-Out-Datenstruktur haben, müssen wir auch eine First-In-First-Out-Datenstruktur haben.Während der Epidemie wurde schätzungsweise jeder in der Warteschlange getestet Nukleinsäuren. Die Schlange ist lang, und der Erste in der Reihe wird zuerst getestet und der Letzte in der Reihe. Testen, das weiß jeder.

Eine Warteschlange ist eine spezielle Art von linearer Tabelle . Die Besonderheit besteht darin, dass sie nur Löschoperationen am Anfang der Tabelle und Einfügeoperationen an der Rückseite der Tabelle zulässt. Warteschlangen sind wie Stacks eine Operation, die den Bedingungen der linearen Tabelle unterliegt. Das Ende, das die Einfügeoperation durchführt, wird als Ende der Warteschlange bezeichnet, und das Ende, das die Löschoperation durchführt, wird als Kopf der Warteschlange bezeichnet.

Warteschlangen sind durch First-in, First-out gekennzeichnet, die folgenden sind Beispiele:

Im Allgemeinen werden Sie, solange Sie von First-In, First-Out ( FIFO), dem vollständigen Namen First In First Out, sprechen, an eine Warteschlange denken, aber wenn Sie eine Warteschlange haben möchten, die Elemente aus dem Kopf der Warteschlange aufnehmen und übernehmen kann Elemente aus dem Ende der Warteschlange, müssen Sie eine spezielle Warteschlange (Zwei-Wege-Warteschlange) verwenden. ), die Zwei-Wege-Warteschlange ist im Allgemeinen einfacher zu implementieren, wenn Sie eine doppelt verkettete Liste verwenden.

Im Folgenden implementieren wir Javaeine einfache Einwegwarteschlange:

class Node<T> {
    public T data;
    public Node next;

    public Node(T data) {
        this.data = data;
    }
}

public class MyQueue<T> {
    private Node<T>  head;
    private Node<T>  rear;
    private int size;

    public MyQueue() {
        size = 0;
    }

    public void pushBack(T element) {
        Node newNode = new Node(element);
        if (isEmpty()) {
            head = newNode;
        } else {
            rear.next = newNode;
        }
        rear = newNode;
        size++;
    }

    public boolean isEmpty() {
        return head == null;
    }

    public T popFront() {
        if (isEmpty()) {
            throw new NullPointerException("队列没有数据");
        } else {
            Node<T> node = head;
            head = head.next;
            size--;
            return node.data;
        }
    }

    public void dispaly() {
        Node temp = head;
        while (temp != null) {
            System.out.print(temp.data +" -> ");
            temp = temp.next;
        }
        System.out.println("");
    }
}

Der Testcode lautet wie folgt:

public class MyStackTest {
    public static void main(String[] args) {
        MyStack<Integer> myStack = new MyStack<>();
        myStack.push(1);
        myStack.push(2);
        myStack.push(3);
        myStack.push(4);
        myStack.display();

        System.out.println(myStack.pop());

        myStack.display();

    }
}

Operationsergebnis:

1 -> 2 -> 3 -> 
1
2 -> 3 -> 
2
3 -> 

Häufig verwendete Warteschlangentypen sind wie folgt:

  • One-Way-Warteschlange: das heißt, was wir eine normale Warteschlange nennen, zuerst rein, zuerst raus.

  • Zwei-Wege-Warteschlange: kann die Warteschlange aus verschiedenen Richtungen betreten und verlassen

  • Prioritätswarteschlange: Der Innenraum wird automatisch sortiert und die Warteschlange in einer bestimmten Reihenfolge eingereiht

  • Sperrwarteschlange: Wenn ein Element aus der Warteschlange genommen wird, wird die Warteschlange blockiert, wenn kein Element vorhanden ist. Wenn die Warteschlange voll ist, wird auch das Einfügen von Elementen in die Warteschlange blockiert.

  • Zirkuläre Warteschlange: Kann als kreisförmige verkettete Liste verstanden werden, aber es ist im Allgemeinen notwendig, die Kopf- und Endknoten zu identifizieren, um Endlosschleifen zu verhindern, und der Endknoten nextzeigt auf den Kopfknoten.

Warteschlangen können im Allgemeinen verwendet werden, um Daten zu speichern, die geordnet werden müssen, oder um Aufgaben zu speichern. Beim Traversieren auf Baumebene können Warteschlangen verwendet werden, um sie zu lösen. Im Allgemeinen können Breitensuchen mit Warteschlangen gelöst werden.

Hash-tabelle

Die vorherige Datenstruktur wird bei der Suche im Allgemeinen =oder verwendet und !=kann bei der Suche nach halben oder anderen Bereichsabfragen verwendet werden . Idealerweise hoffen wir auf jeden Fall, eine bestimmte Position ohne Vergleich direkt zu lokalisieren. (Speicherort), im Array, Elemente können per Index abgerufen werden. Wenn wir also die zu speichernden Daten mit dem Index des Arrays abgleichen und es sich um eine Eins-zu-eins-Beziehung handelt, können wir dann nicht schnell die Position des Elements finden?<>

Solange f(k)Sie kdie entsprechende Position über eine Funktion finden können, ist diese Funktion f(k)eine hashFunktion. Es stellt eine Abbildungsbeziehung dar, aber für unterschiedliche Werte kann es auf denselben Wert (dieselbe hashAdresse) abgebildet werden, d . h f(k1) = f(k2). wir nennen dieses Phänomen 冲突oder 碰撞.

hashDie Tabelle ist wie folgt definiert:

Die Hash-Tabelle (auch Hash-Tabelle genannt) ist eine Datenstruktur, die gemäß dem Schlüssel direkt auf den Speicherort zugreift. Das heißt, es greift auf Datensätze zu, indem es eine Funktion für den Schlüsselwert berechnet, die die abzufragenden Daten einer Position in der Tabelle zuordnet, was die Suche beschleunigt. Diese Zuordnungsfunktion wird als Hash-Funktion bezeichnet, und das Array von Datensätzen wird als Hash-Tabelle bezeichnet.

Häufig verwendete hashFunktionen sind:

  • Direkte Adressierungsmethode: Nehmen Sie das Schlüsselwort oder den Wert einer linearen Funktion des Schlüsselworts als Hash-Funktion heraus, z. B. H(key) = keyoderH(key) = a * key + b
  • Numerisches Analyseverfahren: Für alle möglichen Werte bilden mehrere Ziffern des Schlüsselworts eine Hash-Adresse
  • Die quadratische Methode: Nehmen Sie die mittleren Ziffern nach dem Quadrat des Schlüsselworts als Hash-Adresse
  • Faltmethode: Teilen Sie das Schlüsselwort in mehrere Teile mit gleicher Stellenzahl (die Stellenzahl im letzten Teil kann unterschiedlich sein) und nehmen Sie die Superpositionssumme dieser Teile (abgerundet) als Hash-Adresse.
  • Rest der Teilungsmethode: Nimm den Rest, den man erhält, nachdem das Schlüsselwort durch eine mZahl dividiert wird, die nicht größer als die Länge der Hash-Tabellentabelle ist, pals Hash-Adresse. d.h. h ash(k)=k mod p, p< =m. Das Schlüsselwort kann nicht nur direkt Modulo sein, sondern Modulo kann auch nach Operationen wie der Faltungsmethode und der Quadratmethode verwendet werden. Die richtige Wahl ist sehr wichtig.Im pAllgemeinen wird eine Primzahl oder eine Primzahl verwendet.Wenn die Wahl nicht gut ist, kann es leicht zu Konflikten kommen.mp
  • Zufallszahlenmethode: Nehmen Sie den Zufallsfunktionswert des Schlüsselworts als seine Hash-Adresse.

Keine dieser Methoden kann jedoch Hash-Kollisionen vermeiden und kann nur bewusst reduziert werden. hashWelche Möglichkeiten gibt es also, mit Konflikten umzugehen ?

  • Offene Adressmethode: hashWenn nach der Berechnung bereits Daten im Ort vorhanden sind +1, weiß die Adresse , dh rückblickend, einen leeren Ort zu finden.
  • Re- hashMethode: Nachdem ein Hash-Konflikt auftritt, können Sie eine andere hashFunktion verwenden, um den Pol neu zu berechnen, um eine leere hashAdresse zu finden, und wenn es eine gibt, können Sie die hashFunktion auch überlagern.
  • Kettenadressmethode: Alle hashWerte sind gleich, und der Link wird zu einer verknüpften Liste, die hinter dem Array hängt.
  • Richten Sie einen gemeinsamen Überlaufbereich ein: nicht gemeinsam, was bedeutet, dass alle Elemente, die hashmit Elementen in der Tabelle in Konflikt geraten, eine andere Tabelle erhalten, die auch als Überlauftabelle bezeichnet wird.

JavaIm Inneren wird die Kettenadressmethode verwendet:

Wenn hashder Konflikt jedoch schwerwiegend ist, wird die verknüpfte Liste relativ lang. Beim Abfragen müssen Sie die folgende verknüpfte Liste durchlaufen. Daher wird JDKeine Version optimiert. Wenn die Länge der verknüpften Liste den Schwellenwert überschreitet, wird sie zu einer Rot-Schwarz- Baum Der Rot-Schwarz-Baum hat bestimmte Regeln zum Ausgleichen des Teilbaums, um zu vermeiden, dass er zu einer verknüpften Liste degeneriert, was die Abfrageeffizienz beeinträchtigt.

Aber Sie werden bestimmt denken, was ist, wenn das Array zu klein ist und mehr Daten platziert werden? Die Wahrscheinlichkeit, dass der Konflikt erneut abgespielt wird, wird immer größer. Tatsächlich wird zu diesem Zeitpunkt ein Erweiterungsmechanismus ausgelöst, das Array wird auf die 2doppelte und die hashvorherigen Daten werden in ein anderes Array gehasht.

hashDer Vorteil der Tabelle besteht darin, dass die Lookup-Geschwindigkeit schnell ist, aber wenn der Re-Trigger ständig ausgelöst hashwird, ist auch die Reaktionsgeschwindigkeit langsam. Auch wenn Sie Bereichsabfragen wünschen, hashsind Tabellen keine gute Wahl.

Baum

Arrays und verknüpfte Listen sind beides lineare Strukturen, während der hier einzuführende Baum eine nichtlineare Struktur ist. In Wirklichkeit ist der Baum eine Pyramidenstruktur, und der Baum in der Datenstruktur wird als oberster Wurzelknoten bezeichnet.

Wie definieren wir die Baumstruktur?

Ein Baum ist eine Datenstruktur , die aus n (n≥1 ) endlichen Knoten besteht, um eine Menge mit hierarchischer Beziehung zu bilden . Er wird „Baum“ genannt, weil er wie ein umgedrehter Baum aussieht, was bedeutet, dass er die Wurzeln nach oben und die Blätter nach unten hat. Es hat die folgenden Eigenschaften:

Jeder Knoten hat null oder mehr untergeordnete Knoten; ein Knoten ohne übergeordneten Knoten wird als Wurzelknoten bezeichnet; jeder Nicht-Root-Knoten hat einen und nur einen übergeordneten Knoten; mit Ausnahme des Wurzelknotens kann jeder untergeordnete Knoten in mehrere disjunkte untergeordnete Knoten unterteilt werden Baum . (Baidu-Enzyklopädie)

Das Folgende sind die Grundbegriffe für Bäume (aus der Data Structure CLanguage Edition der Tsinghua University):

  • Grad eines Knotens: Die Anzahl der in einem Knoten enthaltenen Teilbäume wird als Grad des Knotens bezeichnet
  • Der Grad des Baums: In einem Baum wird der größte Knotengrad als Grad des Baums bezeichnet;
  • Blattknoten oder Endknoten: Knoten mit Grad Null;
  • Nichtterminale Knoten oder Verzweigungsknoten: Knoten, deren Grad nicht Null ist;
  • Elternknoten oder Elternknoten: Wenn ein Knoten Kindknoten enthält, wird der Knoten Elternknoten seiner Kindknoten genannt;
  • Kindknoten oder Kindknoten: Der Wurzelknoten des in einem Knoten enthaltenen Teilbaums wird als Kindknoten des Knotens bezeichnet;
  • Geschwisterknoten: Knoten mit demselben Elternknoten werden als Geschwisterknoten bezeichnet;
  • Die Ebene des Knotens: Ausgehend von der Definition der Wurzel ist die Wurzel die erste 1Schicht, die untergeordneten Knoten der Wurzel sind die erste 2Schicht und so weiter;
  • Tiefe: Für jeden Knoten nist ndie Tiefe die Länge des eindeutigen Pfads von der Wurzel zu n, und die Tiefe der Wurzel ist 0;
  • Höhe: Für jeden Knoten nist ndie Höhe ndie Länge des längsten Pfads von zu einem Blatt, und die Höhe aller Blätter ist 0;
  • Cousin-Knoten: Knoten, deren übergeordnete Knoten sich in derselben Schicht befinden, sind Cousins ​​​​voneinander;
  • Vorfahren eines Knotens: alle Knoten auf dem Zweig von der Wurzel bis zum Knoten;
  • Nachkommen: Jeder Knoten im Teilbaum, der an einem Knoten verwurzelt ist, wird als Nachkomme des Knotens bezeichnet.
  • Geordneter Baum: Die Teilbäume der Knoten der Baumarten werden als von links nach rechts geordnet betrachtet (nicht vertauschbar), dann ist der Baum als geordneter Baum zu bezeichnen, ansonsten ist er ein ungeordneter Baum
  • Erstes Kind: Die Wurzel des Teilbaums ganz links in einem geordneten Baum wird als erstes Kind bezeichnet
  • Letztes Kind: Die Wurzel des Teilbaums ganz rechts in einem geordneten Baum wird als letztes Kind bezeichnet
  • Wald: Eine Ansammlung von m( m>=0) disjunkten Bäumen wird Wald genannt;

Bäume, tatsächlich verwenden wir am häufigsten binäre Bäume:

Das Merkmal eines Binärbaums ist, dass jeder Knoten höchstens zwei Teilbäume hat, und die Teilbäume in links und rechts unterteilt sind und die Reihenfolge der linken und rechten untergeordneten Knoten nicht beliebig umgekehrt werden kann.

Ein binärer Baum Javawird dargestellt in:

public class TreeLinkNode {
    int val;
    TreeLinkNode left = null;
    TreeLinkNode right = null;
    TreeLinkNode next = null;

    TreeLinkNode(int val) {
        this.val = val;
    }
}

Vollständiger Binärbaum: Ein Binärbaum mit der Tiefe k und 2<sup>k</sup>-1 Knoten wird als vollständiger Binärbaum bezeichnet

Vollständiger Binärbaum: Ein Binärbaum der Tiefe k mit n Knoten, wenn und nur wenn jeder Knoten einem von 1 bis n nummerierten Knoten in einem vollständigen Binärbaum der Tiefe k entspricht, wird er als vollständiger Binärbaum bezeichnet.

Es gibt mehrere Arten der Traversierung eines allgemeinen Binärbaums:

  • Traversierung der Vorbestellung: Durchlaufen Sie den Wurzelknoten der Bestellung --> linker untergeordneter Knoten --> rechter untergeordneter Knoten
  • Traversierung in der Reihenfolge: Traversierungsreihenfolge linker untergeordneter Knoten --> Wurzelknoten --> rechter untergeordneter Knoten
  • Traversierung nach der Reihenfolge: Traversierungsreihenfolge linker untergeordneter Knoten --> rechter untergeordneter Knoten --> Wurzelknoten
  • Breiten-/Ebenentraversierung: Traversierung von oben nach unten, Schicht für Schicht

Wenn es sich um einen chaotischen Binärbaum handelt, ist die Effizienz des Suchens oder Suchens relativ gering, und es unterscheidet sich nicht von einer chaotischen verknüpften Liste. Warum also eine kompliziertere Struktur?

Tatsächlich kann der Binärbaum beim Sortieren oder Suchen verwendet werden, da der Binärbaum strikte linke und rechte Unterbäume hat, wir können die Größe des Wurzelknotens, des linken untergeordneten Knotens und des rechten untergeordneten Knotens definieren. Es gibt also einen binären Suchbaum:

Binärer Suchbaum, (auch: Binärer Suchbaum, Binärer Sortierbaum) Es ist entweder ein leerer Baum oder ein binärer Baum mit den folgenden Eigenschaften : Wenn sein linker Teilbaum nicht leer ist, dann der linke Der Wert aller Knoten auf dem Teilbaum ist kleiner als der Wert seines Wurzelknotens ; wenn sein rechter Teilbaum nicht leer ist, ist der Wert aller Knoten auf dem rechten Teilbaum größer als der Wert seines Wurzelknotens; sein linker, der rechte Teilbaum ist ebenfalls ein binär sortierter Baum , bzw. . Als klassische Datenstruktur hat der binäre Suchbaum nicht nur die Eigenschaften des schnellen Einfügens und Löschens von verketteten Listen, sondern auch die Vorteile der schnellen Array-Suche und ist daher weit verbreitet, beispielsweise Dateisysteme und Datenbanksysteme verwenden dies im Allgemeinen eine Art Baum Datenstrukturen für effiziente Sortier- und Abrufoperationen.

Ein Beispiel für einen binären Suchbaum sieht wie folgt aus:

Wenn wir zum Beispiel den obigen Baum finden müssen, beginnen4 Sie mit , gehen Sie zum linken Teilbaum, finden Sie ihn , gehen Sie zum rechten Teilbaum, finden Sie ihn , das heißt, einen Baum von Knoten, suchen wir nur mal, das heißt, die Anzahl von Schichten, unter der Annahme eines Knotens, das heißt .545343473nlog(n+1)

Wenn der Baum gut gepflegt wird, ist die Abfrageeffizienz hoch, aber wenn der Baum nicht gut gepflegt wird, wird er leicht zu einer verknüpften Liste degenerieren, und die Abfrageeffizienz wird auch abnehmen, zum Beispiel:

Ein abfragefreundlicher Binärbaum sollte ein ausgeglichener oder nahezu ausgeglichener Binärbaum sein.Was ist ein ausgeglichener Binärbaum:

Die Höhen der linken und rechten Teilbäume jedes Knotens in einem balancierten binären Suchbaum unterscheiden sich um höchstens 1. Ein balancierter Binärbaum wird auch als AVL-Baum bezeichnet.

Damit der Binärbaum nach dem Einfügen oder Löschen von Daten etc. immer noch ein balancierter Binärbaum ist, ist ein Abgleich der Knoten erforderlich, auch Balancing-Prozess genannt, der verschiedene Rotationsanpassungen beinhaltet, die nicht erweitert werden hier vorerst.

Wenn jedoch eine große Anzahl von Aktualisierungen, Löschungen und verschiedene Anpassungen zum Ausgleich von Baumarten viel Leistung opfern müssen, um dieses Problem zu lösen, schlugen einige Bosse rot-schwarze Bäume vor.

Red Black Tree (Red Black Tree) ist ein selbstausgleichender binärer Suchbaum, eine in der Informatik verwendete Datenstruktur , die typischerweise zur Implementierung assoziativer Arrays verwendet wird . [1]

Rot-Schwarz-Bäume wurden 1972 von [Rudolf Bayer] ( https://baike.baidu.com/item/Rudolf Bayer/3014716) erfunden und damals als symmetrische binäre B-Bäume bezeichnet. Später wurde es 1978 von Leo J. Guibas und Robert Sedgewick zum heutigen "rot-schwarzen Baum" modifiziert. [2]

Ein Rot-Schwarz-Baum ist ein spezialisierter AVL-Baum ( Balanced Binary Tree ), der das Gleichgewicht des binären Suchbaums durch bestimmte Operationen während Einfüge- und Löschoperationen aufrechterhält, um eine hohe Suchleistung zu erzielen.

Ein rot-schwarzer Baum hat folgende Eigenschaften:

  • Eigenschaften 1. Knoten sind rot oder schwarz.

  • Eigenschaft 2. Der Wurzelknoten ist schwarz.

  • Eigenschaft 3. Alle Blätter sind schwarz. (Blätter sind NIL-Knoten)

  • Eigenschaft 4. Beide Kinder jedes roten Knotens sind schwarz. (Es können nicht zwei aufeinanderfolgende rote Knoten auf allen Pfaden von jedem Blatt zur Wurzel sein)

  • Eigenschaft 5. Alle Pfade von jedem Knoten zu jedem seiner Blätter enthalten die gleiche Anzahl schwarzer Knoten.

Es sind diese Eigenschaften, die die Anpassung des Rot-Schwarz-Baums nicht so schwierig und häufig machen wie die Anpassung des gewöhnlichen ausgeglichenen Binärbaums. Das heißt, es werden Regeln hinzugefügt, damit es bestimmte Standards erfüllt und die Verwirrung und Häufigkeit des Ausgleichsprozesses verringert.

Die Implementierung der oben erwähnten Hash-Tabelle Javastellt genau die Anwendung des Rot-Schwarz-Baums dar. Bei hashvielen Konflikten wird die verkettete Liste in einen Rot-Schwarz-Baum umgewandelt.

Alle oben genannten sind binäre Bäume, aber wir müssen Multi-Fork-Bäume rippen, warum? Zwar sind die diversen Suchbäume im Binärbaum, der Rot-Schwarz-Baum schon sehr gut, aber bei der Interaktion mit der Disk, die meisten davon im Datenspeicher, müssen wir den IO-Faktor berücksichtigen, da die Disk-IO deutlich langsamer ist als die Erinnerung. Wenn die Ebene des Indexbaums Zehntausende beträgt, ist die Anzahl der Lesevorgänge auf der Festplatte zu hoch. B-Bäume sind besser geeignet für Plattenspeicherung.

Im Jahr 970 schlugen R.Bayer und E.mccreight einen für die äußere Suche geeigneten Baum vor , der ein balancierter Baum mit mehreren Gabeln namens B-Baum (oder B-Baum, B_Baum) ist.

Ein balancierter Baum der Ordnung m ist ein balancierter m-Wege-Suchbaum. Es ist entweder ein leerer Baum oder ein Baum, der die folgenden Eigenschaften erfüllt:

1. Der Wurzelknoten hat mindestens zwei Kinder;

2. Die Anzahl j von Schlüsselwörtern, die in jedem Nicht-Stammknoten enthalten sind, erfüllt: m/2 – 1 <= j <= m – 1;

3. Der Grad aller Knoten außer dem Wurzelknoten (ohne Blattknoten) ist genau die Gesamtzahl der Schlüsselwörter plus 1, also erfüllt die Anzahl der internen Teilbäume k: m/2 <= k <= m ;

4. Alle Blattknoten befinden sich in derselben Schicht.

Jeder Knoten stellt etwas mehr Daten dar. Bei der Suche ist die Operation im Arbeitsspeicher viel schneller als auf der Festplatte, und der bBaum kann die Anzahl der Festplatten-E/A reduzieren. B-Baum:

Und jeder Knoten datakann sehr groß sein, was dazu führt, dass auf jeder Seite nur sehr wenige Daten gefunden werden, und die Anzahl der IO-Anfragen wird natürlich zunehmen.Dann können wir genauso gut nur Daten in Blattknoten speichern:

Der B+-Baum ist eine Variante des B-Baums. Die Blattknoten des B+-Baums speichern Schlüsselwörter und Adressen entsprechender Datensätze, und die Schichten über den Blattknoten werden als Indizes verwendet. Ein B+-Baum der Ordnung m ist wie folgt definiert:

(1) Jeder Knoten hat höchstens m Kinder;

(2) Mit Ausnahme des Wurzelknotens hat jeder Knoten mindestens [m/2] Kinder, und der Wurzelknoten hat mindestens zwei Kinder;

(3) Ein Knoten mit k Kindern muss k Schlüsselwörter haben.

Im Allgemeinen sind die Blattknoten des b+-Baums durch eine verknüpfte Liste verbunden, was für das Durchlaufen und das Durchlaufen von Bereichen geeignet ist.

Das ist der Baum Der Baum hat gegenüber b+dem Baum folgende Vorteile:b+B树

  1. b+Die Zwischenknoten des Baums speichern keine Daten, und jede IO-Abfrage kann mehr Indizes finden, was ein Squat-Baum ist.
  2. Für die Bereichssuche muss der b+Baum nur die verknüpfte Liste von Blattknoten durchlaufen, baber der Baum muss vom Wurzelknoten bis zu den Blattknoten beginnen.

Zusätzlich zu dem obigen Baum gibt es tatsächlich eine Art HuffmanBaum: Konstruieren Sie bei gegebenen N Gewichten als N Blattknoten einen Binärbaum. Wenn die gewichtete Pfadlänge des Baums das Minimum erreicht, wird ein solcher Binärbaum als optimaler Binärbaum bezeichnet , Auch bekannt als Huffman-Baum. Der Huffman-Baum ist der Baum mit der kürzesten gewichteten Pfadlänge, und der Knoten mit dem größeren Gewicht liegt näher an der Wurzel.

Es wird im Allgemeinen zur Komprimierung verwendet, da die Häufigkeit jedes Zeichens in den Daten unterschiedlich ist. Je höher die Häufigkeit des Zeichens, desto kürzer der Code, den wir zum Speichern verwenden, der Zweck der Komprimierung kann erreicht werden. Woher stammt dieser Code?

Angenommen, das Zeichen ist hello, dann kann die Codierung sein (nur ein grober Prototyp der Codierung, Hochfrequenzzeichen, die Codierung ist kürzer), die Codierung ist eine 01Zeichenfolge von Pfaden vom Wurzelknoten zum aktuellen Zeichen:

Durch Codieren unterschiedlicher Gewichte wird der Huffman-Baum effektiv komprimiert.

Haufen

Der Heap ist eigentlich eine Art Binärbaum. Der Heap muss ein vollständiger Binärbaum sein. Ein vollständiger Binärbaum ist: Mit Ausnahme der letzten Schicht ist die Anzahl der Knoten in anderen Schichten voll und die Knoten in der letzten Schicht sind konzentriert in der linken durchgehenden Position.

Der Heap hat eine weitere Anforderung: Der Wert jedes Knotens im Heap muss größer oder gleich (oder kleiner oder gleich) dem Wert seiner linken und rechten untergeordneten Knoten sein.

Es gibt zwei Haupttypen von Haufen:

  • Big-Top-Heap: Jeder Knoten ist größer oder gleich seinen Unterbaumknoten (Heap-Top ist der Maximalwert)
  • Kleiner oberer Heap: Jeder Knoten ist kleiner oder gleich seinen Teilbaumknoten (Heap-Top ist der Mindestwert)

Im Allgemeinen verwenden wir Arrays, um Heaps darzustellen, wie zum Beispiel den folgenden kleinen oberen Heap:

Bild-20220109000632499

Die Beziehung zwischen Eltern-Kind-Knoten und linken und rechten Knoten im Array ist wie folgt:

  • i der Elternteil des Knotens parent = floor((i-1)/2) (abgerundet)
  • i linkes Kind des Knotens2 * i +1
  • i rechtes Kind des Knotens2 * i + 2

Da Daten gespeichert werden, müssen Operationen wie das Einfügen und Löschen beteiligt sein. Das Einfügen und Löschen in der Halde beinhaltet eine Anpassung der Halde. Nach der Anpassung kann seine Definition erneut erfüllt werden. Dieser Anpassungsprozess wird Heapisierung genannt .

Am Beispiel der kleinen Halde soll durch die Anpassung vor allem sichergestellt werden:

  • oder vollständiger Binärbaum
  • Jeder Knoten im Heap ist kleiner oder gleich seinen linken und rechten untergeordneten Knoten

Für den kleinen oberen Stapel lautet die Anpassung: Kleine Elemente schwimmen auf und große Elemente sinken, was ein Prozess des ständigen Austauschs ist.

Der Heap kann im Allgemeinen verwendet werden, um Probleme zu lösen TOP K, oder die zuvor erwähnte Prioritätswarteschlange.

Bild

Ich bin schließlich zur Erklärung der Karte gekommen. Die Karte ist eigentlich eine zweidimensionale Ebene. Ich habe zuvor über das Minenräumen geschrieben. Man kann tatsächlich sagen, dass der gesamte Blockbereich des Minenräumens mit der Karte zusammenhängt. Ein Graph ist eine nichtlineare Datenstruktur, die hauptsächlich aus Kanten und Scheitelpunkten besteht.

Bild-20220109002114134

Gleichzeitig wird der Graph in einen gerichteten Graphen und einen ungerichteten Graphen unterteilt.Das Obige ist ein ungerichteter Graph, da die Kante nicht die Richtung angibt, sondern nur die Beziehung zwischen den beiden anzeigt, während der gerichtete Graph so ist :

Wenn jeder Knoten ein Ort und jede Kante ein Pfad ist, dann ist dies ein Kartennetz, daher werden Graphen oft verwendet, um kürzeste Entfernungen zu lösen. Werfen wir einen Blick auf die Konzepte im Zusammenhang mit dem Diagramm:

  • Vertex: Die grundlegendste Einheit des Graphen, diese Knoten
  • Kante: die Beziehung zwischen Scheitelpunkten
  • Benachbarte Scheitelpunkte: Scheitelpunkte, die direkt durch Kanten verbunden sind
  • Grad: Die Anzahl benachbarter Scheitelpunkte, mit denen ein Scheitelpunkt direkt verbunden ist
  • Gewicht: das Gewicht der Kante

Im Allgemeinen gibt es mehrere Möglichkeiten, Diagramme darzustellen:

  1. Die Adjazenzmatrix, dargestellt durch ein zweidimensionales Array, ist 1 für Verbundenheit und 0 für Getrenntheit.Wenn die Pfadlänge dargestellt wird, kann natürlich eine größere 0Zahl verwendetwerden, um die Pfadlänge darzustellen, und sie wird verwendet, um -1anzuzeigen Trennung.

Im Bild unten sind 0 und 1, 2 verbunden, wir können sehen, dass die 1. und 2. Spalte der 0. Reihe 1 sind, was darauf hinweist, dass sie verbunden sind. Ein weiterer Punkt: Der Scheitelpunkt selbst ist mit 0 markiert, was darauf hinweist, dass er nicht verbunden ist, aber in einigen Fällen kann er als verbundener Zustand angesehen werden.

  1. Nachbarschaftsliste

Die Adjazenzliste, die Speichermethode ähnelt der untergeordneten Kettendarstellung des Baums, ist eine Speicherstruktur, die sequentielle Zuordnung und Kettenzuordnung kombiniert . Wenn der dem Header-Knoten entsprechende Scheitel benachbarte Scheitel aufweist, werden die benachbarten Scheitel der Reihe nach in der einfach verketteten Liste gespeichert, auf die der Header-Knoten zeigt.

Für ungerichtete Graphen verursacht die Verwendung einer Adjazenzliste zur Speicherung auch Datenredundanz.Wenn es einen Tabellenknoten gibt, der auf C in der verknüpften Liste zeigt,auf die der Header-Knoten A zeigt, wird die verknüpfte Liste, auf die der Header-KnotenC zeigt, dies auch tun exist. Ein Tabellenknoten, der auf A zeigt.

Die Traversierung in dem Graphen wird im Allgemeinen in Breiten-Zuerst-Traversierung und Tiefen-Zuerst-Traversierung unterteilt.Breite-Zuerst-Traversierung bezieht sich auf die vorrangige Traversierungvon Scheitelpunkten, die direkt mit dem aktuellen Scheitelpunkt in Beziehung stehen,was im Allgemeinen mittels Warteschlangen implementiert wird. Die Tiefendurchquerung ist, den ganzen Weg in eine Richtung zu gehen und nicht weiter zu gehen, dh die Südwand nicht zu treffen und nicht zurückzublicken, und wird im Allgemeinen rekursiv implementiert.

Neben der Berechnung des minimalen Pfades gibt es noch ein weiteres Konzept: den minimalen Spannbaum.

Ein aufspannender Baum eines verbundenen Graphen mit n Knoten ist ein minimaler verbundener Teilgraph des ursprünglichen Graphen und enthält alle n Knoten im ursprünglichen Graphen und hat die wenigsten Kanten, die den Graphen verbunden halten. Der minimale Spannbaum kann durch den Kruskal-Algorithmus oder den Prim-Algorithmus berechnet werden.

Es gibt ein Sprichwort, dass ein Graph ein Punkt auf einer Ebene ist. Wir nehmen einen der Punkte, und die Kante, die andere Eckpunkte zusammenbringen kann, nimmt das minimale Gewicht und entfernt die redundanten Kanten, was der minimale aufspannende Baum ist.

Natürlich ist der minimale Spannbaum nicht notwendigerweise eindeutig, und es kann mehrere Ergebnisse geben.

Qin Huai@Aussichtspunkt

Die Kenntnis dieser grundlegenden Datenstrukturen ist am nützlichsten, wenn Sie Code oder Datenmodellierung schreiben und eine geeignetere auswählen können. Computer dienen den Menschen, Codes auch.Wir können nicht alle Arten von Datenstrukturen auf einmal beherrschen, aber die grundlegenden Dinge werden sich nicht viel ändern, wenn nicht eine neue Generation revolutionärer Änderungen stattfindet.

Programme bestehen aus Datenstrukturen und Algorithmen. Datenstrukturen sind wie der Eckpfeiler, der in der "Data Structure C Language"-Version mit einem Satz endet:

Um ein "gutes" Programm zu schreiben, ist es notwendig, die Eigenschaften der zu verarbeitenden Objekte und die Beziehungen zwischen den zu verarbeitenden Objekten zu analysieren, was der Hintergrund der Disziplin und Entwicklung der "Datenstruktur" ist.

【Kurze Einführung des Autors】 :
Qin Huai, Autor des öffentlichen Kontos [ Qin Huai Grocery Store ], persönliche Website: http://aphysia.cn, der Weg der Technologie ist nicht auf einmal, die Berge sind hoch und die Flüsse sind lang, auch wenn es langsam ist, es ist endlos.

Sword Point bietet alle Problemlösungen PDF

Hinweise zur Open-Source-Programmierung

{{o.name}}
{{m.name}}

Je suppose que tu aimes

Origine my.oschina.net/u/5077784/blog/5396070
conseillé
Classement