IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> RoaringBitmap数据结构以及精确去重UDAF实现 -> 正文阅读

[数据结构与算法]RoaringBitmap数据结构以及精确去重UDAF实现

一、位图(Bitmap)

?1、什么是比特(bit)
? ? ? 1)它是英文 binary digit 的缩写
? ? ? 2)它是计算机内部存储的最小单位,用二进制的0或者1来表示?
? ? ? 3)1 Byte = 8 bit;1024 Byte = 1 Kb;1024 Kb = 1 Mb;1024 Mb = 1 Gb;1024 Gb = 1 Tb

2、引子

给出40亿个连续不重复且无序的无符号int型整数,目前条件是只有一个2G内存的PC,需要判断出某个数字是否在给出的这40亿个数字里面

分析:int占4个Byte,40亿 * 4 / 1024 / 1024 / 1024 ≈ 14.9 Gb,目前内存只有2G根本不满足要求(需要注意的是 int 无符号最大值是 4294967295,二进制的最高位为符号位),此时就需要位图(Bitmap)来处理了


? ?1)什么是位图(Bitmap)
? ? ? ? a)位图(Bitmap)就是用一个bit位表示数字。从0开始,第N位的bit位表示整数N。bit位为1表示该整数存在,bit位为0表示整数不存在


? ? ? ? b)位图本质上就是一个数组
? ? ? ? c)位图采用的空间换时间的方式来提高计算的效率
? ?2)通过位图(Bitmap)解决后:40亿个数字如果我们用40亿个bit来表示,则需要占据的空间为 40亿 / 8 / 1024 / 1024 ≈ 476.83 Mb,大大降低了内存的消耗?

3、缺点:以上是在数据连续的情况下占用了476.83Mb,假如现在只存第40亿一个数那仍然会占476.83Mb的内存。也就是说在数据密集的时候使用位图是很划算的,如果数据稀疏那就不划算了


二、压缩位图(RoaringBitmap)

1、实现原理
? ? ?1)压缩位图(RoaringBitmap,以下简称RBM)处理的是无符号int类型的整数
? ? ?2)RBM将一个32位的int拆分为高16位与低16位分开去处理,其中高16位作为索引,低16位作为实际存储数据


2、数据结构
? ? ?1)RoaringBitmap

RoaringArray highLowContainer = null;
/**
 * Create an empty bitmap
 */
public RoaringBitmap() {
  highLowContainer = new RoaringArray();
}


? ? ?2)RoaringArray

static final int INITIAL_CAPACITY = 4;
// short占2个字节,16位,一个short正好可以表示int高16位的所有数值
short[] keys = null;
// Container用来存储int低16位的2^16个int类型的整数
Container[] values = null;
protected RoaringArray() {
  this.keys = new short[INITIAL_CAPACITY];
  this.values = new Container[INITIAL_CAPACITY];
}


? ? ?3)Container

? ? ? ? a)ArrayContainer

// ArrayContainer中允许的最大数据量
// 4096 * 2Byte / 1024 = 8k 也就是说ArrayContainer最大容量时所占的内存为8k
static final int DEFAULT_MAX_SIZE = 4096;// containers with DEFAULT_MAX_SIZE or less integers should be ArrayContainers
// 基数(元素个数)
protected int cardinality = 0;
// 用来存储int类型低16位的整数,也就是说ArrayContainer中存储的数字来自0~65535(2^16-1),且只能存这个范围内的4096个数
short[] content;

? ? ? ? b)BitmapContainer

// 最大可以存储2^16个比特位(每个bit对应一个数值, 最大可以表示2^16个int类型的整数)
protected static final int MAX_CAPACITY = 1 << 16;
long[] bitmap;
int cardinality;
public BitmapContainer() {
  this.cardinality = 0;
  // long占8Byte(64bit)
  // 2^16bit / 64bit = 1024 也就是说需要1024个long, 所以此处new一个长度为1024长度的long数组
  // 2^16bit / 8 / 1024 = 8Kb (1024个long * 8Byte / 1024 = 8Kb), 所以BitmapContainer始终占据内存空间为8Kb
  this.bitmap = new long[MAX_CAPACITY / 64];
}
ArrayContainer 与 BitmapContainer 随着存储的数据量增多时所占内存空间对比图

