面试必问:四种经典限流算法

news2024/12/26 20:52:15

今天给大家分享一下限流方面的,大家都知道,在分布式系统中,高并发场景下,为了防止系统因突然的流量激增而导致的崩溃,同时保证服务的高可用性和稳定性,限流是最常用的手段。希望能够给大家带来帮助,大家多多点赞关注支持!!

所以面试经常会被问到限流问题,特别是限流的算法,基本上是必问,今天我为大家分享一下,或许许多人都大概都听过四种限流算法,分别是:固定窗口算法、滑动窗口算法、漏桶算法、令牌桶算法。但是对于他们之间的具体优缺点,以及适用的场景,还有具体的实现可能就不是特别的清楚了,所以我今天为大家详细地分享一下其中的根本区别,以及优缺点还有具体场景,以及最终的实现。

固定窗口限流算法

概念

固定窗口限流算法是一种最简单的限流算法,该算法将时间分成固定的窗口,并在每个窗口内限制请求的数量。具体来说,算法将请求按照时间顺序放入时间窗口中,并计算该时间窗口内的请求数量,如果请求数量超出了限制,则拒绝该请求。
举个例子:假设单位时间是1秒,限流阀值为3。在单位时间1秒内,每来一个请求,计数器就加1,如果计数器累加的次数超过限流阀值3,后续的请求全部拒绝。等到1s结束后,计数器清0,重新开始计数。

实现原理

在一个固定长度的时间窗口内限制请求数量,每来一个请求,请求次数加一,如果请求数量超过最大限制,就拒绝该请求。如下图:
在这里插入图片描述

具体代码实现

我们先说下具体实现流程:
在一开 始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,那么说明请求数过多,拒绝访问;
如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

//计数器 限流
public class CounterLimiter {

    //起始时间
    private static long startTime = System.currentTimeMillis();

    //时间间隔1000ms
    private static long interval = 1000;

    //每个时间间隔内,限制数量
    private static long limit = 3;

    //累加器
    private static AtomicLong accumulator = new AtomicLong();

    /**
     * true 代表放行,请求可已通过
     * false 代表限制,不让请求通过
     */
    public static boolean tryAcquire() {
        long nowTime = System.currentTimeMillis();
        //判断是否在上一个时间间隔内
        if (nowTime < startTime + interval) {
            //如果还在上个时间间隔内
            long count = accumulator.incrementAndGet();
            if (count <= limit) {
                return true;
            } else {
                return false;
            }
        } else {
            //如果不在上一个时间间隔内
            synchronized (CounterLimiter.class) {
                //防止重复初始化
                if (nowTime > startTime + interval) {
                    startTime = nowTime;
                    accumulator.set(0);
                }
            }
            //再次进行判断
            long count = accumulator.incrementAndGet();
            if (count <= limit) {
                return true;
            } else {
                return false;
            }
        }
    }


