TimeWheel时间轮算法原理及实现(附源码)

news2024/12/22 9:22:11

时间轮算法原理及实现

    • 前言
    • 1.时间轮核心
    • 2.简单定时器
    • 3.任务队列
    • 4.优化任务队列
    • 5.简单时间轮
    • 6.多层时间轮

前言

 在各种业务场景中,我们总是会需要一些定时进行一些操作,这些操作可能是需要在指定的某个时间点操作,也可能是每过一个固定的时间间隔后进行操作,这就要求我们需要有一个定时调度的逻辑,同时,这种定时操作,既有可能在某一刻数量比较密集,也有可能时间间隔比较密集,这就要考虑定时调度器对性能的影响.

如果在业务逻辑中,存在数量较大的定时任务,且每个定时任务都创建一个只属于自己的的调度管理器负责自身的生命周期及周期任务执行, 这就极大的浪费cpu的资源,降低自身性能.时间轮算法是一种调度模型,可以有效地利线程资源来处理批量周期任务,时间轮调度模型将数量巨大的定时任务绑定在单个调度器上,并统一使用这个调度器来管理,触发以及执行任务.这种模型使得大量延时任务,周期任务以及通知任务的管理变得高效.

1.时间轮核心

 时间轮算法的核心是,轮询线程不再是负责遍历所有任务,而只在负责处于其当前时间上的任务.就像钟表一样,时间轮有一个指针会一直旋转,每转到一个新的时间,就去执行挂在这一时刻下的所有任务,当然,这些任务通常是交给其他线程去做,指针要做的只是继续往下转,不会关心任务的执行进度.

 下面,我们就从一个最简单的定时任务来一步步优化,看看时间轮到底是怎么设计出来的

2.简单定时器

 这种方式最简单,如果想定期执行一个操作,只需要起一个定时器,设置时间间隔,设置回调函数,让它跑就完了.在定时任务非常少的情况下,这种方式没什么问题.如果定时任务的数目很大,并且都有不同的周期,那就产生了非常多的定时器, 这对系统的内存cpu都产生了很大的压力,程序还没开始跑呢,定时器已经满天飞了…

3.任务队列

 为了不产生过多的定时器,我们只使用一个定时器,将所有的定时任务放到一个队列中,每个定时任务都保存一份自己的定时信息,定时器每隔一个周期轮询一遍队列中的所有任务,如果任务的超时时间已到,则执行该任务,如果超时时间还没到,则将该任务的定时信息减掉一个定时器的时间间隔,等到完全减为0时,该任务就可以被执行了, 这个定时器一直这么执行并轮询下去.假设当前定时任务总数有100个,那定时器每个周期会遍历一个100个元素的队列,听上去还可以,那要有1000个的时候,10000个时候,这定时器就太可怜了,像一头老牛😄

4.优化任务队列

 为了解决任务队列中任务太多,一个定时器压力太大的问题,我们继续对其进行优化,既然所有的定时任务都放在一个队列下不太行,那就对定时任务进行分类,将定时周期相同的任务放在同一个定时器下,这样每个定时器的压力就会大大减小,每个定时器只负责自己周期内的任务,不负责其他周期的任务.但是如果每个任务的周期都相同,还是要产生很多定时器,似乎还是无法从根本上解决问题…

