[中间件~大厂面试题] 腾讯三面,40亿的QQ号如何去重

news2024/11/27 12:34:58

前言: 在Spring Boot框架下,可以使用以下方法来去重40亿个QQ号.请注意:QQ号码的理论最大值为 2 32 − 1 2^{32} - 1 2321,大概是43亿左右。

文章目录

  • 提前总结(总分总~~~)
  • 最粗鲁的方式
    • 1. 使用HashSet去重:
    • 2. 使用Java 8的Stream去重:
    • 3. 使用数据库的去重功能:
  • 限制1GB内存,文件的方式
    • 4. 文件分片
    • 5. 外部排序算法
  • 使用中间件 redis
    • 6. bitmap
    • 7. 布隆过滤器
  • 分析一下布隆过滤器以及bitmap存储40亿个QQ号需要的内存
    • 布隆过滤器:
    • 位图(Bitmap):
  • 总结
    • 1. 使用 HashSet 去重:
    • 2. 使用 Java 8 的 Stream 去重:
    • 3. 使用数据库的去重功能:
    • 4. 文件分片:
    • 5. 外部排序算法:
    • 6. 位图(Bitmap):
    • 7. 布隆过滤器:

提前总结(总分总~~~)

  • 如果限制在1GB内存,并且不依赖外部存储或中间件, HashSetJava 8 Stream 都无法满足要求。
  • 文件分片和外部排序算法可以适应1GB内存限制,但涉及到额外的文件操作和排序步骤。
  • 使用数据库的去重功能可能需要额外的存储开销。
  • Redis作为中间件可以满足内存限制,但需要依赖Redis服务。
  • 布隆过滤器是一种适用于大规模数据去重的高效数据结构,可以根据预期插入数量和误判率进行内存估算。

最粗鲁的方式

1. 使用HashSet去重:

Set<String> qqSet = new HashSet<>();
// 假设qqList是包含40亿个QQ号的列表
for (String qq : qqList) {
    qqSet.add(qq);
}
List<String> distinctQQList = new ArrayList<>(qqSet);

2. 使用Java 8的Stream去重:

List<String> distinctQQList = qqList.stream()
    .distinct()
    .collect(Collectors.toList());

3. 使用数据库的去重功能:

  1. 首先,创建一个数据库表来存储QQ号,可以使用MySQL或其他关系型数据库。
  2. 将40亿个QQ号逐个插入数据库表中,数据库会自动去重。
  3. 最后,从数据库中读取去重后的QQ号列表:
// 假设使用Spring Data JPA进行数据库操作
@Repository
public interface QQRepository extends JpaRepository<QQEntity, Long> {
    List<QQEntity> findAll();
}

// 在业务逻辑中使用QQRepository获取去重后的QQ号列表
List<QQEntity> distinctQQList = qqRepository.findAll();

限制1GB内存,文件的方式