? ? ? ? c)RunContainer

private short[] valueslength;// we interleave values and lengths, so 
// that if you have the values 11,12,13,14,15, you store that as 11,4 where 4 means that beyond 11 itself, there are
// 4 contiguous values that follows.
// Other example: e.g., 1, 10, 20,0, 31,2 would be a concise representation of  1, 2, ..., 11, 20, 31, 32, 33
int nbrruns = 0;// how many runs, this number should fit in 16 bits.
private RunContainer(int nbrruns, short[] valueslength) {
  this.nbrruns = nbrruns;
  this.valueslength = Arrays.copyOf(valueslength, valueslength.length);
}

有关RunContainer的注意事项:

在RBM创立初期只有以上两种容器,RunContainer其实是在后期加入的。RunContainer是基于之前提到的RLE算法进行压缩的,主要解决了大量连续数据的问题。
举例说明:3,4,5,10,20,21,22,23这样一组数据会被优化成3,2,10,0,20,3,原理很简单,就是记录初始数字以及连续的数量,并把压缩后的数据记录在short数组中
显而易见,这种压缩方式对于数据的疏密程度非常敏感,举两个最极端的例子:如果这个Container中所有数据都是连续的,也就是[0,1,2.....65535],压缩后为0,65535,即2个short,4字节。若这个Container中所有数据都是间断的(都是偶数或奇数),也就是[0,2,4,6....65532,65534],压缩后为0,0,2,0.....65534,0,这不仅没有压缩反而膨胀了一倍,65536个short,即128kb
因此是否选择RunContainer是需要判断的,RBM提供了一个转化方法runOptimize()用于对比和其他两种Container的空间大小,若占据优势则会进行转化

3、数据存储使用示例

?????1)代码

RoaringBitmap rbm = new RoaringBitmap();
// 从 0 到 2^16-1, 这个范围内的高位索引为0, 此处取了 5 个数
rbm.add(0);
rbm.add(1);
rbm.add(10);
rbm.add(10000);
rbm.add(65335);
// 从 2^16 到 2^17-1, 这个范围内的高位索引是1, 此处取了 2^15 个偶数
for (int i = 65536; i < 65536 * 2; i+=2) {
  rbm.add(i);
}
// 其实就是从 2^17+2^16 到 2^17+2^17-1 这个范围内的高位索引为3, 此处取了 2^16 个数
for (int i = 3 * 65536; i < 4 * 65536; i++) {
  rbm.add(i);
}
// 用来优化BitmapContainer, 优化为RunContainer
rbm.runOptimize();

? ? ? 2)分析

? ? ? ? ? ?a)优化前

? ? ? ? ? ?b)优化后

? ? ? ? ? ?c)注释
????????????????第一组由于数据最大为 2^16-1 所以最高位的索引为0,且个数没有超过4096,所以直接存到ArrayContainer中
????????????????第二组数据的范围是 2^16 到 2^17-1 所以最高位索引为1,此时需要用BitmapContainer来存储低16位的数字
????????????????第三组如果没有优化的话是BitmapContainer存储从 0 ~ 65535 的所有数据,如果优化以后则会用RunContainer存储,且只会存一个开始值0,还有一个步长65535,中间所有的值连续
需要注意的是第二组在调用优化方法以后并没有被优化成RunContainer
4、常用API

