首页 > 其他分享 >数据结构第18节 散列表

数据结构第18节 散列表

时间:2024-07-09 12:27:24浏览次数:11  
标签:index capacity int 18 value 列表 key table 数据结构

散列表(Hash Table),也称为哈希表,是一种数据结构,它使用哈希函数将键映射到数组的一个索引位置,从而可以快速地插入、查找和删除数据。散列表的核心在于哈希函数和解决冲突的策略。

在Java中,散列表的实现通常涉及以下几个关键部分:

  1. 哈希函数:用于将键转换为数组索引。
  2. 解决冲突:多个键可能映射到同一个数组索引,这称为冲突。常见的解决策略有链地址法(开放寻址法不在此讨论)。
  3. 负载因子:表示散列表的填充程度,当达到一定阈值时,散列表会自动扩容以减少冲突。

散列表实现示例

下面是一个简单的散列表实现,使用链表解决冲突:

import java.util.LinkedList;

public class SimpleHashTable<K, V> {
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    private LinkedList<Entry<K, V>>[] buckets;
    private int size;
    private float loadFactor;
    private int capacity;

    public SimpleHashTable(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public SimpleHashTable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0) {
            throw new IllegalArgumentException("Illegal capacity: " + initialCapacity);
        }
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        }
        this.loadFactor = loadFactor;
        this.capacity = initialCapacity;
        this.buckets = new LinkedList[capacity];
    }

    public V put(K key, V value) {
        int hash = key.hashCode();
        int index = hash % capacity;
        LinkedList<Entry<K, V>> bucket = buckets[index];
        if (bucket == null) {
            bucket = new LinkedList<>();
            buckets[index] = bucket;
        }

        for (Entry<K, V> entry : bucket) {
            if (entry.getKey().equals(key)) {
                V oldValue = entry.getValue();
                entry.setValue(value);
                return oldValue;
            }
        }

        bucket.add(new Entry<>(key, value));
        size++;

        if ((float)size / capacity >= loadFactor) {
            resize();
        }

        return null;
    }

    public V get(K key) {
        int hash = key.hashCode();
        int index = hash % capacity;
        LinkedList<Entry<K, V>> bucket = buckets[index];
        if (bucket != null) {
            for (Entry<K, V> entry : bucket) {
                if (entry.getKey().equals(key)) {
                    return entry.getValue();
                }
            }
        }
        return null;
    }

    private void resize() {
        LinkedList<Entry<K, V>>[] oldBuckets = buckets;
        capacity *= 2;
        buckets = new LinkedList[capacity];

        for (LinkedList<Entry<K, V>> bucket : oldBuckets) {
            if (bucket != null) {
                for (Entry<K, V> entry : bucket) {
                    put(entry.getKey(), entry.getValue());
                }
            }
        }
    }

    private static class Entry<K, V> {
        private K key;
        private V value;

        public Entry(K key, V value) {
            this.key = key;
            this.value = value;
        }

        public K getKey() {
            return key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }
}

使用示例

public class Main {
    public static void main(String[] args) {
        SimpleHashTable<String, Integer> table = new SimpleHashTable<>(16);
        table.put("one", 1);
        table.put("two", 2);
        table.put("three", 3);

        System.out.println(table.get("one"));  // 输出: 1
        System.out.println(table.get("two"));  // 输出: 2
        System.out.println(table.get("three"));  // 输出: 3
    }
}

注意事项

  • 散列表的性能高度依赖于哈希函数的质量和冲突的解决策略。
  • 在实际应用中,散列表的大小通常是2的幂,以优化哈希函数的性能。
  • Java标准库中的HashMap类提供了更复杂和高效的散列表实现,包括更好的哈希函数和冲突解决策略。

以上示例展示了如何在Java中实现一个基本的散列表。在实际项目中,你可能会根据具体需求调整和优化这个实现,例如使用更高效的数据结构替代链表,或者实现更复杂的哈希函数。

为了进一步优化上述散列表的实现,我们可以做以下几点改进:

  1. 使用开放寻址法:开放寻址法(如线性探测、二次探测或双散列)可以在某些情况下比链地址法提供更好的性能,尤其是在高负载因子的情况下。
  2. 提高散列函数质量:使用更好的散列函数可以减少碰撞,提高散列表的效率。
  3. 使用更高效的数据结构:在解决冲突时,使用更高效的数据结构(如红黑树)代替链表,可以减少查找时间。
  4. 实现线程安全:如果散列表将在多线程环境中使用,确保其线程安全是非常重要的。
  5. 添加更多功能:如remove方法、containsKeysize等。