4. 文件分片

  1. 将40亿个QQ号分成多个较小的分段,每个分段可以包含一定数量的QQ号。
  2. 针对每个分段,使用HashSet进行去重,但是只在内存中保留部分分段的去重结果,而不是全部。
  3. 将每个分段的去重结果写入磁盘文件,以释放内存空间。
  4. 对下一个分段进行去重操作,重复上述步骤,直到处理完所有分段。
  5. 最后,将所有磁盘文件中的去重结果合并成最终的去重结果。
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class QQDuplicateRemover {
   private static final int MAX_MEMORY_USAGE_MB = 1024; // 最大内存使用限制,单位:MB
   private static final int MAX_SEGMENT_SIZE = 1000000; // 每个分段的最大数量

   public static void main(String[] args) {
       // 假设qqList是包含40亿个QQ号的列表
       List<String> qqList = loadQQList();

       // 分段处理
       int segmentCount = (int) Math.ceil((double) qqList.size() / MAX_SEGMENT_SIZE);
       List<File> segmentFiles = new ArrayList<>();
       for (int i = 0; i < segmentCount; i++) {
           List<String> segment = qqList.subList(i * MAX_SEGMENT_SIZE, Math.min((i + 1) * MAX_SEGMENT_SIZE, qqList.size()));
           Set<String> segmentSet = new HashSet<>(segment);
           segment.clear(); // 释放内存

           File segmentFile = writeSegmentToFile(segmentSet);
           segmentFiles.add(segmentFile);
           segmentSet.clear();
       }

       // 合并去重结果
       List<String> distinctQQList = mergeSegments(segmentFiles);

       // 输出去重后的QQ号列表
       for (String qq : distinctQQList) {
           System.out.println(qq);
       }
   }

   // 加载40亿个QQ号列表的示例方法
   private static List<String> loadQQList() {
       // TODO: 实现加载40亿个QQ号列表的逻辑,返回List<String>
       return new ArrayList<>();
   }

   // 将分段的去重结果写入磁盘文件
   private static File writeSegmentToFile(Set<String> segmentSet) {
       File segmentFile;
       BufferedWriter writer = null;
       try {
           segmentFile = File.createTempFile("qq_segment_", ".txt");
           writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(segmentFile), StandardCharsets.UTF_8));
           for (String qq : segmentSet) {
               writer.write(qq);
               writer.newLine();
           }
       } catch (IOException e) {
           throw new RuntimeException("Failed to write segment to file.", e);
       } finally {
           if (writer != null) {
               try {
                   writer.close();
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
       }
       return segmentFile;
   }

   // 合并分段文件并去重
   private static List<String> mergeSegments(List<File> segmentFiles) {
       List<String> distinctQQList = new ArrayList<>();
       PriorityQueue<BufferedReader> fileReaders = new PriorityQueue<>(Comparator.comparing(QQDuplicateRemover::readNextLine));
       for (File segmentFile : segmentFiles) {
           try {
               BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(segmentFile), StandardCharsets.UTF_8));
               String line = reader.readLine();
               if (line != null) {
                   fileReaders.offer(reader);
               }
           } catch (IOException e) {
               throw new RuntimeException("Failed to read segment file: " + segmentFile.getName(), e);
           }
       }

       String currentQQ = null;
       while (!fileReaders.isEmpty()) {
           BufferedReader reader = fileReaders.poll();
           String qq = readNextLine(reader);
           if (!qq.equals(currentQQ)) {
               distinctQQList.add(qq);
               currentQQ = qq;
           }

           String nextQQ = readNextLine(reader);
           if (nextQQ != null) {
               fileReaders.offer(reader);
           } else {
               try {
                   reader.close();
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
       }

       return distinctQQList;
   }

   // 读取下一行数据
   private static String readNextLine(BufferedReader reader) {
       try {
           return reader.readLine();
       } catch (IOException e) {
           throw new RuntimeException("Failed to read line from segment file.", e);
       }
   }
}

这个示例代码中,使用了分段处理的思路。首先,将40亿个QQ号划分为较小的分段,并使用HashSet进行去重。然后,将每个分段的去重结果写入临时文件,以释放内存。最后,合并所有分段文件并去重,得到最终的去重结果。

请注意,示例代码中的loadQQList()方法是一个占位方法,需要根据实际情况实现另外,这个示例代码中使用了临时文件来存储分段的去重结果。在实际应用中,你可能需要根据需求进行适当的修改,例如指定输出文件路径、处理异常情况等。此外,由于内存限制为1GB,你可能需要根据实际情况调整MAX_SEGMENT_SIZE的大小,以确保每个分段在内存中的大小不超过限制。

这只是一个基本的示例代码,供你参考和理解分段处理的思路。根据实际需求和具体情况,你可能需要进行更多的优化和改进。