// and取交集
RoaringBitmap roaringBitmapAnd1 = RoaringBitmap.bitmapOf(1, 2, 3);
RoaringBitmap roaringBitmapAnd2 = RoaringBitmap.bitmapOf(3, 6, 4);
RoaringBitmap and = RoaringBitmap.and(roaringBitmapAnd1, roaringBitmapAnd2);
print(and, "and取交集后bitmap的值: ");
System.out.println("统计基数: " + and.getCardinality());
System.out.println("判断bitmap是否为空: " + and.isEmpty());
System.out.println("判断1与2是否相等: " + roaringBitmapAnd1.equals(roaringBitmapAnd2));
System.out.println("判断1与2是否相交: " + RoaringBitmap.intersects(roaringBitmapAnd1, roaringBitmapAnd2));
System.out.println("AND计算并返回基数: " + RoaringBitmap.andCardinality(roaringBitmapAnd1, roaringBitmapAnd2));
roaringBitmapAnd1.remove(2);// 这里是数值, 不是索引
print(roaringBitmapAnd1, "1中移除数字2: ");
//        roaringBitmapAnd2.flip(4L, 5L);// ??????
//        print(roaringBitmapAnd2, "2中翻转数字3: ");
System.out.println("bitmap1小于等于3的整数数目: " + roaringBitmapAnd1.rank(3));
System.out.println("bitmap2中是否包含10: " + roaringBitmapAnd2.contains(10));
//        System.out.println("bitmap1中是否有值在给出的范围: " + roaringBitmapAnd1.contains(2, 3));// ???
System.out.println("bitmap1中是否包含bitmapand: " + roaringBitmapAnd1.contains(and));
roaringBitmapAnd2.add(10);
print(roaringBitmapAnd2, "bitmap2中添加元素10: ");
System.out.println("bitmap1中添加元素6到8: " + RoaringBitmap.add(roaringBitmapAnd1, 6L, 11L));
System.out.println("添加完元素以后bitmap1的值: " + roaringBitmapAnd1);
System.out.println("======================");
?

// or取并集
RoaringBitmap roaringBitmapOr1 = RoaringBitmap.bitmapOf(1, 2, 3);
RoaringBitmap roaringBitmapOr2 = RoaringBitmap.bitmapOf(3, 4, 5);
RoaringBitmap or = RoaringBitmap.or(roaringBitmapOr1, roaringBitmapOr2);
print(or, "or取并集后bitmap的值: ");
System.out.println("统计基数: " + or.getCardinality());
System.out.println("判断bitmap是否为空: " + or.isEmpty());
System.out.println("判断1与2是否相等: " + roaringBitmapOr1.equals(roaringBitmapOr2));
System.out.println("判断1与2是否相交: " + RoaringBitmap.intersects(roaringBitmapOr1, roaringBitmapOr2));
System.out.println("or计算并返回基数: " + RoaringBitmap.orCardinality(roaringBitmapOr1, roaringBitmapOr2));
System.out.println("======================");


// xor取异或: 相同的都是0, 不同的为1
RoaringBitmap roaringBitmapXor1 = RoaringBitmap.bitmapOf(1, 2, 3);
RoaringBitmap roaringBitmapXor2 = RoaringBitmap.bitmapOf(3, 2, 5);
RoaringBitmap xor = RoaringBitmap.xor(roaringBitmapXor1, roaringBitmapXor2);
print(xor, "xor异或后取bitmap的值: ");
System.out.println("统计基数: " + xor.getCardinality());
System.out.println("判断bitmap是否为空: " + xor.isEmpty());
System.out.println("判断1与2是否相等: " + roaringBitmapXor1.equals(roaringBitmapXor2));
System.out.println("判断1与2是否相交: " + RoaringBitmap.intersects(roaringBitmapXor1, roaringBitmapXor2));
System.out.println("xor计算并返回基数: " + RoaringBitmap.xorCardinality(roaringBitmapXor1, roaringBitmapXor2));
System.out.println("======================");


