Implementación Java de lista de omisión e interpretación de papel

Skiplist.png

¿Qué es un reloj de salto?

La mesa de salto fue inventada por William Pugh.

En su artículo "Listas de omisión : una alternativa probabilística a los árboles equilibrados" , presentó en detalle la estructura de datos de la tabla de omisión y operaciones como la inserción y la eliminación.

跳表是一种可以用来代替平衡树的数据结构,跳表使用概率平衡而不是严格执行的平衡,因此,与等效树的等效算法相比,跳表中插入和删除的算法要简单得多,并且速度要快得多。

Skip_list.svg.png

¿Por qué es necesario?

El rendimiento es mejor.

La implementación es relativamente simple en comparación con el árbol rojo-negro.

Ocupa menos memoria.

Interpretación de trabajos

Para conocer la información de primera mano, primero estudiamos el documento y luego combinamos los artículos en línea para implementar una versión java de la lista de omisiones.

William Pugh

Los árboles binarios se pueden utilizar para representar tipos de datos abstractos, como diccionarios y listas ordenadas.

Funcionan bien cuando los elementos se insertan en orden aleatorio. Ciertas secuencias de operaciones (como insertar elementos en orden) producen estructuras de datos generadas que tienen un rendimiento muy bajo.

Si la lista de elementos que se insertarán se puede organizar al azar, el árbol funcionará bien para cualquier secuencia de entrada. En la mayoría de los casos, la consulta debe responderse en línea, por lo que la entrada aleatoria no es práctica.

El algoritmo de árbol equilibrado reorganiza el árbol al realizar operaciones para mantener ciertas condiciones de equilibrio y garantizar un buen rendimiento.

skiplist es una alternativa probabilística a los árboles equilibrados .

Equilibre la lista de omisión consultando un generador de números aleatorios. Aunque la lista de omisión tiene un rendimiento deficiente en el peor de los casos, ninguna secuencia de entrada siempre producirá un rendimiento en el peor de los casos (como la clasificación rápida cuando los elementos dinámicos se seleccionan al azar).

Es poco probable que la estructura de datos de la lista de omisión esté gravemente desequilibrada (por ejemplo, para un diccionario con más de 250 elementos, la probabilidad de que la búsqueda lleve más de 3 veces el tiempo esperado es menos de una en un millón). Es similar a un árbol de búsqueda construido por inserción aleatoria, pero no necesita ser insertado para ser aleatorio.

Es más fácil equilibrar la estructura de datos probabilísticamente que mantener el equilibrio explícitamente.

ps: la mayoría de los programadores pueden escribir una lista de omisión a mano, pero escribir a mano un árbol rojo-negro es mucho más complicado.

Para muchas aplicaciones, la lista de omisión se representa de manera más natural que los árboles, lo que también conduce a algoritmos más simples .

La simplicidad del algoritmo de lista de omisión hace que sea más fácil de implementar y proporciona una mejora significativa de la velocidad del factor constante en los algoritmos de árbol equilibrado y árbol de autoajuste.

skiplist también ahorra mucho espacio . Se pueden configurar fácilmente para requerir un promedio de 4/3 punteros por elemento (o incluso menos), y no es necesario almacenar información de saldo o prioridad con cada nodo.

Idea central del algoritmo

Para una lista vinculada, si queremos encontrar un elemento, es posible que necesitemos recorrer toda la lista vinculada.

Si la lista está ordenada y dos cada nodo tiene un puntero al nodo después de que son dos (Figura 1b), entonces no podemos excedernos encontrando ⌈n/2⌉+1nodos para completar el aspecto.

Si cada nodo tiene cuatro punteros a un nodo después de que tiene cuatro bits, entonces solo para verificar los ⌈n/4⌉+2nodos (Figura 1c).

Si todos los (2 ^ i) th nodos tienen un puntero al nodo después de 2 ^ i (Figura 1d), entonces el número máximo de nodos que se deben verificar es ⌈log_n2⌉, y el costo es solo el número de punteros que se necesitarán doble.

La eficiencia de consulta de esta estructura de datos es muy alta, pero su inserción y eliminación es casi imposible (impráctico).

A continuación, mire una imagen en el periódico:

lista de saltos

Debido a que dicha estructura de datos se basa en listas enlazadas y los punteros adicionales omitirán los nodos intermedios, el autor los llama Listas de omisión .