5.简单时间轮

 这时候时间轮的优势就体现出来了,我们设置一个环状的时间队列,队列上的每一个元素,我们称为一个槽(slot),这个槽所对应的时间在整个时间轮里是唯一的, 根据前面的分析也能知道,槽里面是放任务的,而且肯定不会放一个任务,而是放一个任务队列.

 对这个环状队列,我们维护一个指针,指针指向的槽,就是已经到达超时时间的槽,槽里的任务就要被执行.任务在被插入时间轮的时候,就根据当前时间以及自己的时间周期,确定好了自己会处于时间轮中的哪个槽.等到时间轮指针指到这个槽,任务就被触发.

 这样,时间轮只需要定时的轮询轮上的槽,如果有任务就交给任务处理线程去做,没有就继续轮询,即使总任务有一万个,调度器还是只会轮询这十个槽,而不会去轮询一遍十万个任务.

 这就是单层时间轮,这个时间轮的所能处理的最大周期是有限的,比如,一个具有10个slot的时间轮,wheel size = 10,每两个槽之间的间隔为1s,这个间隔称为tick,即最小的时间间隔,那么这个时间轮的跨度就是10*1 = 10s,也就是所支持能设置的最大周期为10s,如果一个任务每隔11秒执行一次.

 同时,10s这个周期太短了,现在各种系统中不乏周期为成百上千秒的定时任务,且以1s为分割,颗粒太大了,秒级以下的定时任务无法被触发.

 如果仅仅是个时间跨度为10s切以秒为tick的时间轮,是基本满足不了大部分场景的,为了满足需求,最简单快速的方法就是加大时间轮跨度来提升周期,降低tick来提高精度,如果时间跨度提升为60s, tick改成10ms,就需要6000个槽来安插任务,这样就可以设置周期更长的任务,可以根据更精细的时间单位(10ms)来执行定时任务

 需求满足了,但同时又带来了一些问题.

  • 轮询线程遍历效率低下问题:当timescale数量增加,task数量较少时,轮询线程遍历效率下降,比如只有50槽上有task,但是却需要遍历6000个timescale。这违背了我们时间轮算法的初衷:解决遍历轮询线程遍历效率低下的问题
  • 内存空间浪费问题:时间尺度密集,任务数量少,大部分时间尺度占用的内存空间没有意义。

 如果将时间轮跨度设置为1小时,那么整个时间轮需要60x60x1000/100 = 36000个单位的时间刻度,此时时间轮算法的遍历线程会遇到更大的运行效率低下。

 因此简单(单层)时间轮的性能上限很低,一旦精度和时间跨度要求上来,就无法达到期望的目标了.

6.多层时间轮

 在上面的场景下,多层时间轮就诞生了,就像我们生活中见过的水表一样,有非常多的小表盘

 多层时间轮的概念也非常清晰,将时间轮分为多个,每两个轮之间是进位的关系,例如最普遍的秒,分,时.

即:

  • 秒级时间轮上,设有60个槽, 每两个槽之间的时间为1s.

  • 分钟级时间轮上,设有60s个槽,每两个槽之间的时间为1min

  • 小时级时间轮上,设有24个槽,每两个槽之间的时间为1h.

 这样,秒级每走60个槽,时间过去一分钟,秒级时间轮归零,分级时间轮走一个槽; 分级时间轮每走过60个槽,时间过去一小时,分级时间轮归零,小时级时间轮走一个槽.

 通过三级时间轮,只需要遍历60+60+60 =180个槽,就可以成为一个精度为1s, 周期为60x60x60 = 216000s的定时调度器.

 多级时间轮的思想在很多开发中间件中都被应用,例如NettyAkkaQuartzZooKeeperKafka等等.

 作为学习Linux上C++开发的必备书籍,《Linux高性能服务器编程》以及《Linux多线程服务端编程:使用muduo C++网络库》两本书中,都介绍到了时间轮定时器,其中第二本书中,作者陈硕详细介绍了如果将时间轮应用在经典项目:muduo C++网络库中,非常值得参考学习!

 参照《Linux高性能服务器编程》的第11章:11.4高性能定时器,

 以及《Linux多线程服务端编程:使用muduo C++网络库》第7章:7.10 用timing wheel 踢掉空闲连接,

 简单实现了下一个三级定时器,下面是源码,并且在关键的地方进行了注释.

TimWheel.h

#include <memory>
#include <list>
#include <vector>
#include <mutex>

typedef struct TimePos{
    int pos_ms;
    int pos_sec;
    int pos_min;
}TimePos_t;

typedef struct Event {
    int id;
    void(*cb)(void);
    void* arg;
    TimePos_t timePos;
    int interval;
}Event_t;


class TimeWheel {
    typedef std::shared_ptr<TimeWheel> TimeWheelPtr;
    typedef void (*EventCallback_t)(void );
    typedef std::vector<std::list<Event_t>> EventSlotList_t;
public:
    TimeWheel();
    
    void initTimeWheel(int steps, int maxMin);
    void createTimingEvent(int interval, EventCallback_t callback);

public:
    static void* loopForInterval(void* arg);
    
private:
    int getCurrentMs(TimePos_t timePos);
    int createEventId();
    int processEvent(std::list<Event_t> &eventList);
    void getTriggerTimeFromInterval(int interval, TimePos_t &timePos);
    void insertEventToSlot(int interval, Event_t& event);