5. 外部排序算法

  1. 将40亿个QQ号划分为多个小文件,每个文件包含一部分QQ号。
  2. 对每个小文件进行内部排序,可以使用归并排序等算法。
  3. 合并排序后的小文件,使用归并排序的思想,逐步合并文件,同时去除重复的QQ号。
  4. 最终得到去重后的结果。
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class QQDuplicateRemover {
    private static final int MAX_MEMORY_USAGE_MB = 1024; // 最大内存使用限制,单位:MB
    private static final int MAX_SEGMENT_SIZE = 1000000; // 每个分段的最大数量

    public static void main(String[] args) {
        // 假设qqList是包含40亿个QQ号的列表
        List<String> qqList = loadQQList();

        // 外部排序去重
        List<File> sortedSegmentFiles = externalSort(qqList);

        // 合并去重结果
        List<String> distinctQQList = mergeSortedSegments(sortedSegmentFiles);

        // 输出去重后的QQ号列表
        for (String qq : distinctQQList) {
            System.out.println(qq);
        }
    }

    // 加载40亿个QQ号列表的示例方法
    private static List<String> loadQQList() {
        // TODO: 实现加载40亿个QQ号列表的逻辑,返回List<String>
        return new ArrayList<>();
    }

    // 外部排序算法
    private static List<File> externalSort(List<String> qqList) {
        List<File> segmentFiles = new ArrayList<>();
        int segmentCount = (int) Math.ceil((double) qqList.size() / MAX_SEGMENT_SIZE);

        // 将数据分段并排序
        for (int i = 0; i < segmentCount; i++) {
            List<String> segment = qqList.subList(i * MAX_SEGMENT_SIZE, Math.min((i + 1) * MAX_SEGMENT_SIZE, qqList.size()));
            Collections.sort(segment);

            File segmentFile = writeSegmentToFile(segment);
            segmentFiles.add(segmentFile);
        }

        // 逐步合并排序后的分段文件
        while (segmentFiles.size() > 1) {
            List<File> mergedSegmentFiles = new ArrayList<>();
            for (int i = 0; i < segmentFiles.size(); i += 2) {
                File segmentFile1 = segmentFiles.get(i);
                File segmentFile2 = (i + 1 < segmentFiles.size()) ? segmentFiles.get(i + 1) : null;
                File mergedSegmentFile = mergeSegments(segmentFile1, segmentFile2);
                mergedSegmentFiles.add(mergedSegmentFile);
            }
            segmentFiles = mergedSegmentFiles;
        }

        return segmentFiles;
    }

    // 将分段数据写入临时文件
    private static File writeSegmentToFile(List<String> segment) {
        File segmentFile;
        BufferedWriter writer = null;
        try {
            segmentFile = File.createTempFile("qq_segment_", ".txt");
            writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(segmentFile), StandardCharsets.UTF_8));
            for (String qq : segment) {
                writer.write(qq);
                writer.newLine();
            }
        } catch (IOException e) {
            throw new RuntimeException("Failed to write segment to file.", e);
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return segmentFile;
    }

    // 合并两个分段文件
    private static File mergeSegments(File segmentFile1, File segmentFile2) {
        BufferedReader reader1 = null;
        BufferedReader reader2 = null;
        BufferedWriter writer = null;
        File mergedSegmentFile;
        try {
            mergedSegmentFile = File.createTempFile("qq_segment_", ".txt");
            reader1 = new BufferedReader(new InputStreamReader(new FileInputStream(segmentFile1), StandardCharsets.UTF_8));
            reader2 = new BufferedReader(new InputStreamReader(new FileInputStream(segmentFile2), StandardCharsets.UTF_8));
            writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(mergedSegmentFile), StandardCharsets.UTF_8));

            String qq1 = reader1.readLine();
            String qq2 = reader2.readLine();
            while (qq1 != null && qq2 != null) {
                if (qq1.compareTo(qq2) < 0) {
                    writer.write(qq1);
                    writer.newLine();
                    qq1 = reader1.readLine();
                } else if (qq1.compareTo(qq2) > 0) {
                    writer.write(qq2);
                    writer.newLine();
                    qq2 = reader2.readLine();
                } else {
                    writer.write(qq1);
                    writer.newLine();
                    qq1 = reader1.readLine();
                    qq2 = reader2.readLine();
                }
            }

            // 处理文件剩余的数据
            while (qq1 != null) {
                writer.write(qq1);
                writer.newLine();
                qq1 = reader1.readLine();
            }
            while (qq2 != null) {
                writer.write(qq2);
                writer.newLine();
                qq2 = reader2.readLine();
            }
        } catch (IOException e) {
            throw new RuntimeException("Failed to merge segment files.", e);
        } finally {
            try {
                if (reader1 != null) {
                    reader1.close();
                }
                if (reader2 != null) {
                    reader2.close();
                }
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return mergedSegmentFile;
    }

    // 合并排序后的分段文件并去重
    private static List<String> mergeSortedSegments(List<File> segmentFiles) {
        List<String> distinctQQList = new ArrayList<>();
        PriorityQueue<BufferedReader> fileReaders = new PriorityQueue<>(Comparator.comparing(QQDuplicateRemover::readNextLine));
        for (File segmentFile : segmentFiles) {
            try {
                BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(segmentFile), StandardCharsets.UTF_8));
                String line = reader.readLine();
                if (line != null) {
                    fileReaders.offer(reader);
                }
            } catch (IOException e) {
                throw new RuntimeException("Failed to read segment file: " + segmentFile.getName(), e);
            }
        }

        String currentQQ = null;
        while (!fileReaders.isEmpty()) {
            BufferedReader reader = fileReaders.poll();
            String qq = readNextLine(reader);
            if (!qq.equals(currentQQ)) {
                distinctQQList.add(qq);
                currentQQ = qq;
            }

            String nextQQ = readNextLine(reader);
            if (nextQQ != null) {
                fileReaders.offer(reader);
            } else {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return distinctQQList;
    }

    // 读取下一行数据
    private static String readNextLine(BufferedReader reader) {
        try {
            return reader.readLine();
        } catch (IOException e) {
            throw new RuntimeException("Failed to read line from segment file.", e);
        }
    }
}