下面是一个采用线性探测开放寻址法的散列表实现,同时使用了更高效的散列函数:

import java.util.Arrays;

public class OptimizedHashTable<K, V> {
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    private Entry<K, V>[] table;
    private int size;
    private float loadFactor;
    private int capacity;

    public OptimizedHashTable(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public OptimizedHashTable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0) {
            throw new IllegalArgumentException("Illegal capacity: " + initialCapacity);
        }
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        }
        this.loadFactor = loadFactor;
        this.capacity = initialCapacity;
        this.table = new Entry[capacity];
    }

    public V put(K key, V value) {
        int hash = key.hashCode();
        int index = hash & (capacity - 1); // 使用位运算优化
        int originalIndex = index;
        while (table[index] != null && !table[index].getKey().equals(key)) {
            index = (index + 1) & (capacity - 1); // 线性探测
            if (index == originalIndex) {
                throw new IllegalStateException("Table is full.");
            }
        }

        if (table[index] == null) {
            table[index] = new Entry<>(key, value);
            size++;
        } else {
            V oldValue = table[index].getValue();
            table[index].setValue(value);
            return oldValue;
        }

        if ((float)size / capacity >= loadFactor) {
            resize();
        }

        return null;
    }

    public V get(K key) {
        int hash = key.hashCode();
        int index = hash & (capacity - 1);
        int originalIndex = index;
        while (table[index] != null) {
            if (table[index].getKey().equals(key)) {
                return table[index].getValue();
            }
            index = (index + 1) & (capacity - 1);
            if (index == originalIndex) {
                break; // 防止无限循环
            }
        }
        return null;
    }

    private void resize() {
        Entry<K, V>[] oldTable = table;
        capacity *= 2;
        table = new Entry[capacity];
        size = 0;

        for (Entry<K, V> entry : oldTable) {
            if (entry != null) {
                put(entry.getKey(), entry.getValue());
            }
        }
    }

    private static class Entry<K, V> {
        private K key;
        private V value;

        public Entry(K key, V value) {
            this.key = key;
            this.value = value;
        }

        public K getKey() {
            return key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }
}

在这个版本中,我们使用了线性探测来解决冲突,并且通过位运算优化了散列函数。请注意,开放寻址法可能导致散列表在接近满载时性能下降,因此需要谨慎选择负载因子和适当的重新散列策略。

此外,我们没有在上述代码中实现线程安全,因为这通常会增加复杂性和开销。在多线程环境下,可以考虑使用ConcurrentHashMap或其他线程安全的散列表实现。

为了进一步完善代码,我们可以添加一些实用的方法,比如remove方法来删除键值对,containsKey方法来检查是否存在某个键,以及size方法来返回散列表当前的元素数量。同时,为了保证线程安全,我们可以添加同步机制。以下是完善后的代码:

import java.util.Arrays;

public class OptimizedHashTable<K, V> {
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    private Entry<K, V>[] table;
    private int size;
    private float loadFactor;
    private int capacity;