estructura

Como puede verse en la figura, la tabla de salto se compone principalmente de las siguientes partes:

Head: el puntero de nodo responsable de mantener la tabla de omisión.

Nodo de tabla de salto: almacena valores de elementos y múltiples capas.

Capa: contiene punteros a otros elementos. El número de elementos que pasa por el puntero de nivel alto es mayor o igual que el puntero de nivel bajo. Para mejorar la eficiencia de la búsqueda, el programa siempre comienza a acceder desde el nivel alto primero y luego disminuye gradualmente el nivel a medida que se reduce el rango de valores de los elementos.

Fin de la tabla: Todos están compuestos por NULL, lo que significa el final de la lista de omisión.

proceso de algoritmo de lista de omisión

Esta sección proporciona algoritmos para buscar, insertar y eliminar elementos en un diccionario o tabla de símbolos.

La operación de búsqueda devolverá el contenido del valor asociado con la clave requerida o la clave fallida (si la clave no existe).

La operación de inserción asocia la clave especificada con el nuevo valor (si aún no existe, inserte la clave).

La operación Eliminar elimina la clave especificada.

Es fácil admitir otras operaciones, como "encontrar la clave más pequeña" o "encontrar la siguiente clave". Cada elemento está representado por un nodo, cuando se inserta un nodo, su nivel se selecciona aleatoriamente, independientemente del número de elementos en la estructura de datos.

El nodo de nivel i tiene i punteros hacia adelante con índices de 1 a i.

No necesitamos almacenar el nivel del nodo en el nodo. El nivel está limitado por un MaxLevel constante apropiado.

El nivel de la lista es el nivel más grande actualmente en la lista (si la lista está vacía, es 1).

El título de la lista tiene un puntero hacia adelante de un nivel a MaxLevel.

El puntero hacia adelante del encabezado apunta a NIL en un nivel superior al nivel máximo actual de la lista

inicialización

Asigne el elemento NIL y proporcione una clave mayor que cualquier clave legal.

Todos los niveles de todos los skiplists terminan con NIL.

Inicialice una nueva lista para que el nivel de la lista sea igual a 1 y todos los punteros hacia adelante del encabezado de la lista apunten a NIL

Algoritmo de búsqueda

Buscamos elementos atravesando no más que el puntero hacia adelante del nodo que contiene el elemento a buscar (Figura 2).

Si no se puede avanzar más en el nivel actual del puntero hacia adelante, la búsqueda se moverá hacia abajo al siguiente nivel.

Cuando no podemos hacer más procesamiento en el nivel 1, debemos estar inmediatamente antes del nodo que contiene el elemento requerido (si está en la lista)

skiplist-02.PN

Insertar y eliminar algoritmo

Para insertar o eliminar nodos, solo necesitamos buscar y empalmar, como se muestra en la Figura 3.

skiplist-03

La figura 4 muestra el algoritmo de inserción y eliminación.

Mantenga el vector actualizado para que cuando se complete la búsqueda (y estemos listos para realizar el empalme), la actualización [i] contenga un puntero al nodo más a la derecha en el nivel i o superior, que se encuentra a la izquierda / eliminar de la posición de la ilustración.

Si el nivel del nodo insertado es mayor que el nivel máximo anterior de la lista, actualizaremos el nivel máximo de la lista e inicializaremos la parte apropiada del vector de actualización.

Después de cada eliminación, verificamos si se ha eliminado el elemento más grande de la lista y, de ser así, reduzca el nivel máximo de la lista.

skiplist-04

Elige un nivel aleatorio

Inicialmente, discutimos la distribución de probabilidad, en la que la mitad de los nodos con punteros i también tienen punteros i + 1.

Para deshacerse de la constante mágica, decimos que una pequeña parte de los nodos que tienen punteros a i también tienen punteros a i + 1. (Para nuestra discusión inicial, p = 1/2).

El nivel se genera aleatoriamente mediante un algoritmo equivalente al de la Figura 5.

No se hace referencia al número de elementos de la lista al generar niveles.

skiplist-05

¿A qué nivel empezamos a buscar? Definir L (n)