// andNot取差集
RoaringBitmap roaringBitmapAndNot1 = RoaringBitmap.bitmapOf(1, 2, 3);
RoaringBitmap roaringBitmapAndNot2 = RoaringBitmap.bitmapOf(3, 4, 5);
RoaringBitmap andNot1 = RoaringBitmap.andNot(roaringBitmapAndNot1, roaringBitmapAndNot2);
RoaringBitmap andNot2 = RoaringBitmap.andNot(roaringBitmapAndNot2, roaringBitmapAndNot1);
print(andNot1, "andNot计算bitmap1与bitmap2的差集: ");
System.out.println("统计基数: " + andNot1.getCardinality());
System.out.println("判断bitmap是否为空: " + andNot1.isEmpty());
System.out.println("判断1与2是否相等: " + roaringBitmapAndNot1.equals(roaringBitmapAndNot2));
System.out.println("判断1与2是否相交: " + RoaringBitmap.intersects(roaringBitmapAndNot1, roaringBitmapAndNot2));
System.out.println("andNot1计算并返回基数: " + RoaringBitmap.andNotCardinality(roaringBitmapAndNot1, roaringBitmapAndNot2));
System.out.println("---");
print(andNot2, "计算bitmap2与bitmap1的差集: ");
System.out.println("统计基数: " + andNot2.getCardinality());
System.out.println("判断bitmap是否为空: " + andNot2.isEmpty());
System.out.println("判断1与2是否相等: " + roaringBitmapAndNot2.equals(roaringBitmapAndNot1));
System.out.println("判断1与2是否相交: " + RoaringBitmap.intersects(roaringBitmapAndNot2, roaringBitmapAndNot1));
System.out.println("andNot2计算并返回基数: " + RoaringBitmap.andNotCardinality(roaringBitmapAndNot2, roaringBitmapAndNot1));
System.out.println("======================");


private static void print(RoaringBitmap roaringBitmap, String message) {
  System.out.print(message);
  roaringBitmap.forEach((Consumer<? super Integer>)  i -> System.out.print(i + " "));
  System.out.println();
}


## result
and取交集后bitmap的值: 3 
统计基数: 1
判断bitmap是否为空: false
判断1与2是否相等: false
判断1与2是否相交: true
AND计算并返回基数: 1
1中移除数字2: 1 3 
bitmap1小于等于3的整数数目: 2
bitmap2中是否包含10: false
bitmap1中是否包含bitmapand: true
bitmap2中添加元素10: 3 4 6 10 
bitmap1中添加元素6到8: {1,3,6,7,8,9,10}
添加完元素以后bitmap1的值: {1,3}
======================
or取并集后bitmap的值: 1 2 3 4 5 
统计基数: 5
判断bitmap是否为空: false
判断1与2是否相等: false
判断1与2是否相交: true
or计算并返回基数: 5
======================
xor异或后取bitmap的值: 1 5 
统计基数: 2
判断bitmap是否为空: false
判断1与2是否相等: false
判断1与2是否相交: true
xor计算并返回基数: 2
======================
andNot计算bitmap1与bitmap2的差集: 1 2 
统计基数: 2
判断bitmap是否为空: false
判断1与2是否相等: false
判断1与2是否相交: true
andNot1计算并返回基数: 2
---
计算bitmap2与bitmap1的差集: 4 5 
统计基数: 2
判断bitmap是否为空: false
判断1与2是否相等: false
判断1与2是否相交: true
andNot2计算并返回基数: 2


三、压缩位图精确去重UDAF实现

1、构造表以及数据
? ?1)构造的表名称:mart_grocery_crm.bitmap_count_distinct_test_spark
? ?2)mart_grocery_crm.bitmap_count_distinct_test_spark 表中的数据(user_id为int类型)


? ?3)通过SQL查看分组去重后的结果?

SELECT department, count(distinct user_id) FROM mart_grocery_crm.bitmap_count_distinct_test_spark GROUP BY department
--根据部门分组去重后的结果
————————————————————————————————————————————————
|  department  |  `count`(DISTINCT `user_id`)  |
|——————————————————————————————————————————————|
|    waimai    |              2                |
|——————————————————————————————————————————————|
|   xiaoxiang  |              3                |
|——————————————————————————————————————————————|
|    maicai    |              4                |
|——————————————————————————————————————————————|   

2、编写UDAF

? ? ?1)继承 AbstractGenericUDAFResolver 抽象类,重写 getEvaluator 方法

/**
 * @author zhaocesheng
 * @since 2021/08/04
 * 通过RoaringBitmap实现CountDistinct测试类
 */
