⭐️前言⭐️
本篇文章主要针对在面试时可能涉及到的海量数据的面试题,该类型面试题常常考虑通过位图、布隆过滤器或者哈希的方式来解决。
🍉欢迎点赞 👍 收藏 ⭐留言评论
🍉博主将持续更新学习记录收获,友友们有任何问题可以在评论区留言
🍉博客中涉及源码及博主日常练习代码均已上传GitHub
📍内容导读📍
- 🍅 海量数据面试题
- 1. 哈希切割
- 2. 位图应用
- 2.1 只出现一次的整数
- 解法一:哈希切割+哈希表
- 解法二:两个位图
- 解法三:2-bit位图(两个模拟实现)
- 2.2 两个文件交集
- 解法一:哈希切割
- 解法二:位图
- 2.3 交集、并集和差集
- 2.4 不超过2次的所有整数
- 3. 布隆过滤器
- 精确算法和近似算法
- 4. 爬虫URL去重
- 1. 设计思路
- 2. 数据结构
- 3. 核心方法
- 4. 时间和空间复杂度分析
- 5. 优点和局限性
🍅 海量数据面试题
1. 哈希切割
给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?
1、找到出现次数最多的IP地址
思路:
- 哈希切割大文件:根据IP地址进行哈希分区,将大文件切分为多个小文件,每个小文件的大小可以控制在内存可处理的范围内。
- 统计每个小文件中的IP频率:使用哈希表统计每个小文件中的IP出现次数。
- 合并结果:对所有小文件的结果进行汇总,找出出现次数最多的IP。
实现步骤:
1、第一遍扫描文件,进行哈希切割
- 计算每个IP的哈希值,将其划分到不同的小文件中。
2、第二遍遍历小文件,统计IP频率
- 使用哈希表统计IP出现的次数。
3、合并结果,找出出现次数最多的IP
- 遍历所有小文件的统计结果,找出出现次数最多的IP。
代码示例:
import java.io.*;
import java.util.HashMap;
import java.util.Map;public class MostFrequentIP {private static final int NUM_PARTITIONS = 100; // 假设划分为100个小文件// 哈希切割大文件public static void partitionLogFile(String inputFile) throws IOException {BufferedReader reader = new BufferedReader(new FileReader(inputFile));BufferedWriter[] writers = new BufferedWriter[NUM_PARTITIONS];// 初始化写入流for (int i = 0; i < NUM_PARTITIONS; i++) {writers[i] = new BufferedWriter(new FileWriter("partition_" + i + ".txt"));}// 按行读取日志文件String line;while ((line = reader.readLine()) != null) {String ip = extractIP(line);int partitionIndex = Math.abs(ip.hashCode()) % NUM_PARTITIONS;writers[partitionIndex].write(ip + "\n");}// 关闭所有写入流for (BufferedWriter writer : writers) {writer.close();}reader.close();}// 从日志行中提取IP地址(假设每行都是IP地址)private static String extractIP(String line) {return line.trim();}// 统计每个小文件中的IP频率public static Map<String, Integer> countIPs(String partitionFile) throws IOException {Map<String, Integer> ipCountMap = new HashMap<>();BufferedReader reader = new BufferedReader(new FileReader(partitionFile));String ip;while ((ip = reader.readLine()) != null) {ipCountMap.put(ip, ipCountMap.getOrDefault(ip, 0) + 1);}reader.close();return ipCountMap;}// 找出出现次数最多的IPpublic static String findMostFrequentIP() throws IOException {String mostFrequentIP = null;int maxCount = 0;// 遍历所有小文件的统计结果for (int i = 0; i < NUM_PARTITIONS; i++) {Map<String, Integer> ipCountMap = countIPs("partition_" + i + ".txt");for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {if (entry.getValue() > maxCount) {maxCount = entry.getValue();mostFrequentIP = entry.getKey();}}}return mostFrequentIP;}public static void main(String[] args) throws IOException {String inputFile = "large_log_file.txt"; // 假设大文件名为large_log_file.txt// 1. 分区partitionLogFile(inputFile);// 2. 找出出现次数最多的IPString mostFrequentIP = findMostFrequentIP();System.out.println("出现次数最多的IP是:" + mostFrequentIP);}
}
2、找到Top K个出现次数最多的IP
思路:
1.哈希切割大文件:与找到最多的IP相同。
2.统计每个小文件的IP频率:使用哈希表统计每个小文件中的IP频率。
3.合并结果:使用最小堆(Min Heap)来维护前K个频率最高的IP。
实现步骤:
1.第一遍扫描文件,进行哈希切割:与上面一致。
2.第二遍遍历小文件,统计IP频率并维护Top K
- 使用哈希表统计小文件中的IP频率。
- 使用最小堆来维护全局Top K。
3.返回Top K结果。
代码实现:
import java.io.*;
import java.util.*;public class TopKFrequentIPs {private static final int NUM_PARTITIONS = 100; // 假设划分为100个小文件private static final int K = 10; // 假设找前10个IP// 哈希切割大文件public static void partitionLogFile(String inputFile) throws IOException {BufferedReader reader = new BufferedReader(new FileReader(inputFile));BufferedWriter[] writers = new BufferedWriter[NUM_PARTITIONS];for (int i = 0; i < NUM_PARTITIONS; i++) {writers[i] = new BufferedWriter(new FileWriter("partition_" + i + ".txt"));}String line;while ((line = reader.readLine()) != null) {String ip = extractIP(line);int partitionIndex = Math.abs(ip.hashCode()) % NUM_PARTITIONS;writers[partitionIndex].write(ip + "\n");}for (BufferedWriter writer : writers) {writer.close();}reader.close();}// 从日志行中提取IP地址private static String extractIP(String line) {return line.trim();}// 统计每个小文件中的IP频率并维护Top Kpublic static PriorityQueue<Map.Entry<String, Integer>> findTopK() throws IOException {PriorityQueue<Map.Entry<String, Integer>> minHeap = new PriorityQueue<>(K, Comparator.comparingInt(Map.Entry::getValue));for (int i = 0; i < NUM_PARTITIONS; i++) {Map<String, Integer> ipCountMap = countIPs("partition_" + i + ".txt");// 维护最小堆中的Top Kfor (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {if (minHeap.size() < K) {minHeap.offer(entry);} else if (entry.getValue() > minHeap.peek().getValue()) {minHeap.poll();minHeap.offer(entry);}}}return minHeap;}// 统计每个小文件中的IP频率public static Map<String, Integer> countIPs(String partitionFile) throws IOException {Map<String, Integer> ipCountMap = new HashMap<>();BufferedReader reader = new BufferedReader(new FileReader(partitionFile));String ip;while ((ip = reader.readLine()) != null) {ipCountMap.put(ip, ipCountMap.getOrDefault(ip, 0) + 1);}reader.close();return ipCountMap;}public static void main(String[] args) throws IOException {String inputFile = "large_log_file.txt"; // 假设大文件名为large_log_file.txt// 1. 分区partitionLogFile(inputFile);// 2. 找到Top K个出现次数最多的IPPriorityQueue<Map.Entry<String, Integer>> topK = findTopK();List<Map.Entry<String, Integer>> result = new ArrayList<>(topK);result.sort((a, b) -> b.getValue() - a.getValue()); // 从大到小排序System.out.println("Top K个出现次数最多的IP:");for (Map.Entry<String, Integer> entry : result) {System.out.println(entry.getKey() + " : " + entry.getValue());}}
}
2. 位图应用
2.1 只出现一次的整数
题目:给定100亿个整数,设计算法找到只出现一次的整数?
由于数据量非常大,我们需要考虑内存限制,以及如何在分布式或分段的情况下进行计算。
解法一:哈希切割+哈希表
思路概述:
解决这样的大规模问题可以采用哈希切割(Hash Partitioning)和位图(BitMap)相结合的方式。具体步骤如下:
1、哈希切割数据:将100亿个整数划分为多个小文件,每个文件只包含部分整数。
2、统计每个小文件中的整数频率:在小文件中,使用哈希表记录每个整数出现的次数。
3、合并结果:汇总所有小文件的结果,找出只出现一次的整数。
实现步骤:
1、第一遍扫描文件,进行哈希切割
- 根据整数值的哈希值,将其划分到多个小文件中。
2、第二遍遍历小文件,统计每个整数的频率
- 对每个小文件使用哈希表统计出现次数。
3、合并结果
- 汇总所有小文件的统计结果,找到只出现一次的整数。
代码示例:
import java.io.*;
import java.util.HashMap;
import java.util.Map;public class FindUniqueNumber {private static final int NUM_PARTITIONS = 1000; // 假设划分为1000个小文件// 哈希切割大文件public static void partitionLargeFile(String inputFile) throws IOException {BufferedReader reader = new BufferedReader(new FileReader(inputFile));BufferedWriter[] writers = new BufferedWriter[NUM_PARTITIONS];// 初始化写入流for (int i = 0; i < NUM_PARTITIONS; i++) {writers[i] = new BufferedWriter(new FileWriter("partition_" + i + ".txt"));}// 按行读取大文件String line;while ((line = reader.readLine()) != null) {int num = Integer.parseInt(line.trim());int partitionIndex = Math.abs(num) % NUM_PARTITIONS;writers[partitionIndex].write(num + "\n");}// 关闭所有写入流for (BufferedWriter writer : writers) {writer.close();}reader.close();}// 统计每个小文件中的整数频率public static Map<Integer, Integer> countFrequency(String partitionFile) throws IOException {Map<Integer, Integer> frequencyMap = new HashMap<>();BufferedReader reader = new BufferedReader(new FileReader(partitionFile));String line;while ((line = reader.readLine()) != null) {int num = Integer.parseInt(line.trim());frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);}reader.close();return frequencyMap;}// 找出只出现一次的整数public static Integer findUniqueNumber() throws IOException {for (int i = 0; i < NUM_PARTITIONS; i++) {Map<Integer, Integer> frequencyMap = countFrequency("partition_" + i + ".txt");// 遍历哈希表,找到只出现一次的整数for (Map.Entry<Integer, Integer> entry : frequencyMap.entrySet()) {if (entry.getValue() == 1) {return entry.getKey(); // 返回找到的第一个只出现一次的整数}}}return null; // 没有找到只出现一次的整数}public static void main(String[] args) throws IOException {String inputFile = "large_numbers_file.txt"; // 假设大文件名为large_numbers_file.txt// 1. 哈希切割大文件partitionLargeFile(inputFile);// 2. 找到只出现一次的整数Integer uniqueNumber = findUniqueNumber();if (uniqueNumber != null) {System.out.println("只出现一次的整数是:" + uniqueNumber);} else {System.out.println("没有找到只出现一次的整数。");}}
}
解法二:两个位图
如果整数的范围是已知且不大(如0到2^31-1),可以使用位图来记录每个整数的频率。这里我们用两个位图分别记录出现1次和出现2次及以上的整数。
实现步骤
1、初始化两个位图bitMap1
和bitMap2
。
2、第一遍扫描:对于每个整数num:
- 如果
bitMap1
中对应的位为0,则将其置为1。 - 如果
bitMap1
中对应的位已经为1,则将bitMap2
对应的位置为1。
3、第二遍扫描:找出只在bitMap1
中为1且在bitMap2
中为0的整数。
代码示例:
import java.io.*;
import java.util.BitSet;public class FindUniqueUsingBitMap {private static final int MAX_RANGE = Integer.MAX_VALUE; // 假设整数范围为0到2^31-1// 用两个位图记录频率private BitSet bitMap1 = new BitSet(MAX_RANGE);private BitSet bitMap2 = new BitSet(MAX_RANGE);// 第一遍扫描数据public void scanFile(String inputFile) throws IOException {BufferedReader reader = new BufferedReader(new FileReader(inputFile));String line;while ((line = reader.readLine()) != null) {int num = Integer.parseInt(line.trim());if (!bitMap1.get(num)) {bitMap1.set(num); // 第一次出现,将bitMap1对应位设为1} else {bitMap2.set(num); // 第二次出现,将bitMap2对应位设为1}}reader.close();}// 找出只出现一次的整数public int findUnique() {for (int i = 0; i < MAX_RANGE; i++) {// 只在bitMap1中为1,且bitMap2中为0if (bitMap1.get(i) && !bitMap2.get(i)) {return i;}}return -1; // 没有找到只出现一次的整数}public static void main(String[] args) throws IOException {String inputFile = "large_numbers_file.txt"; // 假设大文件名为large_numbers_file.txtFindUniqueUsingBitMap findUnique = new FindUniqueUsingBitMap();// 1. 扫描大文件findUnique.scanFile(inputFile);// 2. 找出只出现一次的整数int uniqueNumber = findUnique.findUnique();if (uniqueNumber != -1) {System.out.println("只出现一次的整数是:" + uniqueNumber);} else {System.out.println("没有找到只出现一次的整数。");}}
}
解法三:2-bit位图(两个模拟实现)
解决方案:2-bit 位图
在一个2-bit位图中,每个整数对应两个位来表示其状态:
00
:表示该整数从未出现。01
:表示该整数出现一次。10
:表示该整数出现两次。11
:表示该整数出现三次及以上。
实现思路
1、初始化2-bit位图。
2、扫描输入数据:
- 如果当前整数的状态为
00
,将其状态设置为01
。 - 如果当前整数的状态为
01
,将其状态设置为10
。 - 如果当前整数的状态为
10
,保持不变。
3、再次扫描2-bit位图,找到状态为01的整数,即只出现一次的整数。
Java中没有直接2-bit位图的实现,可以借助两个1-bit位图 BitSet模拟实现2-bit位图的四个状态,同时也可以如下所示自己实现2-bit位图。
2-bit位图模拟实现:
import java.util.Arrays;public class TwoBitMap {private byte[] elem; // 2-bit位图的字节数组public int usedSize; // 用于记录非零位的个数public TwoBitMap() {// 默认给一个大小elem = new byte[1];}/*** 初始化n个整数的2-bit位图* @param n 整数范围*/public TwoBitMap(int n) {elem = new byte[n / 4 + 1]; // 每个字节可存储4个整数的状态}/*** 获取整数num的状态* @param num 整数* @return 状态值(00、01、10或11)*/public int get(int num) {if (num < 0) {throw new IndexOutOfBoundsException();}int byteIndex = num / 4; // 计算字节索引int bitOffset = (num % 4) * 2; // 计算在字节内的偏移量// 提取对应的2-bit状态return (elem[byteIndex] >> bitOffset) & 0b11;}/*** 设置整数num的状态* @param num 整数* @param state 新的状态值(00、01、10或11)*/public void set(int num, int state) {if (num < 0 || state < 0 || state > 3) { // 状态值应为0到3throw new IndexOutOfBoundsException();}int byteIndex = num / 4;int bitOffset = (num % 4) * 2;// 扩容if (byteIndex > elem.length - 1) {elem = Arrays.copyOf(elem, byteIndex + 1);}// 清除当前2-bit状态elem[byteIndex] &= ~(0b11 << bitOffset);// 设置新的2-bit状态elem[byteIndex] |= (state << bitOffset);usedSize++;}/*** 更新整数num的状态* 00 -> 01, 01 -> 10, 10 -> 11, 11保持不变* @param num 整数*/public void update(int num) {int currentState = get(num);// 根据当前状态进行更新if (currentState == 0) {set(num, 1); // 00 -> 01} else if (currentState == 1) {set(num, 2); // 01 -> 10} else if (currentState == 2) {set(num, 3); // 10 -> 11}// 如果是11,则不变}/*** 统计2-bit位图中非零位的个数* @return 非零位的个数*/public int getUsedSize() {return this.usedSize;}public static void main(String[] args) {int[] array = {1, 2, 3, 10, 4, 18, 13, 2, 10, 18, 3}; // 测试用例TwoBitMap twoBitMap = new TwoBitMap(18);// 更新2-bit位图状态for (int i = 0; i < array.length; i++) {twoBitMap.update(array[i]);}// 找出只出现一次的整数System.out.println("只出现一次的整数:");for (int i = 0; i <= 18; i++) {if (twoBitMap.get(i) == 1) { // 01表示只出现一次System.out.println(i);}}// 找出只出现两次的整数System.out.println("只出现两次的整数:");for (int i = 0; i <= 18; i++) {if (twoBitMap.get(i) == 2) { // 10表示只出现两次System.out.println(i);}}// 找出出现三次及以上的整数System.out.println("出现三次及以上的整数:");for (int i = 0; i <= 18; i++) {if (twoBitMap.get(i) == 3) { // 11表示出现三次及以上System.out.println(i);}}}
}
2.2 两个文件交集
题目:
给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
解法一:哈希切割
思路概述
我们可以将两个大文件进行哈希切割,将整数划分成多个较小的分区,每个分区的数据量可以控制在内存可处理的范围内。然后在每个分区内使用位图或哈希表计算交集。
实现步骤
- 哈希切割
- 首先对两个文件进行哈希切割,将大文件划分成多个较小的分区,每个分区可以独立处理。
- 通过哈希函数,将相同的整数映射到相同的对应分区文件中,这样可以确保相同的整数出现在相同的分区。
- 统计交集
- 针对每个分区文件,使用位图(BitMap)或哈希表来存储和查找交集。
- 合并结果
- 对每个分区的结果进行汇总,得到全局的交集。
具体代码实现(Java)
代码说明
- 使用哈希切割分区文件。
- 在分区文件中寻找交集。
- 合并各个分区的交集。
import java.io.*;
import java.util.BitSet;
import java.util.HashSet;
import java.util.Set;public class FindIntersection {private static final int NUM_PARTITIONS = 100; // 假设划分为100个小文件private static final int MAX_RANGE = Integer.MAX_VALUE; // 假设整数范围为0到2^31-1// 哈希切割大文件public static void partitionFile(String inputFile, String outputPrefix) throws IOException {BufferedReader reader = new BufferedReader(new FileReader(inputFile));BufferedWriter[] writers = new BufferedWriter[NUM_PARTITIONS];// 初始化写入流for (int i = 0; i < NUM_PARTITIONS; i++) {writers[i] = new BufferedWriter(new FileWriter(outputPrefix + "_" + i + ".txt"));}// 按行读取大文件并进行哈希切割String line;while ((line = reader.readLine()) != null) {int num = Integer.parseInt(line.trim());int partitionIndex = Math.abs(num) % NUM_PARTITIONS;writers[partitionIndex].write(num + "\n");}// 关闭所有写入流for (BufferedWriter writer : writers) {writer.close();}reader.close();}// 找出分区文件的交集public static Set<Integer> findIntersectionInPartition(String file1, String file2) throws IOException {Set<Integer> set1 = new HashSet<>();Set<Integer> intersection = new HashSet<>();// 读取第一个分区文件,将整数存入集合BufferedReader reader1 = new BufferedReader(new FileReader(file1));String line;while ((line = reader1.readLine()) != null) {int num = Integer.parseInt(line.trim());set1.add(num);}reader1.close();// 读取第二个分区文件,查找交集BufferedReader reader2 = new BufferedReader(new FileReader(file2));while ((line = reader2.readLine()) != null) {int num = Integer.parseInt(line.trim());if (set1.contains(num)) {intersection.add(num);}}reader2.close();return intersection;}// 合并所有分区的交集public static Set<Integer> findTotalIntersection(String filePrefix1, String filePrefix2) throws IOException {Set<Integer> totalIntersection = new HashSet<>();// 遍历所有分区for (int i = 0; i < NUM_PARTITIONS; i++) {String partitionFile1 = filePrefix1 + "_" + i + ".txt";String partitionFile2 = filePrefix2 + "_" + i + ".txt";// 找出当前分区的交集Set<Integer> partitionIntersection = findIntersectionInPartition(partitionFile1, partitionFile2);totalIntersection.addAll(partitionIntersection);}return totalIntersection;}public static void main(String[] args) throws IOException {String inputFile1 = "large_file_1.txt"; // 假设大文件1的路径String inputFile2 = "large_file_2.txt"; // 假设大文件2的路径// 1. 对两个大文件进行哈希切割partitionFile(inputFile1, "partition_file_1");partitionFile(inputFile2, "partition_file_2");// 2. 找出两个文件的交集Set<Integer> totalIntersection = findTotalIntersection("partition_file_1", "partition_file_2");// 3. 输出交集System.out.println("交集中的整数:");for (int num : totalIntersection) {System.out.println(num);}}
}
代码解释
-
哈希切割(
partitionFile
方法):- 将大文件划分为多个较小的分区文件。
- 根据整数的哈希值,将其分配到不同的分区文件中,确保相同的整数会被分配到相同的分区。
-
查找分区交集(
findIntersectionInPartition
方法):- 对每个分区文件,使用
HashSet
存储第一个文件的整数。 - 再次遍历第二个文件的相应分区,找到两个分区文件的交集。
- 对每个分区文件,使用
-
合并所有分区的交集(
findTotalIntersection
方法):- 遍历所有分区的交集,合并结果得到全局交集。
解法二:位图
思路:
1、遍历第一个文件,将第一个文件的每个数据读取出来,存放到bitSet当中
2、遍历第二个文件,每次读第一个数据,就看bitSet中,之前是否存在
3、如果存在,就是交集
2.3 交集、并集和差集
参考2.2中的解法2,其实我们可以发现,如果把两个文件中的数据都读取出来,存放到位图中,那两个位图去做按位与操作,就可以获取到交集
按位与操作,只有都是1才是1,那么这个时候对应位上是1,说明两个文件中都有该数据
相同的思路,如果我们对两个位图进行其他操作,可以得到其他集合;
比如两个位图按位或操作,就可以得到并集;两个集合按位异或操作,就可以得到差集
按位或,即两个位图的同一位置上,有一个1则整体都是1,那最后按位或后的位图,一个位置上有1,则说明对应数据在两个文件中至少有一个数据存在,该位图即为并集;
按位异或,即两个位图进行无进位加法操作,0的还是0,都是1的也变0,只有其中一个是1的还保留1,那最后得到的这个位图中的每一个1,都代表两个中只有其中一个有,即为差集
2.4 不超过2次的所有整数
题目:
位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
解法1:哈希切割
同2.1哈希切割,把数据用哈希,分成多个小文件,然后分组统计最后汇总
解法2:使用两个位图
用两个位图的同一个位置,去维护四个状态,模拟一个2-bit位图;
00
:表示该整数从未出现。01
:表示该整数出现一次。10
:表示该整数出现两次。11
:表示该整数出现三次及以上。
先第一遍遍历,把数据的出现次数状态,存到两个位图中。
然后二次遍历,排除11
状态的,其他的全部计数
解法二代码示例:
import java.io.*;
import java.util.BitSet;public class FindIntegersWithMax2Occurrences {private static final int MAX_RANGE = Integer.MAX_VALUE; // 假设整数范围是0到2^31-1private BitSet bitSet1; // 第一个BitSetprivate BitSet bitSet2; // 第二个BitSetpublic FindIntegersWithMax2Occurrences(int maxRange) {// 初始化两个BitSetbitSet1 = new BitSet(maxRange);bitSet2 = new BitSet(maxRange);}/*** 更新整数的2-bit位图状态* @param num 整数*/private void updateBitSets(int num) {boolean bit1 = bitSet1.get(num);boolean bit2 = bitSet2.get(num);if (!bit1 && !bit2) {// 当前状态是00,更新为01bitSet1.set(num);} else if (bit1 && !bit2) {// 当前状态是01,更新为10bitSet1.clear(num);bitSet2.set(num);} else if (!bit1 && bit2) {// 当前状态是10,更新为11bitSet1.set(num);}// 如果是11,则保持不变}/*** 第一次遍历文件,更新2-bit位图的状态* @param inputFile 输入文件*/public void firstPass(String inputFile) throws IOException {BufferedReader reader = new BufferedReader(new FileReader(inputFile));String line;while ((line = reader.readLine()) != null) {int num = Integer.parseInt(line.trim());updateBitSets(num);}reader.close();}/*** 第二次遍历文件,找出出现次数不超过2次的整数* @param inputFile 输入文件*/public void secondPass(String inputFile) throws IOException {BufferedReader reader = new BufferedReader(new FileReader(inputFile));String line;System.out.println("出现次数不超过2次的整数:");while ((line = reader.readLine()) != null) {int num = Integer.parseInt(line.trim());boolean bit1 = bitSet1.get(num);boolean bit2 = bitSet2.get(num);// 如果状态是00、01或10,输出该整数if (!(bit1 && bit2)) {System.out.println(num);}}reader.close();}public static void main(String[] args) throws IOException {String inputFile = "large_numbers_file.txt"; // 假设大文件名为large_numbers_file.txtFindIntegersWithMax2Occurrences finder = new FindIntegersWithMax2Occurrences(MAX_RANGE);// 1. 第一次遍历文件,更新2-bit位图状态finder.firstPass(inputFile);// 2. 第二次遍历文件,找出出现次数不超过2次的整数finder.secondPass(inputFile);}
}
3. 布隆过滤器
精确算法和近似算法
题目:
给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
精确算法:
使用哈希切割,分别把两个文件,用相同的哈希方式,切割成同样份数的小文件,再用一一对应的小文件取交集,最后把所有交集汇总起来即可。
近似算法:
1、把第一个文件当中的query映射到布隆过滤器中
2、读取第二个文件,每个query都去布隆过滤器中查找
3、因为可能存在误判,所以这个数据有可能没有,但是被误判为了有。
4. 爬虫URL去重
题目:
多线程并发爬虫时,对url做去重,让你设计整个数据结构和核心方法,思考查询时间复杂度、空间复杂度等因素
可以使用 布隆过滤器(Bloom Filter) 结合 HashSet 来设计多线程并发爬虫中的 URL 去重方案。这种方案在保证较快的查询速度的同时,也能有效控制内存消耗。
1. 设计思路
- 布隆过滤器:用来判断某个 URL 是否可能已经存在,可以节省空间,但有一定的误判率。
- HashSet:在布隆过滤器判断为可能存在时进行二次确认,以确保去重的准确性。
2. 数据结构
- 布隆过滤器(BloomFilter):一个基于哈希的位数组,能够高效地判断元素是否存在,空间复杂度低。
add(url)
: 将 URL 添加到布隆过滤器中。mightContain(url)
: 检查 URL 是否可能存在。
- HashSet:存储经过布隆过滤器验证的 URL,用于消除误判。
contains(url)
: 检查 URL 是否已存在。add(url)
: 将 URL 添加到集合中。
3. 核心方法
以下是Java中实现的核心代码:
import java.util.BitSet;
import java.util.HashSet;
import java.util.concurrent.locks.ReentrantLock;public class UrlDeduplication {// 布隆过滤器位数组private final BitSet bloomFilter;// 位数组的大小private final int size;// HashSet用于消除误判private final HashSet<String> urlSet;// ReentrantLock实现线程安全private final ReentrantLock lock;// 哈希函数数量private static final int HASH_COUNT = 3;// 初始化布隆过滤器和HashSetpublic UrlDeduplication(int size) {this.size = size;this.bloomFilter = new BitSet(size);this.urlSet = new HashSet<>();this.lock = new ReentrantLock();}// 计算哈希值private int[] hash(String url) {int[] hashValues = new int[HASH_COUNT];for (int i = 0; i < HASH_COUNT; i++) {hashValues[i] = Math.abs(url.hashCode() + i) % size;}return hashValues;}// 添加URL到布隆过滤器和HashSetpublic void addUrl(String url) {int[] hashValues = hash(url);lock.lock(); // 加锁保证线程安全try {// 更新布隆过滤器for (int hash : hashValues) {bloomFilter.set(hash);}// 更新HashSeturlSet.add(url);} finally {lock.unlock(); // 解锁}}// 判断URL是否存在public boolean containsUrl(String url) {int[] hashValues = hash(url);lock.lock(); // 加锁保证线程安全try {// 检查布隆过滤器for (int hash : hashValues) {if (!bloomFilter.get(hash)) {return false; // 肯定不存在}}// 可能存在,再次确认return urlSet.contains(url);} finally {lock.unlock(); // 解锁}}
}
4. 时间和空间复杂度分析
- 时间复杂度
addUrl(url)
:- 布隆过滤器:O(k),其中 k 为哈希函数的数量。
- HashSet:O(1)。
- 总体:O(k) + O(1) ≈ O(1)。
containsUrl(url)
:- 布隆过滤器:O(k)。
- HashSet:O(1)。
- 总体:O(k) + O(1) ≈ O(1)。
- 空间复杂度
- 布隆过滤器:O(n),其中 n 为位数组的大小。
- HashSet:O(m),其中 m 为实际存储的 URL 数量。
- 总体:O(n + m)。
5. 优点和局限性
- 优点
- 具有较高的空间效率,能够有效去重。
- 支持多线程并发访问。
- 局限性
- 布隆过滤器可能产生误判,需要使用 HashSet 进行二次确认。
- 随着数据量增加,HashSet 的内存占用会变大。
这种方案在多线程并发环境中较为高效,可以在爬虫系统中实现较好的性能与去重效果。
⭐️最后的话⭐️
总结不易,希望uu们不要吝啬你们的👍哟(^U^)ノ~YO!!如有问题,欢迎评论区批评指正😁