    EventSlotList_t m_eventSlotList;
    TimePos_t m_timePos;
    pthread_t m_loopThread;

    int m_firstLevelCount;
    int m_secondLevelCount;
    int m_thirdLevelCount; 
    
    int m_steps;
    int m_increaseId;  // not used
    std::mutex m_mutex;
};

TimeWheel.cpp

#include "TimeWheel.h"

#include <iostream>
#include <memory.h>
#include <chrono>
#include <thread>

TimeWheel::TimeWheel() : m_steps(0), m_firstLevelCount(0), m_secondLevelCount(60), m_thirdLevelCount(0),
                         m_increaseId (0){
                            memset(&m_timePos, 0, sizeof(m_timePos));
                         }

void* TimeWheel::loopForInterval(void* arg)
{
    if(arg == NULL) {
        printf("valid parameter\n");
        return NULL;
    }
    TimeWheel* timeWheel = reinterpret_cast<TimeWheel*>(arg);
    while(1) {
       std::this_thread::sleep_for(std::chrono::milliseconds(timeWheel->m_steps));
        // printf("wake up\n");
        TimePos pos = {0};
        TimePos m_lastTimePos = timeWheel->m_timePos;
        //update slot of current TimeWheel
        timeWheel->getTriggerTimeFromInterval(timeWheel->m_steps, pos);
        timeWheel->m_timePos = pos;
        {
            std::unique_lock<std::mutex> lock(timeWheel->m_mutex);
            // if minute changed, process in integral point (minute)
            if (pos.pos_min != m_lastTimePos.pos_min)
            {
                // printf("minutes changed\n");
                std::list<Event_t>* eventList = &timeWheel->m_eventSlotList[timeWheel->m_timePos.pos_min + timeWheel->m_firstLevelCount + timeWheel->m_secondLevelCount];
                timeWheel->processEvent(*eventList);
                eventList->clear();
           }
            else if (pos.pos_sec != m_lastTimePos.pos_sec)
            {
                //in same minute, but second changed, now is in this integral second
                // printf("second changed\n");
                std::list<Event_t>* eventList = &timeWheel->m_eventSlotList[timeWheel->m_timePos.pos_sec + timeWheel->m_firstLevelCount];
                timeWheel->processEvent(*eventList);
                eventList->clear();
            }
            else if (pos.pos_ms != m_lastTimePos.pos_ms)
            {
                //now in this ms
                // printf("ms changed\n");
                std::list<Event_t>* eventList = &timeWheel->m_eventSlotList[timeWheel->m_timePos.pos_ms];
                timeWheel->processEvent(*eventList);
                eventList->clear();
            }
            // printf("loop over\n");
        }
     
    }

    return nullptr;
}

//init TimeWheel's step and maxmin, which detemine the max period of this wheel
void TimeWheel::initTimeWheel(int steps, int maxMin)
{
    if (1000 % steps != 0){
        printf("invalid steps\n");
        return;
    }
    m_steps = steps;
    m_firstLevelCount = 1000/steps;
    m_thirdLevelCount = maxMin;

    m_eventSlotList.resize(m_firstLevelCount + m_secondLevelCount + m_thirdLevelCount);
    int ret = pthread_create(&m_loopThread, NULL, loopForInterval, this);
    if(ret != 0) {
        printf("create thread error:%s\n", strerror(errno));
        return;
    }
    // pthread_join(m_loopThread, NULL);
}

void TimeWheel::createTimingEvent(int interval, EventCallback_t callback){
    if(interval < m_steps || interval % m_steps != 0 || interval >= m_steps*m_firstLevelCount*m_secondLevelCount*m_thirdLevelCount){
        printf("invalid interval\n");
        return;
    }
    printf("start create event\n");
    Event_t event = {0};
    event.interval = interval;
    event.cb = callback;
    //set time start
    event.timePos.pos_min = m_timePos.pos_min;
    event.timePos.pos_sec = m_timePos.pos_sec;
    event.timePos.pos_ms = m_timePos.pos_ms;
    event.id = createEventId();
    // insert it to a slot of TimeWheel
     std::unique_lock<std::mutex> lock(m_mutex);
    insertEventToSlot(interval, event);
    printf("create over\n");
}