    public OptimizedHashTable(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public OptimizedHashTable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0) {
            throw new IllegalArgumentException("Illegal capacity: " + initialCapacity);
        }
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        }
        this.loadFactor = loadFactor;
        this.capacity = initialCapacity;
        this.table = new Entry[capacity];
    }

    public synchronized V put(K key, V value) {
        int hash = key.hashCode();
        int index = hash & (capacity - 1); 
        int originalIndex = index;
        while (table[index] != null && !table[index].getKey().equals(key)) {
            index = (index + 1) & (capacity - 1);
            if (index == originalIndex) {
                throw new IllegalStateException("Table is full.");
            }
        }

        if (table[index] == null) {
            table[index] = new Entry<>(key, value);
            size++;
        } else {
            V oldValue = table[index].getValue();
            table[index].setValue(value);
            return oldValue;
        }

        if ((float)size / capacity >= loadFactor) {
            resize();
        }

        return null;
    }

    public synchronized V get(K key) {
        int hash = key.hashCode();
        int index = hash & (capacity - 1);
        int originalIndex = index;
        while (table[index] != null) {
            if (table[index].getKey().equals(key)) {
                return table[index].getValue();
            }
            index = (index + 1) & (capacity - 1);
            if (index == originalIndex) {
                break;
            }
        }
        return null;
    }

    public synchronized V remove(K key) {
        int hash = key.hashCode();
        int index = hash & (capacity - 1);
        int originalIndex = index;
        while (table[index] != null) {
            if (table[index].getKey().equals(key)) {
                V removedValue = table[index].getValue();
                table[index] = null;
                size--;
                // Re-hash the remaining entries to avoid clustering
                rehashAfterRemove(index);
                return removedValue;
            }
            index = (index + 1) & (capacity - 1);
            if (index == originalIndex) {
                break;
            }
        }
        return null;
    }

    private synchronized void rehashAfterRemove(int removedIndex) {
        for (int i = (removedIndex + 1) & (capacity - 1); table[i] != null; i = (i + 1) & (capacity - 1)) {
            Entry<K, V> entry = table[i];
            table[i] = null;
            size--;
            put(entry.getKey(), entry.getValue());
        }
    }

    public synchronized boolean containsKey(K key) {
        int hash = key.hashCode();
        int index = hash & (capacity - 1);
        int originalIndex = index;
        while (table[index] != null) {
            if (table[index].getKey().equals(key)) {
                return true;
            }
            index = (index + 1) & (capacity - 1);
            if (index == originalIndex) {
                break;
            }
        }
        return false;
    }

    public synchronized int size() {
        return size;
    }

    private synchronized void resize() {
        Entry<K, V>[] oldTable = table;
        capacity *= 2;
        table = new Entry[capacity];
        size = 0;

        for (Entry<K, V> entry : oldTable) {
            if (entry != null) {
                put(entry.getKey(), entry.getValue());
            }
        }
    }

    private static class Entry<K, V> {
        private K key;
        private V value;

        public Entry(K key, V value) {
            this.key = key;
            this.value = value;
        }

        public K getKey() {
            return key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }
}

在这个版本中,我们添加了removecontainsKeysize方法,并且所有修改散列表状态的方法都被声明为synchronized,以保证线程安全。但是,请注意,完全的线程安全可能会影响性能,因此在实际应用中,你可能需要根据具体场景权衡线程安全性和性能。

此外,rehashAfterRemove方法用于在删除元素后重新散列受影响的元素,以避免由于删除操作导致的散列表局部热点。这有助于维持散列表的良好性能。

让我们通过一个表格逐步展示上述优化后的散列表代码的运作流程。我们将使用一个初始容量为8,负载因子为0.75的散列表,并执行一系列的插入、查找、删除和扩容操作。

初始状态

IndexKeyValue
0
1
2
3
4
5
6
7

操作 1: 插入 (Key=1, Value=A)

IndexKeyValue
01A
1
2
3
4
5
6
7

操作 2: 插入 (Key=9, Value=B)

由于9 % 8 = 1,但Index 1是空的,所以直接插入。

IndexKeyValue
01A
19B
2
3
4
5
6
7

操作 3: 插入 (Key=17, Value=C)

由于17 % 8 = 1,但Index 1已被占用,进行线性探测,下一个位置2是空的,所以插入Index 2

IndexKeyValue
01A
19B
217C
3
4
5
6
7

操作 4: 插入 (Key=25, Value=D)

由于25 % 8 = 1,并且Index 1Index 2均被占用,继续线性探测,直到找到Index 3是空的,所以插入Index 3

IndexKeyValue
01A
19B
217C
325D
4
5
6
7

操作 5: 查找 (Key=17)

Index 1开始,线性探测直到找到Key=17位于Index 2

IndexKeyValue
01A
19B
217C
325D
4
5
6
7

操作 6: 删除 (Key=9)

删除Key=9,并重新散列Index 2之后的所有元素。

IndexKeyValue
01A
1
217C
325D
4
5
6
7

操作 7: 插入 (Key=33, Value=E)

由于33 % 8 = 1,并且Index 1是空的,所以直接插入Index 1

IndexKeyValue
01A
133E
217C
325D
4
5
6
7

操作 8: 插入 (Key=41, Value=F)

此时,size=5,达到扩容条件(size / capacity >= loadFactor),因此先进行扩容操作,容量变为16,然后重新散列所有元素。

IndexKeyValue
01A
133E
217C
325D
1241F
15