En la lista de exclusión de 16 elementos generada con p = 1/2, podemos encontrar 9 elementos de nivel 1, 3 elementos de nivel 2, 3 elementos de nivel 3 y 1 elemento de nivel 14 (esto es poco probable , Pero puede suceder).

¿Cómo debemos afrontarlo?

Si usamos algoritmos estándar y comenzamos a buscar desde el nivel 14, haremos mucho trabajo inútil.

¿Dónde deberíamos empezar a buscar?

Nuestro análisis muestra que, idealmente, comenzaremos la búsqueda en el nivel L donde se esperan 1 / p nodos.

Esto sucede cuando L = log_ (1 / p) n.

Dado que a menudo citaremos esta fórmula, usaremos L (n) para denotar log_ (1 / p) n.

Hay muchas soluciones para decidir qué hacer con elementos inusualmente grandes en la lista.

No se preocupe, sea optimista.

Simplemente comience la búsqueda desde el nivel más alto presente en la lista.

Como veremos en el análisis, la probabilidad de que el nivel más grande en una lista de n elementos sea significativamente mayor que L (n) es muy pequeña.

La búsqueda desde el nivel más alto de la lista no agrega una pequeña constante al tiempo de búsqueda esperado.

Este es el método utilizado en el algoritmo descrito en este artículo.

Use menos de la cantidad dada.

Aunque un elemento puede contener espacio para 14 punteros, no es necesario utilizar los 14 punteros.

Podemos optar por utilizar solo niveles L (n).

Hay muchas formas de lograr esto, pero todas complican el algoritmo y no pueden mejorar significativamente el rendimiento, por lo que no se recomienda este método.

Corregir aleatoriedad (dados)

Si el nivel aleatorio que generamos es más del doble del nivel máximo actual en la lista, solo necesitamos agregar uno al nivel máximo actual en la lista como el nuevo nivel de nodo.

En la práctica, intuitivamente, este cambio parece funcionar bien.

Sin embargo, dado que el nivel de los nodos ya no es completamente aleatorio, esto destruye por completo nuestra capacidad para analizar los resultados del algoritmo.

Los programadores probablemente deberían implementarlo a voluntad, mientras que los puristas deberían evitarlo.

Determinar MaxLevel

Dado que podemos limitar con seguridad el nivel a L (n), deberíamos elegir MaxLevel = L (n) (donde N es el límite superior del número de elementos en la lista de omisión).

Si p = 1/2, use MaxLevel = 16 para estructuras de datos que contengan hasta 216 elementos.

ps: maxLevel se puede derivar del valor del número de elementos + P.

Para P, la recomendación del autor es utilizar p = 1/4. La siguiente parte del análisis de algoritmos tiene una introducción detallada, que es más larga, y los estudiantes interesados ​​pueden leerla después de la implementación de Java.

Versión de implementación de Java

profundizar la impresión

Independientemente de si nos fijamos en la teoría, creemos que la conocemos, pero a menudo tenemos buen ojo y mano baja.

La mejor manera es escribirlo usted mismo, para que la impresión quede impresionada.

Definición de nodo

Podemos pensar en la lista de omisión como una versión mejorada de la lista vinculada.

Todas las listas vinculadas necesitan un nodo Nodo, definámoslo:

/**
 * 元素节点
 * @param <E> 元素泛型
 * @author 老马啸西风
 */
private static class SkipListNode<E> {
    /**
     * key 信息
     * <p>
     * 这个是什么?index 吗?
     *
     * @since 0.0.4
     */
    int key;
    /**
     * 存放的元素
     */
    E value;
    /**
     * 向前的指针
     * <p>
     * 跳表是多层的,这个向前的指针,最多和层数一样。
     *
     * @since 0.0.4
     */
    SkipListNode<E>[] forwards;

    @SuppressWarnings("all")
    public SkipListNode(int key, E value, int maxLevel) {
        this.key = key;
        this.value = value;
        this.forwards = new SkipListNode[maxLevel];
    }
    @Override
    public String toString() {
        return "SkipListNode{" +
                "key=" + key +
                ", value=" + value +
                ", forwards=" + Arrays.toString(forwards) +
                '}';
    }
}

Resulta que usar una matriz en una lista vinculada puede hacer que el código sea más conciso que usar List.

Al menos es más sencillo de leer, la primera vez que se implementó con list, pero no todo se reescribió más tarde.