public class RBMCountTestUDAF extends AbstractGenericUDAFResolver {
?
    /**
     * @param info UDAF方法入参
     * @return 该方法可以实现不同的入参走不同的实现类里面的实现逻辑
     * @throws SemanticException 可能会抛出语义异常错误
     */
    @Override
    public GenericUDAFEvaluator getEvaluator(TypeInfo[] info) throws SemanticException {
        // 校验长度
        if (info.length > 1) {
            throw new UDFArgumentTypeException(info.length - 1, "Exactly one argument is expected.");
        }
        // 校验类型
        switch (((PrimitiveTypeInfo) info[0]).getPrimitiveCategory()) {
            case INT:
                break;
            case BYTE:
            case SHORT:
            case LONG:
            case FLOAT:
            case DOUBLE:
            case TIMESTAMP:
            case DECIMAL:
            case STRING:
            case BOOLEAN:
            case DATE:
            default:
                throw new UDFArgumentTypeException(0,"Only numeric type arguments are accepted but " 
                        + info[0].getTypeName() + " was passed as parameter 1.");
        }
        // 只有一个实现逻辑就是Count Distinct
        return new RoaringBitmapCountEvaluator();
    }
getEvaluator 方法的目的感觉有两个:
一个是校验函数入参的个数以及入参的类型
一个是根据不同的入参判断走哪个子类,不同的子类对应着不同的实现逻辑(本例中的子类只有一个,也就是实现逻辑只有一个)

? ? ?2)实现类以及init方法

public static class RoaringBitmapCountEvaluator extends GenericUDAFEvaluator {
?
    PrimitiveObjectInspector inputOI;
?
    @Override
    public ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException {
        assert (parameters.length == 1);
        super.init(m, parameters);
        inputOI = (PrimitiveObjectInspector) parameters[0];
        // map和combine阶段返回RoaringBitmap的二进制数组
        if (m == Mode.PARTIAL1 || m == Mode.PARTIAL2) {
            return PrimitiveObjectInspectorFactory.javaByteArrayObjectInspector;
        }
        // 只有map和reduce的情况返回一个Count Distinct之后的数值
        return PrimitiveObjectInspectorFactory.javaIntObjectInspector;
    }
1  实现类需要继承GenericUDAFEvaluator
2  Hive的执行过程其实是mapreduce的过程,可以分为四种情况
   1)多个节点的map阶段收集数据 (对应着Mode.PARTIAL1)
   2)combine阶段部分聚合每个节点中map阶段的数据(对应着Mode.PARTIAL2)
   3)reduce阶段合并各个节点的数据(对应着Mode.FINAL)
   4)有些情况map阶段之后直接输出结果(对应着Mode.COMPLETE)
在本例中map阶段和combine阶段输出的结果为RoaringBitmap的字节数组byte[],只有map阶段以及reduce阶段返回最终结果(int类型)
3  PrimitiveObjectInspector是全局输入输出数据类型的OI实例,用于解析输入输出数据(后续的方法中会用到)

? ? 3)构建中间结果缓存Buffer

/**
 * 构建自己的缓冲Buffer
 */
static class RoaringBitmapAgg implements AggregationBuffer {
    RoaringBitmap rbm;
?
    public byte[] serializeToByte() {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);
        try {
            assert (rbm != null);
            rbm.serialize(dos);
            dos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bos.toByteArray();
    }
?
    public RoaringBitmap deSerializeFromByte(byte[] bytes) {
        RoaringBitmap rbm = new RoaringBitmap();
        DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bytes));
        try {
            rbm.deserialize(dis);
            dis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return rbm;
    }
?
}
?
@Override
public AggregationBuffer getNewAggregationBuffer() throws HiveException {
    RoaringBitmapAgg rbmBuffer = new RoaringBitmapAgg();
    if (rbmBuffer.rbm == null) {
        rbmBuffer.rbm = new RoaringBitmap();
    } else {
        reset(rbmBuffer);
    }
    return rbmBuffer;
}
?
@Override
public void reset(AggregationBuffer agg) throws HiveException {
    RoaringBitmapAgg rbmBuffer = (RoaringBitmapAgg) agg;
    if (rbmBuffer != null && rbmBuffer.rbm != null) {
        rbmBuffer.rbm.clear();
    }
}

