Notas de implementación de la tabla hash "Algorithm 4"

Primero, la función hash

La función hash convierte la clave en el índice de la matriz. Nuestra función hash debe ser rápida y distribuir todas las claves de manera uniforme.Por ejemplo, para una tabla hash de tamaño M, nuestra función hash debe poder convertir cualquier clave en un entero de 0 a M-1. Debería haber diferentes funciones hash para diferentes teclas. Muchas clases de uso común en Java han reescrito el método hashCode para usar diferentes funciones hash para diferentes tipos de datos.

Segundo, la tabla hash basada en el método de la cremallera.

El estado ideal del algoritmo hash es convertir diferentes claves a diferentes valores de índice, pero esto es obviamente imposible, y habrá conflictos, por lo que debemos lidiar con los conflictos.
Un método directo es apuntar cada índice en una matriz de tamaño M a una lista vinculada. Cada nodo en la lista vinculada almacena un par de valores clave cuyo valor hash es el valor de índice de la lista vinculada . Este método es el método de cierre. .

La siguiente es la estructura de datos básica:

public class SeparateChainingHashST<Key,Value> {
    /**
     * 键值对总数
     */
    private int N;
    /**
     * 散列表大小
     */
    private int M;

    private SequentialSearchST<Key,Value>[] st;

    public SeparateChainingHashST() {
        this(997);
    }
    public SeparateChainingHashST(int M) {
        this.M=M;
        st=new SequentialSearchST[M];
        for (int i = 0; i < M; i++) {
        	//数组中每个索引值都初始化一个链表
            st[i]=new SequentialSearchST<>();
        }
    }
}    

SequentialSearchSTEs la lista vinculada desordenada implementada en la búsqueda secuencial anterior:

ublic class SequentialSearchST<Key, Value> {
    /**
     * 首节点
     */
    private Node first;
    private int size;


    private class Node {
        private Key key;
        private Value value;
        private Node next;

        public Node(Key key, Value value, Node next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

    /**
     * 根据key查询对应的值,一个个往下遍历直到找到相等的key,返回对应的值,否则返回null
     * @param key
     * @return
     */
    public Value get(Key key) {
        for (Node x = first; x != null; x = x.next) {

            if (key.equals(x.key)) {
                return x.value;
            }
        }
        return null;
    }

    /**
     * 加入一个元素
     * @param key
     * @param value
     */
    public void put(Key key, Value value) {
        for (Node x = first; x != null; x = x.next) {
            //key已存在,更新对应的值
            if (key.equals(x.key)) {
                x.value = value;
                return;
            }
        }
        //key不存在,新添加一个节点
        first = new Node(key, value, first);
        size++;
    }
    public boolean isEmpty() {
        return size == 0;
    }

    private int size() {
        return size;
    }
    public boolean contains(Key key) {
        if (key == null) throw new IllegalArgumentException("argument to contains() is null");
        return get(key) != null;
    }

    /**
     * 删除key对应的节点
     * @param key
     */
    public void delete(Key key) {
        if (key == null) throw new IllegalArgumentException("argument to delete() is null");
        first = delete(first, key);
    }

    /**
     * 递归查找,直到找到相等的key,正常删除链表节点
     * @param x
     * @param key
     * @return
     */
    private Node delete(Node x, Key key) {
        if (x == null) return null;
        if (key.equals(x.key)) {
            size--;
            return x.next;
        }
        x.next = delete(x.next, key);
        return x;
    }
    public Iterable<Key> keys()  {
        Queue<Key> queue=new Queue<>();
        while (first!=null){
            queue.enqueue(first.key);
            first=first.next;
        }
        return queue;
    }
}

Cálculo de hash: como se
mencionó anteriormente, el método hashCode () se ha reescrito para todos los tipos de datos en Java. Este método devuelve un entero de 32 bits, pero lo que necesitamos es el índice de la matriz, por lo que debemos cambiar el método hashCode predeterminado y El método restante se combina para producir un número entero de 0 a M-1, y debido a que el valor devuelto por hashCode está firmado, por lo que incluso si el resultado del cálculo es negativo, debemos pasar 0x7fffffffa un número entero no negativo de 31 bits. Luego, use el resto del método de división para dejar que %MM sea un número primo mayor.

 private int hash(Key key){
        return (key.hashCode() & 0x7fffffff) % M;
    }

De esta manera, podemos insertar, eliminar y obtener datos:
Implementación de inserción: de la
siguiente manera, primero calcule el valor hash de la clave para obtener un índice de matriz y luego inserte el par clave-valor en la lista vinculada correspondiente al índice.

 public void put(Key key,Value value){
        if (key==null){
            throw new NoSuchElementException("key为空");
        }
        if (value==null){
            delete(key);
        }
        //保证链表的长度在2到8之间
        if (N>=8*M){
            resize(M*2);
        }
        st[hash(key)].put(key,value);
    }

Implementación de eliminación:
primero calcule el valor hash de la clave, busque la lista vinculada donde se encuentra y elimine la clave si existe en la lista vinculada.

 /**
     * 删除指定键值对
     * @param key
     */
    public void delete(Key key) {
        if (key == null) throw new IllegalArgumentException("argument to delete() is null");

        int i = hash(key);
        if (st[i].contains(key)){
            N--;
        }
        st[i].delete(key);
        //保证链表平均长度在2到8间 此为下界2
        if (N>0 && N<=M*2){
            resize(M/2);
        }

    }
 public boolean contains(Key key) {
        if (key == null) throw new IllegalArgumentException("key为空");
        return get(key) != null;
    }    

Obtener valor:
calcule el valor hash de la clave y devuelva el valor en la lista vinculada correspondiente

public Value get(Key key){
        if (key == null) return null;

        return st[hash(key)].get(key);
    }

Utilizamos listas vinculadas M para almacenar claves N. No importa cómo se distribuyan las claves en la tabla, la longitud promedio debe ser N/M.
Una ventaja de usar el método de la cremallera es que si hay más claves de lo esperado, el tiempo de búsqueda solo será más largo que elegir una matriz más grande; si es más bajo de lo esperado, aunque causará un poco de pérdida de espacio, la búsqueda es rápida.
Por lo tanto, cuando la memoria es suficiente, puede elegir una M lo suficientemente grande como para que la búsqueda se use constantemente; cuando la memoria es escasa, elegir la M más grande aún puede mejorar el rendimiento en M veces.

Ajuste dinámicamente la matriz:
después de la eliminación, si la longitud promedio N / M es menor que 2, la matriz se duplica: M / 2;
después de agregar datos, si la N / M es mayor que 8, la matriz se duplica: M * 2

 private void resize(int capacity){
        SeparateChainingHashST<Key,Value>hashST=new SeparateChainingHashST<>(capacity);
        for (int i = 0; i < M; i++) {
            for (Key key:st[i].keys()){
                if (key!=null){
                    hashST.put(key,st[i].get(key));
                }
            }
        }
        this.M=hashST.M;
        this.N=hashST.N;
        this.st=hashST.st;
    }

Tres tablas hash basadas en el método de detección lineal

Otra forma de implementar una tabla hash es usar una matriz de tamaño M para almacenar N pares de clave-valor (M> N) y usar colisiones para resolver conflictos de colisión. Todos los métodos basados ​​en esta estrategia se convierten en tablas hash de direcciones abiertas.
El método más simple de la tabla hash de dirección abierta es el método de detección lineal, es decir, si se produce un conflicto (el valor hash de una clave ya está ocupado por una clave diferente), compruebe directamente la siguiente posición de la tabla hash (índice +1), si Si todavía hay un conflicto, sigue detectando hacia atrás hasta que encuentra una posición vacía e inserta el par clave-valor en él.
La estructura de datos es la siguiente:
aquí usa una Clave [] para guardar la clave, y un Valor [] para guardar el valor correspondiente a la clave

public class LinearProbingHashST<Key, Value> {
    private Key[] keys;
    private Value[] values;

    /**
     * 键值对数
     */
    private int N;
    /**
     * 线性表大小
     */
    private int M;

    public LinearProbingHashST(int M) {
        this.M = M;
        keys = (Key[]) new Object[this.M];
        values = (Value[]) new Object[this.M];
    }
}    

Operación de inserción:
necesitamos calcular el valor hash de la clave que se va a insertar, y luego determinar si el índice actual está ocupado por otras claves, si la clave ocupada también es la clave que se va a insertar, luego modificar el valor correspondiente, de lo contrario iterar hasta encontrar una posición vacía .

    public void put(Key key, Value value) {
        if (key == null) throw new IllegalArgumentException("first argument to put() is null");
        if (value==null){
            delete(key);
        }
        //保证使用率 N/M 小于等于 1/2 ,当使用率趋近于1时,探测的次数会变得很大
        if (N>=M*2){
            resize(M*2);
        }
        int i;
        for (i = hash(key); keys[i] != null; i = (i + 1) % M) {
            if (keys[i].equals(key)) {
            	//待插入的key存在,修改对应的值并返回
                values[i] = value;
                return;
            }
        }
        //找到空位置,插入键值对
        keys[i] = key;
        values[i] = value;
        N++;
    }
     private int hash(Key key) {
        return (key.hashCode() & 0x7fffffff) % M;
    }

Operación de consulta:
calcule el valor hash de la clave. Si la posición actual entra en conflicto (ocupada por otras teclas), continúe retrocediendo. La condición final del recorrido es desde hash (clave) a la siguiente posición nula. Si existe, devuelva el correspondiente El valor de, o nulo si no existe

 public Value get(Key key) {
        for (int i = hash(key); keys[i] != null; i = (i + 1) % M) {
            if (keys[i].equals(key)) {
                return values[i];
            }
        }
        return null;
    }

Para una tabla hash de la siguiente manera:

0   1   2   3    4    5   6    7    8     9    10   11    12     13    14    15
P   M            A    C   S    H 	L			E						R	  X
10  9			 8    4	  0    5    11          12                      3     7

El valor hash de A es 4, y el valor hash de H también es 4, pero debido al conflicto en 4, se mueve linealmente a la posición 7 al insertar H, por lo que cuando buscamos el valor correspondiente a H, comenzamos con el índice 4. , No coincida y mire hacia abajo en orden. Si no se encuentra antes de la posición vacía 9, significa que no hay H, pero debido a que H está en la posición 7 antes de 9, el índice devuelve el valor correspondiente 5.
Operación de eliminación:
para la operación de eliminación, No puede establecer directamente la clave correspondiente en nulo.
Lo mismo es la tabla de hash anterior: si se elimina C y el valor de hash de H es 4, en el proceso de detección lineal, devolverá nulo porque el índice 5 está vacío y "falsamente" piensa que H no existe en la tabla de hash. Pero el hecho es que H existe y está en el índice 7.

Por lo tanto, después de eliminar una clave determinada, debe volver a insertar todas las claves desde la tecla + 1 hasta la siguiente posición vacía para evitar el error anterior.

  public void delete(Key key) {
        if (!contains(key)) {
            return;
        }
        int i=hash(key);
        //线性探测找到待删除的key的索引
        while (!keys[i].equals(key)){
            i=(i+1)%M;
        }
        //置空
        keys[i]=null;
        values[i]=null;
        //将i+1的位置到下一个空位置前的所有key重新插入到散列表中
        i=(i+1)%M;
        while (keys[i]!=null){
            Key oldKey=keys[i];
            Value oldValue=values[i];
            keys[i]=null;
            values[i]=null;

            N--;
            put(oldKey,oldValue);
            i=(i+1)%M;
        }
        N--;
        if (N>0 && N <= M/8){
            resize(M/2);
        }
    }

Además α= N/M, llamamos a α la tasa de uso de la tabla hash. Las
siguientes conclusiones se dan en el "Algoritmo 4":

En una tabla hash basada en la detección lineal de tamaño M y que contiene N teclas, si nuestra función hash puede distribuir de manera uniforme e independiente todas las teclas entre 0 y M-1, entonces aciertos y errores El número de sondas requeridas para la búsqueda es:
1/2 (1 + 1/1 − α) y 1/2 (1 + 1 / (1 − α) ^ 2)

Es decir, cuando la tabla hash está casi llena, el número de detecciones requeridas para la búsqueda es enorme (α se acerca a 1 ), pero cuando α < 1/2se excede la tasa de uso , el número estimado de detecciones es solo entre 1.5 y 2.5, por lo que debemos asegurarnos de que El valor no es mayor que 1/2.
En base a esto, necesitamos
ajustar dinámicamente la matriz antes de la inserción y después de la eliminación:

Juez antes de insertar:

        //保证使用率 N/M 不能超过1/2 ,当使用率趋近于1时,探测的次数会变得很大
        if (N>=M*2){
            resize(M*2);
        }

Juicio después de la eliminación:
asegúrese de que la relación entre la cantidad de memoria utilizada y el valor de la clave con respecto al número en la tabla esté siempre dentro de un cierto rango.

        //数组减小一半,如果N/M 为12.5% 或更少
		if (N>0 && N <= M/8){
            resize(M/2);
        }
Publicado 75 artículos originales · alabanza ganado 13 · vistas 8369

Supongo que te gusta

Origin blog.csdn.net/weixin_43696529/article/details/104731252
Recomendado
Clasificación