面试官:你来介绍和手写布隆过滤器

一,介绍布隆过滤器:

布隆过滤器是一种高效的概率型数据结构,主要用于判断某个元素是否存在于集合中。与传统的数据结构相比,布隆过滤器在空间效率和查询速度上具有显著优势,但其代价是存在一定的误判率。

布隆过滤器最早由 Burton Howard Bloom 在1970年提出,广泛应用于网络爬虫、数据库系统、缓存系统等领域。例如,Google Chrome 浏览器就曾使用布隆过滤器来检测恶意网址,而 Redis 也通过布隆过滤器插件支持高效的去重查询。

二,布隆过滤器原理:

布隆过滤器的核心思想是利用多个哈希函数和位数组来存储数据的存在性信息。具体来说,它的实现可以分为以下几个步骤:

1,位数组初始化 布隆过滤器首先需要一个长度为 m 的位数组,初始时所有位均置为 0。

2,哈希函数映射 当向布隆过滤器中添加一个元素时,会使用 k 个不同的哈希函数对该元素进行计算,得到k个哈希值。这些哈希值会对 m 取模,从而在位数组上标记对应的位置为 1。

3,查询元素是否存在 查询时,同样使用这 k 个哈希函数计算待查元素的哈希值,并检查位数组中对应的位是否均为 1。如果所有位均为 1,则认为该元素可能存在于集合中;如果有任意一位为 0,则可以确定该元素一定不存在

大致图片如下:

这里借鉴了一下别人的图:

三,优点及局限性

优点:

1,极高的空间效率:由于仅使用位数组存储信息,布隆过滤器相比传统哈希表节省了大量内存。

2,常数时间的查询:无论存储多少元素,查询操作的时间复杂度始终是 O(k),其中 k 是哈希函数的数量。

3,天然支持高并发:由于位数组的原子性操作,布隆过滤器可以轻松应用于多线程环境。

局限性:

1,存在误判:由于哈希冲突,布隆过滤器可能会误判某些不存在的元素为存在。

2,无法删除元素:传统的布隆过滤器不支持删除操作,因为多个元素可能共享相同的位。

3,哈希函数的选择影响性能:如果哈希函数分布不均匀,可能导致误判率上升。

四,布隆过滤器实战

首先先引入依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.25.0</version> 
</dependency>

实战代码:

import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonBloomFilterExample {

    public static void main(String[] args) {
        // 1. 配置 Redisson 客户端
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://127.0.0.1:6379")
              

        // 2. 创建 Redisson 客户端
        RedissonClient redisson = Redisson.create(config);

        try {
            // 3. 获取布隆过滤器实例
            RBloomFilter<String> bloomFilter = redisson.getBloomFilter("myBloomFilter");

            // falseProbability - 误判率(0.0 < falseProbability < 1.0)
            bloomFilter.tryInit(100000L, 0.03);

            // 5. 添加元素
            bloomFilter.add("element1");
            bloomFilter.add("element2");
            bloomFilter.add("element3");

            // 6. 检查元素是否存在
            System.out.println("Contains element1: " + bloomFilter.contains("element1")); // true
            System.out.println("Contains element2: " + bloomFilter.contains("element2")); // true
            System.out.println("Contains element4: " + bloomFilter.contains("element4")); // false

            // 7. 统计功能
            System.out.println("Expected insertions: " + bloomFilter.getExpectedInsertions());
            System.out.println("False probability: " + bloomFilter.getFalseProbability());
            System.out.println("Hash iterations: " + bloomFilter.getHashIterations());
            System.out.println("Current size: " + bloomFilter.getSize());
            
        } finally {
            //关闭连接
            redisson.shutdown();
        }
    }
}

五,手写布隆过滤器

手写代码:

import java.util.BitSet;

public class SimpleBloomFilter {
    private BitSet bitSet;
    private int bitSetSize;
    private int numHashFunctions = 3; // 固定使用3个哈希函数
    
    /**
     * 简化的布隆过滤器构造方法
     * @param expectedNumItems 预期要存储的元素数量
     */
    public SimpleBloomFilter(int expectedNumItems) {
        // 简化版:每个元素分配10个bit位
        this.bitSetSize = expectedNumItems * 10;
        this.bitSet = new BitSet(bitSetSize);
    }
    