通过上述步骤,我们可以清晰地看到散列表如何通过散列函数和线性探测来管理元素的插入、查找和删除操作,以及如何在达到特定条件时自动进行扩容,以保持良好的性能。

标签:index,capacity,int,18,value,列表,key,table,数据结构
From: https://blog.csdn.net/hummhumm/article/details/140292230

相关文章

  • 数据结构——二叉树之c语言实现堆与堆排序
    目录前言:1.二叉树的概念及结构1.1特殊的二叉树 1.2二叉树的存储结构  1.顺序存储2.链式存储 2.二叉树的顺序结构及实现 2.1堆的概念   ​编辑2.2堆的创建3.堆的实现3.1堆的初始化和销毁 初始化:销毁: 插入:向上调整:删除: 向下调整: 堆顶元素......
  • linux 上安装FTP : vsftpd (含常见问题:读取目录列表失败,的处理)
    服务器上有时候需要安装ftp以便调试或给不懂使用服务器命令的同学更新文件 1、安装vsftpdyumupdateyuminstallvsftpd2、编辑配置文件确保以下配置的值和下面一致anonymous_enable=NOlocal_enable=YESwrite_enable=YESchroot_local_user=YES这些配置......
  • 【Redis 理论与实践学习】 一、Redis的数据结构:4.Set类型
    文章目录简介Set和List的区别常用命令增删改查类命令添加元素移除元素判断元素是否存在获取集合大小获取集合所有成员随机获取元素随机移除并返回元素运算操作命令集合间操作集合间操作并存储应用场景博客点赞用户点赞操作公众号共同关注用户关注集合共同关注查询......
  • Vue3 如何接入 i18n 实现国际化多语言
    1.基本方法在Vue.js3中实现网页的国际化多语言,最常用的包是vue-i18n,通常我们会与vue-i18n-routing一起使用。vue-i18n负责根据当前页面的语言渲染文本占位符,例如:<span>{{t('Login')}}</span>当语言设置为中文时,会将Login渲染为“登录”。vue-i18n-routing负责......
  • Python数据结构详解:列表、字典、集合与元组的使用技巧
    前言哈喽,大家好!今天我要和大家分享的是关于Python中最常用的数据结构:列表、字典、集合和元组的使用技巧。你有没有遇到过在处理数据时,不知道该用哪种数据结构来存储和操作数据的情况呢?别担心,今天这篇文章就来帮你搞定这些问题,让你在数据处理上更加得心应手。最后,别忘了关......
  • 题解:洛谷 P1890 gcd区间
    题解:洛谷P1890gcd区间标签:线段树,st表,分块,dp题意给定数列\(a\),有\(m\)次询问求区间\([l,r]\)的最大公约数。思路这道题有多种写法,如标签所示。线段树线段树可以维护具有结合性的操作,很明显\(\gcd\)满足。这道题线段树跑的慢是因为无修改操作,自然没有其他\(O(1)......
  • 【数据结构】—— 单链表(single linked list)
    文章目录1、单链表的定义优点和缺点单链表的构成2、单链表的创建初始化:3、单链表的接口实现打印尾插头插尾删头删查找在指定位置之前插入在指定位置之后插入删除指定位置的节点删除指定位置之后的节点销毁链表4、源代码1、单链表的定义单链表(SinglyLinkedList......
  • 高效维护区间之和/区间最值的数据结构(一)——树状数组
    高效维护区间之和/区间最值的数据结构(一)——树状数组树状数组的核心思想:分治。将数组以二叉树的形式进行维护区间之和。设aaa为原数组,......
  • C++数据结构底层实现算法
    1.vector底层数据结构为数组,支持快速随机访问2.list底层数据结构为双向链表,支持快速增删3.deque底层数据结构为一个中央控制器和多个缓冲区,详细见STL源码剖析P146,支持首尾(中间不能)快速增删,也支持随机访问deque是一个双端队列(double-endedqueue),也是在堆中保存内容的.每个......
  • P7382 [COCI2018-2019#6] Simfonija (中位数)
    P7382[COCI2018-2019#6]Simfonija中位数不妨设\(C_i=A_i-B_i\),那么操作后的代数式可以写成:\[\sum\limits_{i=1}^n|C_i+x|\]如果\(k=0\),那么\(x\)的取值就是一个经典问题了,即\(C\)序列的中位数(偶数取中间任意)。如果\(k\ne0\),要使答案最小,就是将\(k\)个数的代价变......