La comparación es la siguiente:

newNode.forwards[i] = updates[i].forwards[i];   //数组

newNode.getForwards().get(i).set(i, updates.get(i).getForwards(i)); //列表

Implementación de consultas

La idea de la consulta es muy simple: partimos del nivel superior y buscamos de izquierda a derecha (el nivel superior puede ser el más rápido para ubicar la posición aproximada del elemento que queremos encontrar), si el siguiente elemento es mayor que el especificado, comenzamos a buscar el siguiente nivel .

En cualquier nivel, el valor correspondiente se devolverá directamente cuando se encuentre.

Encuentra la capa inferior, no hay valor, significa que el elemento no existe.

/**
 * 执行查询
 * @param searchKey 查找的 key
 * @return 结果
 * @since 0.0.4
 * @author 老马啸西风
 */
public E search(final int searchKey) {
    // 从左边最上层开始向右
    SkipListNode<E> c = this.head;
    // 从已有的最上层开始遍历
    for(int i = currentLevel-1; i >= 0; i--) {
        while (c.forwards[i].key < searchKey) {
            // 当前节点在这一层直接向前
            c = c.forwards[i];
        }
        // 判断下一个元素是否满足条件
        if(c.forwards[i].key == searchKey) {
            return c.forwards[i].value;
        }
    }
    // 查询失败,元素不存在。
    return null;
}

ps: muchas implementaciones en Internet son incorrectas. La mayoría de ellos no comprenden la esencia de la consulta de listas de omisión.

insertar

Si la clave no existe, inserte la clave y el valor correspondiente; si la clave existe, actualice el valor.

Si el nivel del nodo que se va a insertar es superior al nivel actual de la tabla de salto currentLevel, currentLevel se actualiza.

Seleccione el nivel aleatorio del nodo que se va a insertar:

randomLevel solo depende del nivel más alto de la tabla de saltos y del valor de probabilidad p.

El algoritmo está en el código subyacente.

Otra forma de lograr esto es, si el randomLevel generado es mayor que el nivel actual de la tabla de salto actual, entonces establezca randomLevel en currentLevel + 1, que es conveniente para búsquedas futuras, lo cual es aceptable en ingeniería, pero también destruye el algoritmo. Aleatoriedad.

/**
 * 插入元素
 *
 *
 * @param searchKey 查询的 key
 * @param newValue 元素
 * @since 0.0.4
 * @author 老马啸西风
 */
@SuppressWarnings("all")
public void insert(int searchKey, E newValue) {
    SkipListNode<E>[] updates = new SkipListNode[maxLevel];
    SkipListNode<E> curNode = this.head;
    for (int i = currentLevel - 1; i >= 0; i--) {
        while (curNode.forwards[i].key < searchKey) {
            curNode = curNode.forwards[i];
        }
        // curNode.key < searchKey <= curNode.forward[i].key
        updates[i] = curNode;
    }
    // 获取第一个元素
    curNode = curNode.forwards[0];
    if (curNode.key == searchKey) {
        // 更新对应的值
        curNode.value = newValue;
    } else {
        // 插入新元素
        int randomLevel = getRandomLevel();
        // 如果层级高于当前层级,则更新 currentLevel
        if (this.currentLevel < randomLevel) {
            for (int i = currentLevel; i < randomLevel; i++) {
                updates[i] = this.head;
            }
            currentLevel = randomLevel;
        }
        // 构建新增的元素节点
        //head==>new  L-1
        //head==>pre==>new L-0
        SkipListNode<E> newNode = new SkipListNode<>(searchKey, newValue, randomLevel);
        for (int i = 0; i < randomLevel; i++) {
            newNode.forwards[i] = updates[i].forwards[i];
            updates[i].forwards[i] = newNode;
        }
    }
}

Entre ellos, getRandomLevel es un método para generar niveles aleatoriamente.

/**
 * 获取随机的级别
 * @return 级别
 * @since 0.0.4
 */
private int getRandomLevel() {
    int lvl = 1;
    //Math.random() 返回一个介于 [0,1) 之间的数字
    while (lvl < this.maxLevel && Math.random() < this.p) {
        lvl++;
    }
    return lvl;
}

Personalmente, creo que skiplist es muy inteligente, utiliza la aleatoriedad para lograr un efecto de equilibrio similar al de un árbol de equilibrio.