    // 测试
    public static void main(String[] args) {

        //线程池,用于多线程模拟测试
        ExecutorService pool = Executors.newFixedThreadPool(10);
        // 被限制的次数
        AtomicInteger limited = new AtomicInteger(0);
        // 线程数
        final int threads = 2;
        // 每条线程的执行轮数
        final int turns = 20;
        // 同步器
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        long start = System.currentTimeMillis();
        for (int i = 0; i < threads; i++) {
            pool.submit(() ->
            {
                try {

                    for (int j = 0; j < turns; j++) {

                        boolean flag = tryAcquire();
                        if (!flag) {
                            // 被限制的次数累积
                            limited.getAndIncrement();
                        }
                        Thread.sleep(200);
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
                //等待所有线程结束
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        float time = (System.currentTimeMillis() - start) / 1000F;
        //输出统计结果
        System.out.println("限制的次数为:" + limited.get() +
                ",通过的次数为:" + (threads * turns - limited.get()));
        System.out.println("限制的比例为:" + (float) limited.get() / (float) (threads * turns));
        System.out.println("运行的时长为:" + time + "s");
    }

}

优缺点

  • 优点:固定窗口算法非常简单,易于实现和理解。
  • 缺点:存在明显的临界问题,假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在 1秒里面,瞬间发送了200个请求。我们刚才规定的是1分钟最多100个请求(规划的吞吐量),也就是每秒钟最多1.7个请求,用户通过在时间窗口的重置节点处突发请求, 可以瞬间超过我们的速率限制。所以用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。

滑动窗口限流算法

概念

滑动窗口限流算法是一种常用的限流算法,用于控制系统对外提供服务的速率,防止系统被过多的请求压垮。它将单位时间周期分为n个小周期,分别记录每个小周期内接口的访问次数,并且根据时间滑动删除过期的小周期。它可以解决固定窗口临界值的问题。

实现原理

它是对固定窗口算法的一种改进。在滑动窗口算法中,窗口的起止时间是动态的,窗口的大小固定。这种算法能够较好地处理窗口边界问题,但是实现相对复杂,需要记录每个请求的时间戳。

实现原理是: 每来一个请求,就向后推一个时间窗口,计算这个窗口内的请求数量。如果请求数量超过限制就拒绝请求,否则就处理请求,并记录请求的时间戳。另外还需要一个任务清理过期的时间戳。

在这里插入图片描述

代码实现

 /**
     * 单位时间划分的小周期(单位时间是1分钟,10s一个小格子窗口,一共6个格子)
     */
    private int SUB_CYCLE = 10;

    /**
     * 每分钟限流请求数
     */
    private int thresholdPerMin = 100;

    /**
     * 计数器, k-为当前窗口的开始时间值秒,value为当前窗口的计数
     */
    private final TreeMap<Long, Integer> counters = new TreeMap<>();

   /**
     * 滑动窗口时间算法实现
     */
     public synchronized boolean slidingWindowsTryAcquire() {
        long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //获取当前时间在哪个小周期窗口
        int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数

        //超过阀值限流
        if (currentWindowNum >= thresholdPerMin) {
            return false;
        }

        //计数器+1
        counters.get(currentWindowTime)++;
        return true;
    }

   /**
    * 统计当前窗口的请求数
    */
    private synchronized int countCurrentWindow(long currentWindowTime) {
        //计算窗口开始位置
        long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1);
        int count = 0;

        //遍历存储的计数器
        Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Long, Integer> entry = iterator.next();
            // 删除无效过期的子窗口计数器
            if (entry.getKey() < startTime) {
                iterator.remove();
            } else {
                //累加当前窗口的所有计数器之和
                count =count + entry.getValue();
            }
        }
        return count;
    }

优缺点

优点

  • 简单易懂
  • 精度高(通过调整时间窗口的大小来实现不同的限流效果)
  • 可扩展性强(可以非常容易地与其他限流算法结合使用)

缺点

  • 突发流量无法处理,无法应对短时间内的大量请求,但是一旦到达限流后,请求都会直接暴力被拒绝。所以需要合理调整时间窗口大小。

漏桶限流算法

概念

漏桶限流算法是一种流量控制算法,用于控制流入网络的数据速率,以防止网络拥塞。它的思想是将数据包看作是水滴,漏桶看作是一个固定容量的水桶,数据包像水滴一样从桶的顶部流入桶中,并通过桶底的一个小孔以一定的速度流出,从而限制了数据包的流量。

实现原理

漏桶算法限流的基本原理为:水(对应请求)从进水口进入到漏桶里,漏桶以一定的速度出水(请求放行),当水流入速度过大,桶内的总水量大于桶容量会直接溢出,请求被拒绝。

漏桶限流规则如下:

  1. 进水口(对应客户端请求)以任意速率流入进入漏桶。
  2. 漏桶的容量是固定的,出水(放行)速率也是固定的。
  3. 漏桶容量是不变的,如果处理速度太慢,桶内水量会超出了桶的容量,则后面流入的水滴会溢出,表示请求拒绝。

如下图:
在这里插入图片描述

具体代码实现

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

//漏斗限流
public class LeakBucketLimiter {

    //桶的大小
    private static long capacity = 10;
    //流出速率,每秒两个
    private static long rate = 2;
    //开始时间
    private static long startTime = System.currentTimeMillis();
    //桶中剩余的水
    private static AtomicLong water = new AtomicLong();

    /**
     * true 代表放行,请求可已通过
     * false 代表限制,不让请求通过
     */
    public synchronized static boolean tryAcquire() {
        //如果桶的余量问0,直接放行
        if (water.get() == 0) {
            startTime = System.currentTimeMillis();
            water.set(1);
            return true;
        }
        //计算从当前时间到开始时间流出的水,和现在桶中剩余的水
        //桶中剩余的水
        water.set(water.get() - (System.currentTimeMillis() - startTime) / 1000 * rate);
        //防止出现<0的情况
        water.set(Math.max(0, water.get()));
        //设置新的开始时间
        startTime += (System.currentTimeMillis() - startTime) / 1000 * 1000;
        //如果当前水小于容量,表示可以放行
        if (water.get() < capacity) {
            water.incrementAndGet();
            return true;
        } else {
            return false;
        }
    }


    // 测试
    public static void main(String[] args) {

        //线程池,用于多线程模拟测试
        ExecutorService pool = Executors.newFixedThreadPool(10);
        // 被限制的次数
        AtomicInteger limited = new AtomicInteger(0);
        // 线程数
        final int threads = 2;
        // 每条线程的执行轮数
        final int turns = 20;
        // 同步器
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        long start = System.currentTimeMillis();
        for (int i = 0; i < threads; i++) {
            pool.submit(() ->
            {
                try {

                    for (int j = 0; j < turns; j++) {

                        boolean flag = tryAcquire();
                        if (!flag) {
                            // 被限制的次数累积
                            limited.getAndIncrement();
                        }
                        Thread.sleep(200);
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
                //等待所有线程结束
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        float time = (System.currentTimeMillis() - start) / 1000F;
        //输出统计结果
        System.out.println("限制的次数为:" + limited.get() +
                ",通过的次数为:" + (threads * turns - limited.get()));
        System.out.println("限制的比例为:" + (float) limited.get() / (float) (threads * turns));
        System.out.println("运行的时长为:" + time + "s");
    }

}

优缺点

优点

  1. 可以平滑限制请求的处理速度,避免瞬间请求过多导致系统崩溃或者雪崩。
  2. 可以控制请求的处理速度,使得系统可以适应不同的流量需求,避免过载或者过度闲置。
  3. 可以通过调整桶的大小和漏出速率来满足不同的限流需求,可以灵活地适应不同的场景。

缺点

  1. 需要对请求进行缓存,会增加服务器的内存消耗。
  2. 对于流量波动比较大的场景,需要较为灵活的参数配置才能达到较好的效果。
  3. 但是面对突发流量的时候,漏桶算法还是循规蹈矩地处理请求,流量变突发时,我们肯定希望系统尽量快点处理请求,提升用户体验。

令牌桶算法

概念

令牌桶算法是一种常用的限流算法,可以用于限制单位时间内请求的数量。该算法维护一个固定容量的令牌桶,每秒钟会向令牌桶中放入一定数量的令牌。当有请求到来时,如果令牌桶中有足够的令牌,则请求被允许通过并从令牌桶中消耗一个令牌,否则请求被拒绝。

实现原理

令牌桶算法中新请求到来时会从桶里拿走一个令牌,如果桶内没有令牌可拿,就拒绝服务。 当然,令牌的数量也是有上限的。令牌的数量与时间和发放速率强相关,时间流逝的时间越长,会不断往桶里加入越多的令牌,如果令牌发放的速度比申请速度快,令牌桶会放满令牌,直到令牌占满整个令牌桶。
令牌桶限流大致的规则如下:

  1. 进水口按照某个速度,向桶中放入令牌。
  2. 令牌的容量是固定的,但是放行的速度不是固定的,只要桶中还有剩余令牌,一旦请求过来就能申请成功,然后放行。
  3. 如果令牌的发放速度,慢于请求到来速度,桶内就无牌可领,请求就会被拒绝。

总之,令牌的发送速率可以设置,从而可以对突发的出口流量进行有效的应对。

在这里插入图片描述

代码实现

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

//令牌桶
public class TokenBucketLimiter {
    //桶的容量
    private static long capacity = 10;
    //放入令牌的速率,每秒2个
    private static long rate = 2;
    //上次放置令牌的时间
    private static long lastTime = System.currentTimeMillis();
    //桶中令牌的余量
    private static AtomicLong tokenNum = new AtomicLong();

    /**
     * true 代表放行,请求可已通过
     * false 代表限制,不让请求通过
     */
    public synchronized static boolean tryAcquire() {
        //更新桶中剩余令牌的数量
        long now = System.currentTimeMillis();
        tokenNum.addAndGet((now - lastTime) / 1000 * rate);
        tokenNum.set(Math.min(capacity, tokenNum.get()));
        //更新时间
        lastTime += (now - lastTime) / 1000 * 1000;
        //桶中还有令牌就放行
        if (tokenNum.get() > 0) {
            tokenNum.decrementAndGet();
            return true;
        } else {
            return false;
        }
    }


    //测试
    public static void main(String[] args) {

        //线程池,用于多线程模拟测试
        ExecutorService pool = Executors.newFixedThreadPool(10);
        // 被限制的次数
        AtomicInteger limited = new AtomicInteger(0);
        // 线程数
        final int threads = 2;
        // 每条线程的执行轮数
        final int turns = 20;
        // 同步器
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        long start = System.currentTimeMillis();
        for (int i = 0; i < threads; i++) {
            pool.submit(() ->
            {
                try {

                    for (int j = 0; j < turns; j++) {

                        boolean flag = tryAcquire();
                        if (!flag) {
                            // 被限制的次数累积
                            limited.getAndIncrement();
                        }
                        Thread.sleep(200);
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
                //等待所有线程结束
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        float time = (System.currentTimeMillis() - start) / 1000F;
        //输出统计结果
        System.out.println("限制的次数为:" + limited.get() +
                ",通过的次数为:" + (threads * turns - limited.get()));
        System.out.println("限制的比例为:" + (float) limited.get() / (float) (threads * turns));
        System.out.println("运行的时长为:" + time + "s");
    }

}

优缺点

优点

  • 稳定性高:令牌桶算法可以控制请求的处理速度,可以使系统的负载变得稳定。
  • 精度高:令牌桶算法可以根据实际情况动态调整生成令牌的速率,可以实现较高精度的限流。
  • 弹性好:令牌桶算法可以处理突发流量,可以在短时间内提供更多的处理能力,以处理突发流量。

Guava的RateLimiter限流组件,就是基于令牌桶算法实现的。

缺点

  • 实现复杂:相对于固定窗口算法等其他限流算法,令牌桶算法的实现较为复杂。
  • 对短时请求难以处理:在短时间内有大量请求到来时,可能会导致令牌桶中的令牌被快速消耗完,从而限流。这种情况下,可以考虑使用漏桶算法。
  • 时间精度要求高:令牌桶算法需要在固定的时间间隔内生成令牌,因此要求时间精度较高,如果系统时间不准确,可能会导致限流效果不理想。

总体来说,令牌桶算法具有较高的稳定性和精度,但实现相对复杂,适用于对稳定性和精度要求较高的场景。

最终对比

在这里插入图片描述

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

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

相关文章

STM32常见面试题

一、STM32F1和F4的区别&#xff1f; 内核不同&#xff1a;F1是Cortex-M3内核&#xff0c;F4是Cortex-M4内核&#xff1b; 主频不同&#xff1a;F1主频72MHz&#xff0c;F4主频168MHz&#xff1b; 浮点运算&#xff1a;F1无浮点运算单位&#xff0c;F4有&#xff1b; 功能性能&…

【无标题】vue中表单绑定v-model

表单绑定v-model 表单控件在实际开发中是非常常见的。特别是对于用户信息的提交&#xff0c;需要大量的表单。 Vue中使用v-model指令来实现表单元素和数据的双向绑定。 案例的解析&#xff1a; 当我们在输入框输入内容时 因为input中的v-model绑定了message&#xff0c;所以会…

Vue-搭建Vuex开发环境

1 安装Vuex 安装之前需要了解一个版本问题&#xff0c;在vue2中&#xff0c;要用vuex的3版本&#xff0c;在vue3中&#xff0c;要用vuex的4版本&#xff0c;要严格遵循这个版本要求&#xff0c;不然就会出现各种意想不到的问题&#xff0c;例如下方安装报错&#xff0c;就算因…

ubuntu修改应用图表|任务栏收藏|快捷方式|收藏夹

需要知道应用程序对应的.desktop文件的位置&#xff0c;然后使用sudo gedit打开。修改对应位置的信息就可以了。 参考&#xff1a;Linux下Desktop文件入门 1.desktop文件位置 一般存放在/usr/share/applications这个位置里面。 以vscode为例&#xff0c;使用sudo gedit code…

POJ - 2287 Tian Ji -- The Horse Racing

题目来源 2287 -- Tian Ji -- The Horse Racing (poj.org) 题目描述 田忌赛马是中国历史上一个著名的故事。 这个故事发生在2300年前&#xff0c;田忌是齐国的一个大官&#xff0c;他喜欢和齐王以及其他公子赛马。 田忌和齐王都有三类马&#xff0c;分别是下等马&#xff0…

1750_使用gcc对嵌入式代码控制逻辑进行测试

全部学习汇总&#xff1a; GreyZhang/c_basic: little bits of c. (github.com) 相信很多人的C语言学习是从printf开始的&#xff0c;为了验证我们的程序代码运行结果&#xff0c;我们通常会选择使用printf打印出我们计算的结果看一下是否与预期一致。到了嵌入式软件开发&#…

web前端工程师个人简历编写(附详细代码)

web前端工程师 h5css3完成简历编写&#xff0c;效果如下&#xff1a; 底部附有详细代码编写 编写Web前端工程师个人简历时&#xff0c;需要注意以下几点&#xff1a; 简洁明了&#xff1a;简历应该简洁明了&#xff0c;内容要点突出&#xff0c;避免冗长和废话。用简洁的语言…

Boost的介绍、安装与环境配置

文章目录 一、Boost库简介二、Boost的安装与编译&#xff08;一&#xff09;下载解压&#xff08;二&#xff09;编译静态库 三、配置VS环境四、其它环境的配置&#xff08;vscode、DevC&#xff09;&#xff08;一&#xff09;在DEVC中配置使用boost库的环境&#xff08;二&am…

java: 程序包javax.servlet.http不存在

问题描述 当项目从2.7.x的springboot升级到3.0.x的时候&#xff0c;遇到一个问题“java: 程序包javax.servlet.http不存在” 。这可能是一些包的精简变化导致的。错误信息如下&#xff1a; 错误代码段 package com.softdev.system.generator.config;import com.softdev.system…

深度学习-ubuntu18.04+RTX3080+cuda11.2+cudnn8.1.0下安装polarstream全纪录

&#xff11;、安装 创建一个python3.7的虚拟环境 conda create --name polarstream python3.7 激活虚拟环境 source activate polarstream以下操作均在虚拟环境中进行 安装与cuda和python版本对应的torch版本,参考https://blog.csdn.net/didadifish/article/details/12748…

【软件设计师暴击考点】操作系统知识高频考点暴击系列【二】

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;软件…

栈的应用——括号匹配、表达式求值、递归

目录 一、栈在括号匹配中的应用逻辑实现代码实现 二、栈在表达式求值中的应用手算实现代码实现 三、栈在递归中的应用逻辑实现代码实现 一、栈在括号匹配中的应用 括号匹配&#xff0c;顾名思义。若括号按照正确的格式嵌套&#xff0c;则可正确匹配&#xff0c;例如([])&#…

scratch lenet(12): LeNet-5输出层和损失函数的计算

文章目录 1. 目的2. 输出层结构2.1 Gaussian Connection2.2 Gaussian Connection 的 weight 可视化 3. Loss Function3.1 当前类别判断错误时&#xff0c;loss function 中的项&#xff08;基本项&#xff09;3.2 判断为其他类别时&#xff0c; loss function 中的项&#xff0…

Spring发展历程及其体系结构

⭐作者介绍&#xff1a;大二本科网络工程专业在读&#xff0c;持续学习Java&#xff0c;努力输出优质文章 ⭐作者主页&#xff1a;逐梦苍穹 ⭐所属专栏&#xff1a;Spring 目录 发展历程体系结构 发展历程 体系结构 Spring框架的体系结构的主要组成部分&#xff1a; 核心容器…

idea如何集成Tomcat

&#xff08;1&#xff09;、这里应该找Add Configuration点击这里&#xff1a;如果没有标志&#xff0c;点击Exit (2)、这里可以配置一个配置项&#xff1a; &#xff08;3&#xff09;、loacl是本地&#xff0c;那个是远程&#xff1a;这里我选择本地 &#xff08;4&#xff…

代码随想录算法训练营第四十二天 | 01背包理论基础,01背包理论基础(滚动数组),416. 分割等和子集

代码随想录算法训练营第四十二天 | 01背包理论基础&#xff0c;01背包理论基础&#xff08;滚动数组&#xff09;&#xff0c;416. 分割等和子集 1.1 01背包理论基础 01背包 回溯法&#xff1a;暴力的解法是o(2^n)指数级别的时间复杂度&#xff0c;需要动态规划的解法来进行优…

如果你曾经拥有python,那么现在你应该拥抱Julia吗?

看完本文&#xff0c;您就会有较成熟的想法。 Julia和Python的区别是什么&#xff1f;为什么Julia适合用于大规模计算和超级计算机模拟&#xff1f; 你一定听说过Julia和Python这两个编程语言。虽然它们都可以用于从简单的机器学习应用程序到巨大的超级计算机模拟的所有方面&am…

Gradio的Audio组件介绍

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

ImGUI项目建立(cmake+MinGW64)

Dear ImGUI ImGui是一个轻量级的C图形界面库&#xff0c;它可以用于创建各种交互式的工具和编辑器。具有跨平台、高性能的特点。 ImGUI自身不能创建窗口&#xff0c;需要使用Win32API或glfw或SDL等工具来创建窗口&#xff0c;另外需要使用OpenGL或DirectX、vulkan用于渲染图形…

excel数据的编排与整理——表格结构的整理(一)

excel数据的编排与整理——表格结构的整理(一) 1 快速移动一列数据到指定位置 1.1 移动到相邻行 1.1.1 题目内容 1.1.2 选中年龄列➡移动到左侧直到出现十字箭头 1.1.3 按下shift键和左键➡移动到指定位置,直到出现"T"字形 1.1.4 松开鼠标左键后,移动就完成了 1.2…