int TimeWheel::createEventId() {
    return m_increaseId++;  
}


void TimeWheel::getTriggerTimeFromInterval(int interval, TimePos_t &timePos) {
    //get current time: ms
    int curTime = getCurrentMs(m_timePos);
    // printf("interval = %d,current ms = %d\n", interval, curTime);

    //caculate which slot this interval should belong to 
    int futureTime = curTime + interval;
    // printf("future ms = %d\n", futureTime);
    timePos.pos_min =  (futureTime/1000/60)%m_thirdLevelCount;
    timePos.pos_sec =  (futureTime%(1000*60))/1000;
    timePos.pos_ms = (futureTime%1000)/m_steps;

    // printf("next minPos=%d, secPos=%d, msPos=%d\n", timePos.pos_min, timePos.pos_sec, timePos.pos_ms);
    return;
}

int TimeWheel::getCurrentMs(TimePos_t timePos) {
    return m_steps * timePos.pos_ms + timePos.pos_sec*1000 +  timePos.pos_min*60*1000;
}

int TimeWheel::processEvent(std::list<Event_t> &eventList){
    // printf("eventList.size=%d\n", eventList.size());

    //process the event for current slot
    for(auto event = eventList.begin(); event != eventList.end(); event ++) {
        //caculate the current ms
        int currentMs = getCurrentMs(m_timePos);
        //caculate last  time(ms) this event was processed
        int lastProcessedMs = getCurrentMs(event->timePos);
        //caculate the distance between now and last time(ms)
        int distanceMs = (currentMs - lastProcessedMs + (m_secondLevelCount+1)*60*1000)%((m_secondLevelCount+1)*60*1000);

        //if interval == distanceMs, need process this event
        if (event->interval == distanceMs)
        {
            //process event
            event->cb();
            //get now pos as this event's start point
            event->timePos = m_timePos;
            //add this event to slot
            insertEventToSlot(event->interval, *event);
        }
        else
        {
            //this condition will be trigger when process the integral point 
            printf("event->interval != distanceMs\n");
            // although this event in this positon, but it not arriving timing, it will continue move to next slot caculate by distance ms.
            insertEventToSlot(distanceMs, *event);
        }
    }
    return 0;
}

void TimeWheel::insertEventToSlot(int interval, Event_t& event)
{
    printf("insertEventToSlot\n");

    TimePos_t timePos = {0};

    //caculate the which slot this event should be set to
    getTriggerTimeFromInterval(interval, timePos);
     {
        // printf("timePos.pos_min=%d, m_timePos.pos_min=%d\n", timePos.pos_min, m_timePos.pos_min);
        // printf("timePos.pos_sec=%d, m_timePos.pos_sec=%d\n", timePos.pos_sec, m_timePos.pos_sec);
        // printf("timePos.pos_ms=%d, m_timePos.pos_ms=%d\n", timePos.pos_ms, m_timePos.pos_ms);

        // if minutes not equal to current minute, first insert it to it's minute slot
        if (timePos.pos_min != m_timePos.pos_min)
        {
            printf("insert to %d minute\n", m_firstLevelCount + m_secondLevelCount + timePos.pos_min);
                m_eventSlotList[m_firstLevelCount + m_secondLevelCount + timePos.pos_min]
                    .push_back(event);
        }
        // if minutes is equal, but second changed, insert slot to this  integral point second
        else if (timePos.pos_sec != m_timePos.pos_sec)
        {
            printf("insert to %d sec\n",m_firstLevelCount + timePos.pos_sec);
            m_eventSlotList[m_firstLevelCount + timePos.pos_sec].push_back(event);
        }
        //if minute and second is equal, mean this event will not be trigger in integral point, set it to ms slot
        else if (timePos.pos_ms != m_timePos.pos_ms)
        {
            printf("insert to %d ms\n", timePos.pos_ms);
            m_eventSlotList[timePos.pos_ms].push_back(event);
        }
     }
    return;
}

main.cpp

#include <iostream>
#include "TimeWheel.h"
using namespace std;

void funccc(void) {
    cout << "exec function" << endl;
}