这个示例代码中使用了外部排序算法来处理40亿个QQ号的去重问题。首先,将数据分段,并在内存中对每个分段进行排序。然后,逐步合并排序后的分段文件,直到只剩下一个文件为止。最后,对合并后的分段文件进行去重操作,得到最终的去重结果。

请注意,示例代码中的loadQQList()方法是一个占位方法,需要根据实际情况实现另外,这个示例代码中使用了临时文件来存储分段和合并后的结果。在实际应用中,你可能需要根据需求进行适当的修改,例如指定输出文件路径、处理异常情况等。此外,由于内存限制为1GB,你可能需要根据实际情况调整MAX_SEGMENT_SIZE的大小,以确保每个分段在内存中的大小不超过限制。

这只是一个基本的示例代码,供你参考和理解外部排序算法的实现。根据实际需求和具体情况,你可能需要进行更多的优化和改进。

使用中间件 redis

6. bitmap

Redis位图(Bitmap)是一种特殊的数据结构,用于处理位级别的操作。你可以借助Redis位图来解决去重问题,包括处理大规模数据的去重。

以下是使用Redis位图解决去重问题的一般步骤:

  1. 将需要去重的数据转换为唯一标识,例如将QQ号转换为对应的整数或字符串。

  2. 使用Redis的位图数据结构,创建一个位图键(Key)用于存储去重标记。你可以使用Redis的SETBIT命令将位图中的特定位设置为1,表示该标识已经存在。

  3. 遍历数据集,对于每个唯一标识,使用SETBIT命令设置位图中对应的位。如果该位已经被设置为1,表示标识已经存在,可以进行相应的处理(例如丢弃或记录重复数据)。

  4. 完成遍历后,可以使用位图的其他命令(如BITCOUNT)获取去重后的数量,或者使用GETBIT命令检查特定位是否被设置,以判断某个标识是否存在。

下面是一个使用Redis位图进行去重的示例代码(使用Java Redis客户端Jedis):

import redis.clients.jedis.Jedis;

public class RedisBitmapDuplicateRemover {
    private static final String BITMAP_KEY = "duplicate_bitmap";

    public static void main(String[] args) {
        // 假设qqList是包含40亿个QQ号的列表
        List<String> qqList = loadQQList();

        // 初始化Redis连接
        Jedis jedis = new Jedis("localhost");

        // 去重计数器
        int duplicateCount = 0;

        // 遍历qqList进行去重
        for (String qq : qqList) {
            long bit = Long.parseLong(qq); // 假设QQ号被转换为长整型

            // 检查位图中的对应位是否已经被设置
            boolean isDuplicate = jedis.getbit(BITMAP_KEY, bit);

            if (isDuplicate) {
                // 处理重复数据
                duplicateCount++;
            } else {
                // 设置位图中的对应位为1,表示标识已经存在
                jedis.setbit(BITMAP_KEY, bit, true);
                // 处理非重复数据
            }
        }

        // 获取去重后的数量
        long distinctCount = jedis.bitcount(BITMAP_KEY);

        System.out.println("Duplicate count: " + duplicateCount);
        System.out.println("Distinct count: " + distinctCount);

        // 关闭Redis连接
        jedis.close();
    }

    // 加载40亿个QQ号列表的示例方法
    private static List<String> loadQQList() {
        // TODO: 实现加载40亿个QQ号列表的逻辑,返回List<String>
        return new ArrayList<>();
    }
}

在这个示例中,我们首先将QQ号转换为长整型,并使用Redis位图存储去重标记。然后遍历QQ号列表,对于每个QQ号,我们使用GETBIT命令检查位图中对应的位是否已经被设置。如果已经被设置,表示该QQ号重复,我们可以进行相应的处理。如果位图中对应的位未被设置,表示该QQ号是首次出现,我们使用SETBIT命令将位图中对应的位设置为1,表示标识已经存在。

最后,我们使用BITCOUNT命令获取位图中被设置为1的位的数量,即去重后的数量。同时,我们还可以使用其他位图命令来进行更多的操作,例如使用GETBIT命令检查特定位是否被设置。