Sin embargo, debido a la aleatoriedad, cada lista vinculada se genera de manera diferente.

Eliminar

Elimina la clave específica y el valor correspondiente.

Si el nodo que se va a eliminar es el nodo con el nivel más alto en la tabla de salto, el currentLevel debe actualizarse después de la eliminación.

/**
 * 删除一个元素
 * @param searchKey 查询的 key
 * @since 0.0.4
* @author 老马啸西风
 */
@SuppressWarnings("all")
public void delete(int searchKey) {
    SkipListNode<E>[] updates = new SkipListNode[maxLevel];
    SkipListNode<E> curNode = this.head;
    for (int i = currentLevel - 1; i >= 0; i--) {
        while (curNode.forwards[i].key < searchKey) {
            curNode = curNode.forwards[i];
        }
        // curNode.key < searchKey <= curNode.forward[i].key
        // 设置每一层对应的元素信息
        updates[i] = curNode;
    }
    // 最下面一层的第一个指向的元素
    curNode = curNode.forwards[0];
    if (curNode.key == searchKey) {
        for (int i = 0; i < currentLevel; i++) {
            if (updates[i].forwards[i] != curNode) {
                break;
            }
            updates[i].forwards[i] = curNode.forwards[i];
        }
        // 移除无用的层级
        while (currentLevel > 0 && this.head.forwards[currentLevel-1] ==  this.NIL) {
            currentLevel--;
        }
    }
}

Tabla de salto de salida

Para facilitar la prueba, implementamos un método para generar la tabla de salto.

/**
 * 打印 list
 * @since 0.0.4
 */
public void printList() {
    for (int i = currentLevel - 1; i >= 0; i--) {
        SkipListNode<E> curNode = this.head.forwards[i];
        System.out.print("HEAD->");
        while (curNode != NIL) {
            String line = String.format("(%s,%s)->", curNode.key, curNode.value);
            System.out.print(line);
            curNode = curNode.forwards[i];
        }
        System.out.println("NIL");
    }
}

prueba

public static void main(String[] args) {
    SkipList<String> list = new SkipList<>();
    list.insert(3, "耳朵听声音");
    list.insert(7, "镰刀来割草");
    list.insert(6, "口哨嘟嘟响");
    list.insert(4, "红旗迎风飘");
    list.insert(2, "小鸭水上漂");
    list.insert(9, "勺子能吃饭");
    list.insert(1, "铅笔细又长");
    list.insert(5, "秤钩来买菜");
    list.insert(8, "麻花扭一扭");
    list.printList();
    System.out.println("---------------");
    list.delete(3);
    list.delete(4);
    list.printList();
    System.out.println(list.search(8));
}

El registro es el siguiente:

HEAD->(5,秤钩来买菜)->(6,口哨嘟嘟响)->NIL
HEAD->(1,铅笔细又长)->(2,小鸭水上漂)->(3,耳朵听声音)->(4,红旗迎风飘)->(5,秤钩来买菜)->(6,口哨嘟嘟响)->(7,镰刀来割草)->(8,麻花扭一扭)->(9,勺子能吃饭)->NIL
---------------
HEAD->(5,秤钩来买菜)->(6,口哨嘟嘟响)->NIL
HEAD->(1,铅笔细又长)->(2,小鸭水上漂)->(5,秤钩来买菜)->(6,口哨嘟嘟响)->(7,镰刀来割草)->(8,麻花扭一扭)->(9,勺子能吃饭)->NIL
麻花扭一扭

resumen

SkipList es una estructura de datos muy inteligente. Hasta ahora, todavía no puedo escribir árboles rojo-negro a mano, pero escribir una lista de omisión es relativamente más fácil. ¡Como el autor del artículo!

En la siguiente sección, vamos a trabajar con la estructura de datos ConcurrentSkipListSet en jdk y sentir el encanto de la implementación oficial de Java.

Espero que este artículo te sea útil. Si tienes otras ideas, también puedes compartirlas contigo en la sección de comentarios.

¡Los me gusta, las colecciones y el reenvío de los geeks son la mayor motivación para la escritura continua de Ma!

Aprendizaje profundo

Supongo que te gusta

Origin blog.51cto.com/9250070/2546328
Recomendado
Clasificación