int main()
{
    TimeWheel wheel;
    wheel.initTimeWheel(100, 10);
    wheel.createTimingEvent(200, funccc);
    while (1)
    {
       
    }
}

 对于上述实现的三级时间轮,下篇文章将会详细拆解其各个步骤,然后大家就可以自己撸一个时间轮了!

 对于学习Linux开发以及C/C++开发,除了上文提到的书之外,《C++ Primer》以及《Efficetive C++》等数也是必不可少.这些书籍网上资源也有很多,但是一本一本收集起来还是挺费劲的,如果需要这些书的话,我将其整理到了公众号 上,公众号搜索 程序员DeRozan,回复1207即可直接拿到整理好的学习资料.

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

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

相关文章

【蓝桥OJ—C语言】高斯日记、马虎的算式、第39级台阶

文章目录高斯日记马虎的算式第39级台阶总结高斯日记 题目&#xff1a; 大数学家高斯有个好习惯&#xff1a;无论如何都要记日记。 他的日记有个与众不同的地方&#xff0c;他从不注明年月日&#xff0c;而是用一个整数代替&#xff0c;比如&#xff1a;4210。 后来人们知道&am…

You Only Need 90K Parameters to Adapt Light 论文阅读笔记

这是BMVC2022的论文&#xff0c;提出了一个轻量化的局部全局双支路的低光照图像质量增强网络&#xff0c;有监督。 思路是先用encoder f(⋅)f(\cdot)f(⋅)转到raw-RGB域&#xff0c;再用decoder gt(⋅)g_t(\cdot)gt​(⋅)模拟ISP过程转到sRGB域。虽然文章好像没有明确指出&…

【蓝牙mesh】Network协议层介绍

【蓝牙mesh】Network协议层介绍 Network层简介 上一章节我们讲解了蓝牙Mesh中Lower层的功能和数据格式。 Lower层的数据往下传输就到了网络层&#xff08;Network Layer&#xff09;。网络层定义了收到Lower层的数据后&#xff0c;如何对其进行判断、封装、加密、认证&#xf…

学习(mianshi)必备-ClickHouse高性能查询/写入和常见注意事项(五)

目录 一、ClickHouse高性能查询原因-稀疏索引 二、ClickHouse高性能写入-LSM-Tree存储结构 什么是LSM-Tree 三、ClickHouse的常见注意事项和异常问题排查 一、ClickHouse高性能查询原因-稀疏索引 密集索引: 在密集索引中&#xff0c;数据库中的每个键值都有一个索引记录&…

Amazon S3 服务15岁生日快乐!

2021年3月14日&#xff0c;作为第一个发布的服务&#xff0c;Amazon S3 服务15周岁啦&#xff01;在中国文化里&#xff0c;15岁是个临界点&#xff0c;是从“舞勺之年”到“舞象之年”的过渡。相信对于 Amazon S3 和其他的云服务15周岁也将是其迎接更加美好未来的全新起点。亚…

【论文解读】如何使用1B参数的小模型吊打GPT3.5

大型语言模型 (LLM) 通过利用思维链 (CoT) 提示生成中间推理链作为推断答案的基本原理&#xff0c;在复杂推理上表现出了令人印象深刻的性能。 然而现有的 CoT 研究主要集中在语言模态上。 我们提出 Multimodal-CoT&#xff08;多模态思维链推理模型&#xff09;&#xff0c;它…

利用steam搬砖信息差赚钱,单账号200+,小白也能轻松上手!

现在很多人在做互联网而且也赚到钱了&#xff0c;但还是有很多人赚不到钱&#xff0c;这是为什么&#xff1f; 这里我不得不说一个词叫做赛道&#xff0c;也就是选择&#xff0c;选择大于努力&#xff0c;项目本身大于一切&#xff0c;90%的人都觉得直播带货赚钱&#xff0c;但…

VSCode配置(一)Remote SSH

插件安装 Remote-SSHRemote Explorer 可以完成下面任务 连接远程服务器&#xff08;支持rsa key的认证登陆&#xff09;&#xff0c;并访问文件结构可以经过中转机&#xff08;跳转机&#xff09;访问内网机器&#xff0c;进行IP穿透可以建立tunnel&#xff0c;将本地端口映…

弱监督论文阅读:P2BNet算法笔记