请注意,示例代码中的loadQQList()方法仍然是一个占位方法,需要根据实际情况实现。此外,你需要确保已经安装了Redis并正确配置了Redis连接。

总结而言,借助Redis位图,你可以高效地解决大规模数据的去重问题。

7. 布隆过滤器

使用布隆过滤器(Bloom Filter)可以高效地进行快速查找和去重操作,而无需存储实际的数据。以下是使用布隆过滤器实现去重的一般步骤:

初始化布隆过滤器:确定布隆过滤器的大小(比特位数)和哈希函数的数量。

将需要去重的数据转换为唯一标识,例如将QQ号转换为对应的整数或字符串。

对每个唯一标识,使用多个不同的哈希函数进行哈希计算,得到多个哈希值。

将得到的多个哈希值对应的位设置为1,表示该标识已经存在。

当需要判断某个标识是否存在时,对该标识使用相同的哈希函数进行哈希计算,并检查对应的位是否都为1。如果有任何一个位为0,则标识不存在;如果所有位都为1,则标识可能存在(存在一定的误判率)。

下面是一个使用布隆过滤器实现去重的示例代码(使用Java Bloom Filter库 Guava):

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterDuplicateRemover {
    private static final int EXPECTED_INSERTIONS = 1000000; // 预期插入数量
    private static final double FPP = 0.01; // 期望误判率

    public static void main(String[] args) {
        // 初始化布隆过滤器
        BloomFilter<CharSequence> bloomFilter = BloomFilter.create(
                Funnels.stringFunnel(), EXPECTED_INSERTIONS, FPP);

        // 假设qqList是包含大量QQ号的列表
        List<String> qqList = loadQQList();

        // 去重计数器
        int duplicateCount = 0;

        // 遍历qqList进行去重
        for (String qq : qqList) {
            if (bloomFilter.mightContain(qq)) {
                // 处理重复数据
                duplicateCount++;
            } else {
                bloomFilter.put(qq);
                // 处理非重复数据
            }
        }

        // 获取去重后的数量
        long distinctCount = qqList.size() - duplicateCount;

        System.out.println("Duplicate count: " + duplicateCount);
        System.out.println("Distinct count: " + distinctCount);
    }

    // 加载大量QQ号列表的示例方法
    private static List<String> loadQQList() {
        // TODO: 实现加载大量QQ号列表的逻辑,返回List<String>
        return new ArrayList<>();
    }
}

在这个示例中,我们使用Guava库中的Bloom Filter实现去重功能。首先,我们初始化布隆过滤器 bloomFilter,指定预期插入数量和期望误判率。然后,遍历QQ号列表 qqList,对于每个QQ号,我们首先使用 mightContain() 方法判断该QQ号是否可能已经存在于布隆过滤器中。如果返回 true,表示该QQ号可能已经存在,我们可以进行相应的处理。如果返回 false,表示该QQ号很可能不存在,我们使用 put() 方法将其插入布隆过滤器中,并进行相应的处理。

最后,我们可以通过计算重复数据的数量,以及使用列表总数量减去重复数量,得到去重后的数量。

请注意,示例代码中的 loadQQList() 方法仍然是一个占位方法,需要根据实际情况实现。此外,你需要确保已经引入了Guava库并正确配置了依赖项。

总结而言,布隆过滤器是一种高效的去重数据结构,适用于大规模数据的去重操作。然而,布隆过滤器存在一定的误判率,因此在需要高精确度的场景下,可能需要额外的校验手段来确认数据的存在与否。

分析一下布隆过滤器以及bitmap存储40亿个QQ号需要的内存

布隆过滤器和位图(Bitmap)都是常见的数据结构,用于高效地存储和查询大量数据。下面我会分析一下布隆过滤器和位图存储40亿个QQ号所需的内存。

布隆过滤器:

要估算布隆过滤器需要的具体内存量,需要考虑以下几个因素:

  1. 期望的插入数量(Expected Insertions):这是指预计会插入到布隆过滤器中的元素数量。

  2. 期望的误判率(False Positive Probability):这是指布隆过滤器允许的判断错误的概率,也就是将一个不存在的元素误判为存在的概率。

  3. 哈希函数的数量(Hash Functions Count):布隆过滤器使用多个哈希函数来将输入元素映射到位数组中的多个位置,这有助于减小误判率。

布隆过滤器的内存占用可以通过以下公式近似计算:

M = − ( N ∗ l n ( p ) ) / ( l n ( 2 ) 2 ) M = -(N * ln(p)) / (ln(2)^2) M=(Nln(p))/(ln(2)2)

其中,M 是所需的比特位数(内存占用),N 是预期插入数量,p 是期望误判率。

请注意,该公式仅提供了一个近似值,并且假设哈希函数的输出是均匀分布的。实际上,布隆过滤器的性能和内存占用与哈希函数的选择、哈希函数之间的相互独立性等因素也有关系。

例如,假设我们希望将40亿个QQ号存储在布隆过滤器中,并且期望误判率为0.01%(0.0001),我们可以将这些值代入公式进行估算:

M = ( 40 亿 ∗ l n ( 0.0001 ) ) / ( l n ( 2 ) 2 ) M = (40亿 * ln(0.0001)) / (ln(2)^2) M=(40亿ln(0.0001))/(ln(2)2)
B y t e s = c e i l ( M / 8 ) Bytes = ceil(M / 8) Bytes=ceil(M/8)

根据计算结果,布隆过滤器大约需要占用约 596.05MB 的内存。

需要注意的是,这只是一个估算值,实际的内存占用可能会有所偏差。同时,为了避免误判率过高,可能需要使用更多的哈希函数,从而增加内存占用。

因此,具体的内存占用量还需根据实际需求和误判率要求进行调整和测试。

位图(Bitmap):

位图的内存分析源于腾讯三面:40 亿个 QQ 号码如何去重?

位图是一种使用比特位来表示数据存在与否的数据结构。对于40亿个QQ号,每个QQ号可以用一个唯一的整数或哈希值来表示。
如果我们使用32位整数来表示每个QQ号,那么每个整数需要占用32个比特位。因此,存储40亿个QQ号所需的内存为:
来看绝招!我们可以对hashmap进行优化,采用bitmap这种数据结构,可以顺利地同时解决时间问题和空间问题。
在很多实际项目中,bitmap经常用到。我看了不少组件的源码,发现很多地方都有bitmap实现,bitmap图解如下:

在这里插入图片描述

这是一个unsigned char类型,可以看到,共有8位,取值范围是[0, 255],如上这个unsigned char的值是255,它能标识0~7这些数字都存在。
同理,如下这个unsigned char类型的值是254,它对应的含义是:1~7这些数字存在,而数字0不存在:

在这里插入图片描述

由此可见,一个unsigned char类型的数据,可以标识0~7这8个整数的存在与否。以此类推:

一个unsigned int类型数据可以标识0~31这32个整数的存在与否。
两个unsigned int类型数据可以标识0~63这64个整数的存在与否。
显然,可以推导出来:512MB大小足够标识所有QQ号码的存在与否,请注意:QQ号码的理论最大值为2^32 - 1,大概是43亿左右。
接下来的问题就很简单了:用512MB的unsigned int数组来记录文件中QQ号码的存在与否,形成一个bitmap,比如:

bitmapFlag[123] = 1
bitmapFlag[567] = 1
bitmapFlag[123] = 1
bitmapFlag[890] = 1

实际上就是:

bitmapFlag[123] = 1
bitmapFlag[567] = 1
bitmapFlag[890] = 1

然后从小到大遍历所有正整数(4字节),当bitmapFlag值为1时,就表明该数是存在的。

总结

1. 使用 HashSet 去重:

这是一种简单粗暴的方法,使用内置的哈希集合数据结构。
需要将40亿个QQ号全部加载到内存中,因此可能会超出1GB的内存限制。
优点是实现简单,查询性能较好,但内存占用较高。

2. 使用 Java 8 的 Stream 去重:

利用 Java 8 的流操作特性,通过 distinct() 方法去重。
同样需要将40亿个QQ号全部加载到内存中,可能会超出1GB的内存限制。
优点是简单易用,但内存占用较高。

3. 使用数据库的去重功能:

利用数据库的去重功能,将QQ号作为唯一索引或使用 DISTINCT 关键字进行查询。
需要将数据存储在数据库中,可能需要额外的存储开销。
优点是适用于大规模数据,但需要依赖数据库系统。

4. 文件分片:

将数据进行分片存储,每个文件存储部分QQ号,然后逐个文件进行去重。
可以通过逐个读取文件并使用 HashSet 进行去重,最后合并结果。
可以适应1GB内存限制,但需要额外的文件操作和合并步骤。

5. 外部排序算法:

使用外部排序算法,将数据划分为多个部分,在排序过程中进行去重。
可以适应1GB内存限制,但需要额外的排序和合并步骤。

6. 位图(Bitmap):

使用位图存储40亿个QQ号,每个QQ号使用一个比特位表示存在与否。
需要大约512MB的内存来存储40亿个QQ号。

7. 布隆过滤器:

使用布隆过滤器存储40亿个QQ号,以较小的内存(596.05MB )占用来实现去重功能。
布隆过滤器的具体内存占用取决于预期插入数量和期望误判率,可以根据公式进行估算。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1056011.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入理解Linux网络笔记(二):内核和用户进程协作之阻塞方式

本文为《深入理解Linux网络》学习笔记&#xff0c;使用的Linux源码版本是3.10&#xff0c;网卡驱动默认采用的都是Intel的igb网卡驱动 Linux源码在线阅读&#xff1a;https://elixir.bootlin.com/linux/v3.10/source 2、内核是如何与用户进程协作的&#xff08;一&#xff09; …

疯 狂 的 文 件 夹 【收藏起来】

文章目录 &#x1f4c1;故事的开始&#x1f4c1;一起疯狂&#x1f4da;资源领取 专栏Python零基础入门篇&#x1f525;Python网络蜘蛛&#x1f525;Python数据分析Django基础入门宝典&#x1f525;小玩意儿&#x1f525;Web前端学习tkinter学习笔记Excel自动化处理 &#x1f4…

力扣-345.反转字符串中的元音字母

Idea 将s中的元音字母存在字符串sv中&#xff0c;并且使用一个数组依次存储元音字母的下标。 然后将字符串sv进行反转&#xff0c;并遍历元音下标数组&#xff0c;将反转后的字符串sv依次插入到源字符串s中 AC Code class Solution { public:string reverseVowels(string s) {…

力扣 -- 1049. 最后一块石头的重量 II(01背包问题)

参考代码&#xff1a; 未优化代码&#xff1a; class Solution { public:int lastStoneWeightII(vector<int>& stones) {int nstones.size();int sum0;for(const auto& e:stones){sume;}int aimsum/2;//多开一行&#xff0c;多开一列vector<vector<int&g…

排序---P1116 车厢重组

P1116 车厢重组 来自 <车厢重组 - 洛谷> 其实这道题本质上就是求逆序对的过程&#xff1a; 两种方法&#xff1a;一个是通过冒泡排序过程求逆序对&#xff1b;一个是通过归并排序过程求逆序对。 法一&#xff1a;当通过冒泡排序进行正序排列时&#xff0c;相邻两个数需要…

批量将文件名称符合要求的文件自动复制到新文件夹:Python实现

本文介绍基于Python语言&#xff0c;读取一个文件夹&#xff0c;并将其中每一个子文件夹内符合名称要求的文件加以筛选&#xff0c;并将筛选得到的文件复制到另一个目标文件夹中的方法。 本文的需求是&#xff1a;现在有一个大的文件夹&#xff0c;其中含有多个子文件夹&#x…

Redis与分布式-集群搭建

接上文 Redis与分布式-哨兵模式 1. 集群搭建 搭建简单的redis集群&#xff0c;创建6个配置&#xff0c;开启集群模式&#xff0c;将之前配置过的redis删除&#xff0c;重新复制6份 针对主节点redis 1&#xff0c;redis 2&#xff0c;redis 3都是以上修改内容&#xff0c;只是…

C++位图—布隆过滤器

目录 位图概念位图应用 布隆过滤器简介布隆过滤器的优缺点布隆过滤器应用场景布隆过滤器实现布隆过滤器误判率分析 总结 位图概念 位图是一种数据结构&#xff0c;用于表示一组元素的存在或不存在&#xff0c;通常用于大规模数据集的快速查询。它基于一个位数组&#xff08;或位…

管理经济学基本概念(二): 规模经济、需求曲线、供给曲线等

1、关键术语 1.1、边际报酬递减规律 边际报酬递减规律是指随着产出量的扩大&#xff0c;边际生产率(与增量投入要素相联系的增量产出量)最终会下降。 递增的边际生产率意味着边际成本递增。 递增的边际成本最终导致平均成本递增。 1.2、规模经济 (1) 如果长期平均成本相对…

打开泰坦陨落2找不到msvcp120.dll无法执行代码/msvcr120.dll丢失修复方法