1  RoaringBitmapAgg 类的目的是为了缓存 RoaringBitmap 聚集或者取并集后的部分结果
2  RoaringBitmapAgg 中的 serializeToByte 方法目的是为了将 RoaringBitmap 序列化成二进制流,在 map 或者 combine 阶段作为输出的结果使用
3  RoaringBitmapAgg 中的 deSerializeFromByte 方法目的是为了将二进制流反序列化成 RoaringBitmap,在 combine 或者 reduce 阶段将入参反序列化成 RoaringBitmap 后做合并计算时使用
4  getNewAggregationBuffer 方法在map阶段执行一次,目的是获取中间结果缓存对象
5  reset 方法mapreduce支持mapper和reducer的重用,所以为了兼容,也需要做内存的重用(不是很明白???)

? ? ? 4)map阶段的iterate方法

/**
 * map阶段各个节点将数据写入RoaringBitmap, 需要注意的是需要把参数转换成int类型
 *
 * @param agg        buffer
 * @param parameters 列值
 * @throws HiveException UDF异常
 */
@Override
public void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException {
    assert (parameters.length == 1);
    RoaringBitmapAgg rbmBuffer = (RoaringBitmapAgg) agg;
    if (rbmBuffer != null && rbmBuffer.rbm != null && parameters[0] != null) {
        rbmBuffer.rbm.add(PrimitiveObjectInspectorUtils.getInt(parameters[0], inputOI));
    }
}
1  该方法只会发生在 map 阶段

2  该方法的目的是为了聚集map阶段所在节点的有效数据

3  此处需要通过 PrimitiveObjectInspector 以及入参得到int类型的列值,然后将列值写入 RoaringBitmap

? ? ? 5)terminatePartial方法

/**
 * map和combine阶段返回部分聚集结果
 *
 * @param agg buffer
 * @return byte[]
 * @throws HiveException UDF异常
 */
@Override
public Object terminatePartial(AggregationBuffer agg) throws HiveException {
    RoaringBitmapAgg rbmBuffer = (RoaringBitmapAgg) agg;
    if (rbmBuffer != null) {
        return rbmBuffer.serializeToByte();
    }
    return new Byte[0];
}
map 阶段或者 combine 阶段结束以后将结果序列化

? ? ?6)merge方法

/**
 * combine和reducer阶段聚合数据
 *
 * @param agg     buffer
 * @param partial 部分聚集数据
 * @throws HiveException UDF异常
 */
@Override
public void merge(AggregationBuffer agg, Object partial) throws HiveException {
    RoaringBitmapAgg rbmBuffer = (RoaringBitmapAgg) agg;
    if (rbmBuffer != null && rbmBuffer.rbm != null) {
        rbmBuffer.rbm.or(rbmBuffer.deSerializeFromByte((byte[]) partial));
    }
}
combine 阶段或者 reduce 阶段将结果聚合在一起,本例中是将各个节点的 Roaringbitmap 取并集

? ? ? 7)terminate方法

/**
 * 得到reduce后的最终结果
 *
 * @param agg buffer
 * @return 取并集后的最终结果
 * @throws HiveException UDF异常
 */
@Override
public Object terminate(AggregationBuffer agg) throws HiveException {
    RoaringBitmapAgg rbmBuffer = (RoaringBitmapAgg) agg;
    if (rbmBuffer.rbm != null) {
        return rbmBuffer.rbm.getCardinality();
    }
    return -1;
}
输出 reduce 后的最终结果,该结果的类型与init方法中的定义的返回类型前后呼应

3、UDAF的工作流程总览
? ? 1)方法执行流程

?
? ? 2)MR中数据流转流程

? ??? ?

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-05-08 08:20:57  更:2022-05-08 08:25:03 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 3:42:42-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码