标题&#xff1a;Point-to-Box Network for Accurate Object Detection via Single Point Supervision 会议&#xff1a;ECCV2022 论文地址&#xff1a;https://link.springer.com/10.1007/978-3-031-20077-9_4 官方代码&#xff1a;http://www.github.com/ucas-vg/P2BNet 作者…

2023年网络安全竞赛——Python渗透测试PortScan.py

端口扫描Python渗透测试:需求环境可私信博主获取 任务环境说明: 服务器场景:PYsystem0041服务器场景操作系统:未知服务器场景FTP用户名:anonymous 密码:空1. 从靶机服务器的FTP上下载PortScan.py,编辑Python程序PortScan.py,实现

操作系统核心知识点整理--内存篇

操作系统核心知识点整理--内存篇按段对内存进行管理内存分区内存分页为什么需要多级页表TLB解决了多级页表什么样的缺陷?TLB缓存命中率高的原理是什么?段页结合: 为什么需要虚拟内存&#xff1f;虚拟地址到物理地址的转换过程段页式管理下程序如何载入内存&#xff1f;页面置…

NCNN Conv量化详解1

1. NCNN的Conv量化计算流程 正常的fp32计算中,一个Conv的计算流程如下: 在NCNN Conv进行Int8计算时,计算流程如下: NCNN首先将输入(bottom_blob)和权重(weight_blob)量化成INT8,在INT8下计算卷积,然后反量化到fp32,再和未量化的bias相加,得到输出(top_blob) 输入和…

学了一年Java的我,想转嵌入式了

秋名山码民的主页 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f64f;作者水平有限&#xff0c;如发现错误&#xff0c;还请私信或者评论区留言&#xff01; 目录前言为啥我想去转行&#xff1f;如果我现在选择转硬件&#xff0c;我…

【华为OD机试模拟题】用 C++ 实现 - 相同字符连续出现的最大次数(2023.Q1)

最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…

计算机网络——问答2023自用

1、高速缓冲存储器Cache的作用&#xff1f; 这种局部存储器介于CPU与主存储器DRAM之间&#xff0c;一般由高速SRAM构成&#xff0c;容量小但速度快&#xff0c;引入它是为了减小或消除CPU与内存之间的速度差异对系统性能带来的影响 &#xff08;Cache可以保存CPU刚用过或循环使…

【react storybook】从零搭建react脚手架,并使用storybook发布组件库到npm,并生成可视化UI文档

storybook成品展示开发准备开发组件写MDX文档发布文档发布组件成品展示 可视化UI文档页面&#xff1a; 可视化UI文档地址&#xff1a; https://guozia007.gitee.io/storybook-ui/?path/docs/mdx-button--default-story组件库地址&#xff1a; https://www.npmjs.com/pac…

Vmware虚拟机无法联通主机解决方法二

昨天在遇到了VMware 虚拟机无法联通主机&#xff0c;导致我在CentOS-7 搭建的伪Hadoop3 服务&#xff0c;无法访问管理平台&#xff0c;使用将网络编辑器修改为“桥接”模式解决。今天在学习HBase 时&#xff0c;昨天的问题又重新了&#xff0c;我通过SSH 工具MobaXterm 都无法…

《第一行代码》 第八章:应用手机多媒体

一&#xff0c;使用通知 第一步&#xff0c;创建项目&#xff0c;书写布局 <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:orientation"vertical"android:layout_width"match_parent"android:layout_he…

【数据结构趣味多】Map和Set

1.概念及场景 Map和set是一种专门用来进行搜索的容器或者数据结构&#xff0c;其搜索的效率与其具体的实例化子类有关。 在此之前&#xff0c;我还接触过直接查询O(N)和二分查询O(logN)&#xff0c;这两个查询有很多不足之出&#xff0c;直接查询的速率太低&#xff0c;而二分查…

如何压缩RAR格式文件?

RAR是我们日常生活工作中经常用到的压缩文件格式之一&#xff0c;那么RAR文件如何压缩呢&#xff1f; 不管压缩哪种格式的压缩文件&#xff0c;我们都需要用到压缩软件。针对RAR格式&#xff0c;我们可以选择最常见的WinRAR&#xff0c;当然如果有同样适用于RAR格式的压缩软件…