msvcp120.dll 是 Windows 操作系统中的一个动态链接库文件&#xff0c;对于许多程序和游戏的运行起着至关重要的作用。然而&#xff0c;有时候我们可能会遇到 msvcp120.dll 丢失的情况&#xff0c;导致电脑出现各种问题。本文将详细介绍 msvcp120.dll 丢失的四种解决方法&#…

【项目】5.1阻塞和非阻塞、同步和异步 5.2Unix、Linux上的五种IO模型

5.1阻塞和非阻塞、同步和异步&#xff08;网络IO&#xff09; 典型的一次IO的两个阶段是什么&#xff1f;数据就绪和数据读写 数据就绪&#xff1a;根据IO操作的就绪状态 阻塞非阻塞 数据读写&#xff1a;根据应用程序和内核的交互方式 同步异步 陈硕&#xff1a;在处理IO的…

【小沐学Python】各种Web服务器汇总(Python、Node.js、PHP、httpd、Nginx)

文章目录 1、Web服务器2、Python2.1 简介2.2 安装2.3 使用2.3.1 http.server&#xff08;命令&#xff09;2.3.2 socketserver2.3.3 flask2.3.4 fastapi 3、NodeJS3.1 简介3.2 安装3.3 使用3.3.1 http-server&#xff08;命令&#xff09;3.3.2 http3.3.3 express 4、PHP4.1 简…

选择排序算法:简单但有效的排序方法

在计算机科学中&#xff0c;排序算法是基础且重要的主题之一。选择排序&#xff08;Selection Sort&#xff09;是其中一个简单但非常有用的排序算法。本文将详细介绍选择排序的原理和步骤&#xff0c;并提供Java语言的实现示例。 选择排序的原理 选择排序的核心思想是不断地从…

网络工程师怎么才算开窍

做网络工程师怎么样才算开窍&#xff1f;刚才有个朋友他说他希望从事网络工程师之后若干年。比如说当他到35岁的时候&#xff0c;他不希望出现跟今天现在是2023年&#xff0c;今年的这种裁员潮里面所遇到的那些主人公&#xff0c;就是技术学的也不错&#xff0c;然后工作也不错…

37 二叉树的最大深度

二叉树的最大深度 题解1 深度优先搜索&#xff08;递归弹栈&#xff09;题解2 广度优先搜索&#xff08;队列&#xff09; 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 提示&#xff1a; 树中节点…

阿里云PolarDB自研数据库详细介绍_兼容MySQL、PostgreSQL和Oracle语法

阿里云PolarDB数据库是阿里巴巴自研的关系型分布式云原生数据库&#xff0c;PolarDB兼容三种数据库引擎&#xff1a;MySQL、PostgreSQL、Oracle&#xff08;语法兼容&#xff09;&#xff0c;目前提供云原生数据库PolarDB MySQL版、云原生数据库PolarDB PostgreSQL版和云原生数…

使用 Python 的多项 Logistic 回归问题

一、说明 多项逻辑回归是一种统计方法&#xff0c;用于预测两个以上类别的分类结果。当因变量是分类变量而不是连续变量时&#xff0c;它特别有用。 二、分类预测 在多项式逻辑回归中&#xff0c;模型预测属于因变量每个类别的观测值的概率。这些概率可以解释为观察结果属于每…

聊聊并发编程——原子操作类和Fork/Join框架

目录 原子操作类 实现原子性原理 保证原子性的方法 Fork/Join框架 分而治之 工作窃取算法 Fork/Join框架的设计 示例 原子操作类 线程A和线程B同时更新变量i进行操作i1,最后的结果可能i不等于3而是等于2。这是线程不安全的更新操作&#xff0c;一般我们会使用Synchron…

CCF CSP认证 历年题目自练Day18

CCF CSP认证 历年题目自练Day18 题目一 试题编号&#xff1a; 201809-1 试题名称&#xff1a; 卖菜 时间限制&#xff1a; 1.0s 内存限制&#xff1a; 256.0MB 问题描述&#xff1a; 问题描述   在一条街上有n个卖菜的商店&#xff0c;按1至n的顺序排成一排&#xff0c;这…

如何保持终身学习

文章目录 2.1. 了解你的大脑2.2 学习是对神经元网络的塑造2.3 大脑的一生 3.学习的心里基础3.1 固定思维与成长思维3.2 我们为什么要学习 4. 学习路径4.1 构建知识模块4.2 大脑是如何使用注意力的4.3 提高专注力4.4 放松一下&#xff0c;学的更好4.5 巩固你的学习痕迹4.6 被动学…