    /**
     * 添加元素到布隆过滤器
     * @param item 要添加的元素
     */
    public void add(String item) {
        // 使用3种不同的哈希方式
        int hash1 = hash1(item);
        int hash2 = hash2(item);
        int hash3 = hash3(item);
        
        bitSet.set(hash1 % bitSetSize, true);
        bitSet.set(hash2 % bitSetSize, true);
        bitSet.set(hash3 % bitSetSize, true);
    }
    
    /**
     * 检查元素是否可能在布隆过滤器中
     * @param item 要检查的元素
     * @return 如果返回false,则肯定不存在;如果返回true,则可能存在
     */
    public boolean mightContain(String item) {
        int hash1 = hash1(item);
        int hash2 = hash2(item);
        int hash3 = hash3(item);
        
        return bitSet.get(hash1 % bitSetSize) 
            && bitSet.get(hash2 % bitSetSize) 
            && bitSet.get(hash3 % bitSetSize);
    }
    
    // 第一种哈希函数:使用Java默认的hashCode
    private int hash1(String item) {
        return item.hashCode();
    }
    
    // 第二种哈希函数:简单变种
    private int hash2(String item) {
        return item.hashCode() * 31;
    }
    
    // 第三种哈希函数:另一种变种
    private int hash3(String item) {
        return Math.abs(item.hashCode() ^ (item.hashCode() >>> 16));
    }
    
    public static void main(String[] args) {
        // 测试简化版布隆过滤器
        SimpleBloomFilter filter = new SimpleBloomFilter(1000);
        
        // 添加一些元素
        filter.add("apple");
        filter.add("banana");
        filter.add("orange");
        
        // 测试存在的元素
        System.out.println(filter.mightContain("apple"));   // true
        System.out.println(filter.mightContain("banana"));  // true
        System.out.println(filter.mightContain("orange"));  // true
        
        // 测试不存在的元素
        System.out.println(filter.mightContain("grape"));   // 可能是false
        System.out.println(filter.mightContain("melon"));   // 可能是false
    }
}

这里说一下我三个哈希算法的设计思路:

第一个hash1的话:hashCode() 是Java对象的基础哈希方法,能快速生成一个初始哈希值。布隆过滤器不要求哈希函数完美无冲突,所以直接用它是可行的。

第二个hash2的话:31 是一个经典素数(HashMap也在用),乘法能扰动哈希分布,减少碰撞。

第三个hash3的话:(hashCode() >>> 16) 是 HashMap的扰动函数,目的是让高位也参与运算,减少冲突。Math.abs 确保结果为正,方便后续取模映射到布隆过滤器位数组。

