Auteur original: JeemyJohn
Adresse d'origine: Le principe et la réalisation du skip table
Lecture recommandée: Algorithme Manga: Qu'est-ce qu'une table de saut?
1. Le principe du saut de table
Quiconque a étudié la structure des données sait que la complexité temporelle de l'interrogation d'un élément dans une liste liée à une seule liaison est O (n). Même si la liste à liaison simple est ordonnée, nous ne pouvons pas réduire la complexité de temps de 2 points.
Comme le montre la figure ci-dessus, si nous voulons interroger le nœud avec l'élément 55, nous devons commencer à partir du nœud de début et boucler jusqu'au dernier nœud, à l'exclusion de -INF (infini négatif), et interroger 8 fois. Alors, quelle méthode peut être utilisée pour visiter 55 personnes avec moins de temps? Le plus intuitif, bien sûr, est un nouveau raccourci pour accéder à 55.
Comme le montre la figure ci-dessus, nous voulons interroger le nœud avec l'élément 55, et nous n'avons besoin de rechercher que 4 fois dans la couche L2. Dans cette structure, l'interrogation de l'élément avec un nœud de 46 coûtera 5 fois le plus de requêtes. C'est-à-dire, première requête 46 en L2, et recherche de l'élément 55 après 4. Comme la liste chaînée est ordonnée, 46 doit être à gauche de 55, il n'y a donc pas d'élément 46 dans la couche L2. Ensuite, nous retournons à l'élément 37 et continuons la recherche de 46 à la couche suivante, L1. Heureusement, nous n'avons besoin que d'une requête supplémentaire pour trouver 46. Cela a coûté un total de 5 requêtes.
Alors, comment pouvons-nous rechercher 55 plus rapidement? Avec l'expérience ci-dessus, il nous est facile de penser et de créer un raccourci.
Comme le montre l'image ci-dessus, nous n'avons besoin que de 2 recherches pour rechercher 55. Dans cette structure, l'élément de requête 46 est toujours celui qui prend le plus de temps, nécessitant 5 requêtes. Autrement dit, recherchez d'abord 2 fois dans la couche L3, puis recherchez 2 fois dans la couche L2, et enfin recherchez une fois dans la couche L1, un total de 5 fois. De toute évidence, cette idée est très similaire à 2 points, donc notre diagramme de structure final devrait être comme indiqué ci-dessous.
Nous pouvons voir que la visite la plus longue 46 nécessite 6 requêtes. Autrement dit, L4 accède 55, L3 accède 21, 55, L2 accède 37, 55 et L1 accède 46. Nous pensons intuitivement que cette structure accélérera l'interrogation d'un élément d'une liste chaînée ordonnée. Alors, quelle est la complexité de l'algorithme?
S'il y a n éléments, car il est de 2 points, le nombre de couches doit être log n couches (tous les journaux de cet article sont basés sur 2), plus une couche propre. Prenez l'image ci-dessus à titre d'exemple, si elle a 4 éléments, alors les couches sont L3 et L4, plus sa propre L2, un total de 3 couches; si elle a 8 éléments, alors c'est 3 + 1 couches. La requête la plus chronophage est naturellement d'accéder à toutes les couches, et elle prend logn + logn, qui est 2logn. Pourquoi est 2 fois le logn? Prenons l'exemple de 46 dans la figure ci-dessus. La requête à 46 doit accéder à toutes les couches, et chaque couche doit visiter 2 éléments, l'élément du milieu et le dernier élément. La complexité temporelle est donc O (logn).
Jusqu'à présent, nous avons présenté la liste de sauts la plus idéale, mais que faire si vous souhaitez insérer ou supprimer un élément dans l'image ci-dessus? Par exemple, si on veut insérer un élément 22, 23, 24 ..., naturellement dans la couche L1, on insère ces éléments après l'élément 21, qu'en est-il des couches L2 et L3? Doit-on réfléchir à la manière d'ajuster la connexion après l'insertion afin de maintenir cette structure de liste à sauter idéale. On sait que l'ajustement d'un arbre binaire équilibré est un casse-tête, gaucher, droitier, gaucher, gaucher, gaucher, gaucher, gaucher, gaucher, gaucher, gaucher et gaucher. Heureusement, nous n'avons pas besoin d'ajuster la connexion par des opérations compliquées pour maintenir une table de saut aussi parfaite. Il existe un algorithme d'insertion basé sur des statistiques de probabilité qui peut également obtenir une efficacité de requête avec une complexité temporelle de O (logn). Ce type de table de saut est ce que nous voulons vraiment réaliser.
2. Analyse des étapes de réalisation de la table de saut
Parlons d'abord de l'insertion. Regardons la structure idéale de la table de sauts. Le nombre d'éléments de la couche L2 est égal à 1/2 du nombre d'éléments de la couche L1 et le nombre d'éléments de la couche L3 est égal à 1/2 de le nombre d'éléments dans la couche L2 analogie. À partir de là, nous pouvons penser que tant que nous essayons de nous assurer que le nombre d'éléments dans la couche supérieure est égal à 1/2 des éléments de la couche suivante lors de l'insertion, notre liste de sauts peut devenir une liste de sauts idéale. Alors, comment pouvons-nous nous assurer que le nombre d'éléments du calque précédent est égal à 1/2 du nombre d'éléments du calque suivant lors de l'insertion? C'est facile, lancez une pièce! En supposant que l'élément X doit être inséré dans la liste à sauter, il est évident que X doit être inséré dans le calque L1. Dois-je donc insérer X dans la couche L2? Nous espérons que le nombre d'éléments dans la couche supérieure est égal à 1/2 du nombre d'éléments dans la couche inférieure, nous avons donc une probabilité de 1/2 que nous voulons que X soit inséré dans la couche L2, puis retournez une pièce , insérez-le à l'avant et ne l'insérez pas à l'arrière. Alors L3 devrait-il insérer X? Par rapport à la couche L2, nous espérons toujours que la probabilité de 1/2 est insérée, alors continuez à lancer la pièce! Par analogie, la probabilité que l'élément X soit inséré dans la nième couche est de (1/2) n fois. De cette façon, nous pouvons insérer un élément dans la liste de sauts.
Voici la figure ci-dessus à titre d'exemple: l'état de test initial de la table de saut est le suivant, il n'y a pas d'élément dans la table:
Si nous voulons insérer l'élément 2, insérez d'abord l'élément 2 en bas, comme indiqué ci-dessous:
Ensuite, nous jetons une pièce, le résultat est des têtes, puis nous devons insérer 2 dans la couche L2, comme indiqué ci-dessous:
Continuez à lancer la pièce, le résultat est le contraire, puis l'opération d'insertion de l'élément 2 s'arrête, et la structure de la table après insertion est comme le montre la figure ci-dessus. Ensuite, nous insérons l'élément 33, qui est le même que l'élément 2, insérons maintenant 33 dans la couche L1, comme indiqué ci-dessous:
Ensuite, lancez une pièce de monnaie, le résultat est le contraire, puis l'opération d'insertion de l'élément 33 est terminée, et la structure de table après insertion est comme représentée sur la figure ci-dessus. Ensuite, nous insérons l'élément 55, premier insert 55 dans L1, après l'insertion, comme indiqué ci-dessous:
Ensuite, lancez une pièce, le résultat est des têtes, puis 55 doit être inséré dans la couche L2, comme indiqué ci-dessous:
Continuez à lancer la pièce, et le résultat est à nouveau heads, puis 55 doit être inséré dans la couche L3, comme indiqué ci-dessous:
Par analogie, nous insérons les éléments restants. Bien sûr, en raison de la petite échelle, le résultat peut ne pas être une liste de sauts idéale. Mais si l'échelle du nombre d'éléments n est grande, les étudiants qui ont étudié la théorie des probabilités savent que la structure de la table finale doit être très proche de la table de saut idéale.
Bien sûr, ce genre d'analyse est très direct perceptivement, mais la preuve de la complexité du temps est vraiment compliquée.Je ne vais pas y entrer ici.Si vous êtes intéressé, vous pouvez lire l'article sur la table de saut. Parlons à nouveau de la suppression. Il n'y a rien à dire sur la suppression. Supprimez simplement l'élément directement, puis ajustez le pointeur après avoir supprimé l'élément. C'est exactement la même chose que l'opération normale de suppression de liste chaînée. Discutons à nouveau de la complexité temporelle. La complexité temporelle de l'insertion et de la suppression est la complexité temporelle de l'interrogation de la position d'insertion de l'élément. Ce n'est pas difficile à comprendre, c'est donc O (logn).
3. Mise en œuvre du code
Dans le chapitre 2, nous utilisons un tirage au sort pour déterminer le plus haut niveau d'insertion de nouveaux éléments, qui ne peut bien sûr pas être implémenté dans le programme. Dans le code, nous utilisons la génération de nombres aléatoires pour obtenir le plus haut niveau d'insertion de nouveaux éléments. Estimons d'abord l'échelle de n, puis définissons le nombre maximum de niveaux maxLevel de la table de saut. Ensuite, la couche inférieure, qui est la 0ème couche, doit insérer des éléments avec une probabilité de 1; la couche la plus élevée, qui est la couche maxLevel, a la probabilité d'insertion de l'élément. Est 1/2 ^ maxLevel.
Nous générons d'abord aléatoirement un entier r allant de 0 à 2 ^ maxLevel-1. Alors la probabilité que l'élément r soit inférieur à 2 ^ (maxLevel-1) est 1/2, la probabilité que r soit inférieur à 2 ^ (maxLevel-2) est 1/4, ..., la probabilité que r soit moins de 2 est 1/2 ^ (maxLevel- 1) La probabilité que r soit inférieur à 1 est 1/2 ^ maxLevel.
Par exemple, supposons que maxLevel vaut 4, alors la plage de r est de 0 à 15, la probabilité que r soit inférieur à 8 est 1/2, la probabilité que r soit inférieur à 4 est 1/4, la probabilité que r soit inférieur que 2 est 1/8 et r est inférieur à 1. La probabilité de est de 1/16. 1/16 est exactement la probabilité d'insérer des éléments dans la couche maxLevel, 1/8 est exactement la probabilité d'insérer des éléments dans maxLevel couche, et ainsi de suite.
Grâce à cette analyse, nous pouvons d'abord comparer r et 1, si r <1, alors l'élément doit être inséré sous la couche maxLevel; sinon, comparez r et 2, si r <2, alors l'élément doit être inséré dans le maxLevel- 1 calque ci-dessous; comparez r et 4, si r <4, alors l'élément sera inséré sous le calque maxLevel-2 ... Si r> 2 ^ (maxLevel-1), alors l'élément ne sera inséré que dans le bas couche.
L'analyse ci-dessus est la clé de l'algorithme de nombres aléatoires. L'algorithme n'a rien à voir avec l'implémentation et le langage, mais il est plus facile pour les programmeurs Java de comprendre la liste de raccourcis de l'implémentation du code Java. Ci-dessous, je publierai l'implémentation du code java de quelqu'un d'autre.
/*************************** SkipList.java *********************/
import java.util.Random;
public class SkipList<T extends Comparable<? super T>> {
private int maxLevel;
private SkipListNode<T>[] root;
private int[] powers;
private Random rd = new Random();
SkipList() {
this(4);
}
SkipList(int i) {
maxLevel = i;
root = new SkipListNode[maxLevel];
powers = new int[maxLevel];
for (int j = 0; j < maxLevel; j++)
root[j] = null;
choosePowers();
}
public boolean isEmpty() {
return root[0] == null;
}
public void choosePowers() {
powers[maxLevel-1] = (2 << (maxLevel-1)) - 1; // 2^maxLevel - 1
for (int i = maxLevel - 2, j = 0; i >= 0; i--, j++)
powers[i] = powers[i+1] - (2 << j); // 2^(j+1)
}
public int chooseLevel() {
int i, r = Math.abs(rd.nextInt()) % powers[maxLevel-1] + 1;
for (i = 1; i < maxLevel; i++)
if (r < powers[i])
return i-1; // return a level < the highest level;
return i-1; // return the highest level;
}
// make sure (with isEmpty()) that search() is called for a nonempty list;
public T search(T key) {
int lvl;
SkipListNode<T> prev, curr; // find the highest nonnull
for (lvl = maxLevel-1; lvl >= 0 && root[lvl] == null; lvl--); // level;
prev = curr = root[lvl];
while (true) {
if (key.equals(curr.key)) // success if equal;
return curr.key;
else if (key.compareTo(curr.key) < 0) { // if smaller, go down,
if (lvl == 0) // if possible
return null;
else if (curr == root[lvl]) // by one level
curr = root[--lvl]; // starting from the
else curr = prev.next[--lvl]; // predecessor which
} // can be the root;
else { // if greater,
prev = curr; // go to the next
if (curr.next[lvl] != null) // non-null node
curr = curr.next[lvl]; // on the same level
else { // or to a list on a lower level;
for (lvl--; lvl >= 0 && curr.next[lvl] == null; lvl--);
if (lvl >= 0)
curr = curr.next[lvl];
else return null;
}
}
}
}
public void insert(T key) {
SkipListNode<T>[] curr = new SkipListNode[maxLevel];
SkipListNode<T>[] prev = new SkipListNode[maxLevel];
SkipListNode<T> newNode;
int lvl, i;
curr[maxLevel-1] = root[maxLevel-1];
prev[maxLevel-1] = null;
for (lvl = maxLevel - 1; lvl >= 0; lvl--) {
while (curr[lvl] != null && curr[lvl].key.compareTo(key) < 0) {
prev[lvl] = curr[lvl]; // go to the next
curr[lvl] = curr[lvl].next[lvl]; // if smaller;
}
if (curr[lvl] != null && key.equals(curr[lvl].key)) // don't
return; // include duplicates;
if (lvl > 0) // go one level down
if (prev[lvl] == null) { // if not the lowest
curr[lvl-1] = root[lvl-1]; // level, using a link
prev[lvl-1] = null; // either from the root
}
else { // or from the predecessor;
curr[lvl-1] = prev[lvl].next[lvl-1];
prev[lvl-1] = prev[lvl];
}
}
lvl = chooseLevel(); // generate randomly level
newNode = new SkipListNode<T>(key,lvl+1); // for newNode;
for (i = 0; i <= lvl; i++) { // initialize next fields of
newNode.next[i] = curr[i]; // newNode and reset to newNode
if (prev[i] == null) // either fields of the root
root[i] = newNode; // or next fields of newNode's
else prev[i].next[i] = newNode; // predecessors;
}
}
}
原文地址:https://blog.csdn.net/u013709270/article/details/53470428