<think>好的,用户问的是在面试中如果被问到数组去重该怎么回答。我需要先回想常见的数组去重方法,然后整理出一个清晰的回答结构。首先得考虑不同方法的实现方式、时间复杂度适用场景。 首先想到最简单的是用Set,ES6的Set数据结构可以自动去重,然后转回数组。这方法简单但可能不适用于对象等引用类型,因为对象即使内容相同,引用不同也会被当作不同元素。 然后是使用filterindexOf的组合。遍历数组,检查当前元素第一次出现的索引是否等于当前索引,这样可以过滤掉重复项。但这种方法时间复杂度是O(n²),对于大数组效率不高。另外,NaN的情况可能会有问题,因为indexOf无法正确识别NaN。 接下来是使用reduce,逐个累积不重复的元素。同样面临filter类似的问题,但代码更函数式。 还有一种是用对象或Map来记录已出现的元素,利用O(1)的查询时间,这样时间复杂度可以降到O(n)。但需要注意对象的键会被转为字符串,比如数字1字符串'1'会被视为相同,这时候可能需要使用typeof来区分类型。或者用ES6的Map,可以保存键的类型。 另外,针对排序后的数组,可以在遍历时比较前一个元素,跳过重复的。这种方法时间复杂度主要是排序的O(n log n),之后线性遍历,所以整体是O(n log n)。不过会改变原数组的顺序,如果要求保持原顺序就不适用了。 还要考虑特殊值的情况,比如NaN、null、undefined、对象等。例如,Set会认为NaN等于自身,所以用Set可以正确去重包含NaN的数组,而indexOf不行。 在面试回答时,应该分点说明不同方法,并比较它们的优缺点,以及适用场景。最好还能提到时间复杂度空间复杂度,展示对性能的理解。此外,询问面试官是否有特定要求,比如是否允许修改原数组,是否需要处理特定类型的数据,这样可以体现沟通能力。 可能还需要手写代码示例,比如用Set的最简方法,或者用对象的哈希表方法,这时候要注意代码的准确性边界情况处理,比如处理对象元素时的问题。 最后,总结不同方法的适用情况,建议根据实际需求选择合适的方法,并说明自己会考虑数据规模、类型以及环境支持(比如是否需要兼容ES5)等因素来决定使用哪种方法。</think>在面试中回答“数组去重”问题时,可以按以下结构组织答案,体现技术深度逻辑性: --- ### **1. 明确问题** 先确认需求,避免假设: - “请问需要处理的是基本数据类型(如数字、字符串)还是包含对象的复杂类型?” - “是否需要保持原数组顺序?” --- ### **2. 分场景给出解决方案** #### **场景1:仅基本数据类型(ES6环境)** **方法1:利用 `Set` 数据结构** ```javascript const uniqueArray = (arr) => [...new Set(arr)]; ``` - **优点**:代码简洁,时间复杂度 `O(n)` - **缺点**:无法处理对象类型的去重(如 `{} !== {}`) --- #### **场景2:兼容对象类型或需自定义规则** **方法2:使用 `filter` + `Map` 记录唯一性** ```javascript const uniqueArray = (arr) => { const seen = new Map(); return arr.filter(item => { // 自定义唯一性判断逻辑(如根据id去重对象) const key = typeof item + JSON.stringify(item); return seen.has(key) ? false : seen.set(key, true); }); }; ``` - **优点**:支持复杂类型,可扩展性强 - **缺点**:空间复杂度 `O(n)`,需处理 `JSON.stringify` 的局限性(如函数、循环引用) --- #### **场景3:无ES6支持(传统写法)** **方法3:遍历 + 索引比对** ```javascript function uniqueArray(arr) { return arr.filter((item, index) => arr.indexOf(item) === index); } ``` - **优点**:兼容性高(ES5) - **缺点**:时间复杂度 `O(n²)`,无法处理 `NaN`(`indexOf(NaN)` 始终返回-1) --- #### **场景4:需处理 `NaN`** **方法4:手动标记 `NaN`** ```javascript const uniqueArray = (arr) => { const seen = []; return arr.filter(item => { const isNaN = Number.isNaN(item); // 处理 NaN 的特殊情况 if (isNaN && seen.includes('NaN')) return false; if (isNaN) { seen.push('NaN'); return true; } return seen.includes(item) ? false : seen.push(item); }); }; ``` --- ### **3. 分析复杂度** | 方法 | 时间复杂度 | 空间复杂度 | 适用场景 | |------------|------------|------------|------------------------| | `Set` | O(n) | O(n) | 简单类型,ES6环境 | | `Map` | O(n) | O(n) | 复杂类型,自定义规则 | | 双重遍历 | O(n²) | O(1) | 兼容性要求高 | --- ### **4. 附加思考(加分项)** - **稳定性**:是否要保持原数组顺序?(如 `Map` 方案天然保持顺序) - **特殊值处理**:如何区分 `+0` 与 `-0`?如何处理 `Symbol` 类型? - **性能优化**:超大规模数组可使用位运算或分治策略(如布隆过滤器) --- ### **5. 示例回答** “我会先确认数据结构需求。如果是基本类型且环境支持ES6,优先用 `Set` 实现,时间复杂度最优;如果包含对象,则用 `Map` 记录唯一标识;若需兼容旧环境,可以用双重遍历,但需注意性能。此外,还需处理 `NaN` 等边界情况。” --- 通过分场景、分析复杂度、提供优化方向,能体现你全面的技术思考能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值