当前位置: 首页 > news >正文

排序算法:归并排序

        约翰·冯·诺伊曼在 1945 年提出了归并排序。在讲解归并排序之前,我们先一起思考一个问题:如何将两个有序的列表合并成一个有序的列表?

将两个有序的列表合并成一个有序的列表

        这太简单了,笔者首先想到的思路就是,将两个列表拼接成一个列表,然后之前学的冒泡、选择、插入、希尔、堆、快排都可以派上用场了。

觉得太暴力了一点?那我们换个思路。

        既然列表已经有序了,通过前几章的学习,我们已经知道,插入排序的过程中,被插入的数组也是有序的。这就好办了,我们将其中一个列表中的元素逐个插入另一个列表中即可。

        但是按照这个思路,我们只需要一个列表有序就行了,另一个列表不管是不是有序的,都会被逐个取出来,插入第一个列表中。那么,在两个列表都已经有序的情况下,还可以有更优的合并方案吗?

        深入思考之后,我们发现,在第二个列表向第一个列表逐个插入的过程中,由于第二个列表已经有序,所以后续插入的元素一定不会在前面插入的元素之前。在逐个插入的过程中,每次插入时,只需要从上次插入的位置开始,继续向后寻找插入位置即可。这样一来,我们最多只需要将两个有序数组遍历一次就可以完成合并。

        思路很接近了,如何实现它呢?我们发现,在向数组中不断插入新数字时,原数组需要不断腾出位置,这是一个比较复杂的过程,而且这个过程必然导致增加一轮遍历。

        但好在我们有一个替代方案:只要开辟一个长度等同于两个数组长度之和的新数组,并使用两个指针来遍历原有的两个数组,不断将较小的数字添加到新数组中,并移动对应的指针即可。

根据这个思路,我们可以写出合并两个有序列表的代码:

// 将两个有序数组合并为一个有序数组
private static int[] merge(int[] arr1, int[] arr2) {int[] result = new int[arr1.length + arr2.length];int index1 = 0, index2 = 0;while (index1 < arr1.length && index2 < arr2.length) {if (arr1[index1] <= arr2[index2]) {result[index1 + index2] = arr1[index1];index1++;} else {result[index1 + index2] = arr2[index2];index2++;}}// 将剩余数字补到结果数组之后while (index1 < arr1.length) {result[index1 + index2] = arr1[index1];index1++;}while (index2 < arr2.length) {result[index1 + index2] = arr2[index2];index2++;}return result;
}

        这份代码的实现思路和我们分析的一模一样:首先开辟了一个新数组 result,长度等同于 arr1 和 arr2 的长度之和,然后使用 index1 记录 arr1 数组的下标,index2 记录 arr2 数组的下标。再将两个数组中较小的值不断添加到 result 中。其中,result 的当前下标等同于 index1 和 index2 之和。

如果你对 ++ 运算符用得熟练的话:

result[index1 + index2] = arr1[index1];
index1++;

可以简写成:

result[index1 + index2] = arr1[index1++];

这样代码看起来会更简洁一些。

        合并有序数组的问题解决了,但我们排序时用的都是无序数组,那么上哪里去找这两个有序的数组呢?

        答案是 —— 自己拆分,我们可以把数组不断地拆成两份,直到只剩下一个数字时,这一个数字组成的数组我们就可以认为它是有序的。

        然后通过上述合并有序列表的思路,将 1 个数字组成的有序数组合并成一个包含 2 个数字的有序数组,再将 2 个数字组成的有序数组合并成包含 4 个数字的有序数组...直到整个数组排序完成,这就是归并排序(Merge Sort)的思想。

将数组拆分成有序数组

拆分过程使用了二分的思想,这是一个递归的过程,归并排序使用的递归框架如下:

public static void mergeSort(int[] arr) {if (arr.length == 0) return;int[] result = mergeSort(arr, 0, arr.length - 1);// 将结果拷贝到 arr 数组中for (int i = 0; i < result.length; i++) {arr[i] = result[i];}
}// 对 arr 的 [start, end] 区间归并排序
private static int[] mergeSort(int[] arr, int start, int end) {// 只剩下一个数字,停止拆分,返回单个数字组成的数组if (start == end) return new int[]{arr[start]};int middle = (start + end) / 2;// 拆分左边区域int[] left = mergeSort(arr, start, middle);// 拆分右边区域int[] right = mergeSort(arr, middle + 1, end);// 合并左右区域return merge(left, right);
}
    其中, mergeSort(int[] arr) 函数是对外暴露的公共方法,内部调用了私有的mergeSort(int[] arr, int start, int end) 函数,这个函数用于对 arr 的 [start, end] 区间进行归并排序。可以看到,我们在这个函数中,将原有数组不断地二分,直到只剩下最后一个数字。此时嵌套的递归开始返回,一层层地调用merge(int[] arr1, int[] arr2)函数,也就是我们刚才写的将两个有序数组合并为一个有序数组的函数。这就是最经典的归并排序,只需要一个二分拆数组的递归函数和一个合并两个有序列表的函数即可。但这份代码还有一个缺点,那就是在递归过程中,开辟了很多临时空间,接下来我们就来看下它的优化过程。

归并排序的优化:减少临时空间的开辟

    为了减少在递归过程中不断开辟空间的问题,我们可以在归并排序之前,先开辟出一个临时空间,在递归过程中统一使用此空间进行归并即可。

代码如下:

public static void mergeSort(int[] arr) {if (arr.length == 0) return;int[] result = new int[arr.length];mergeSort(arr, 0, arr.length - 1, result);
}// 对 arr 的 [start, end] 区间归并排序
private static void mergeSort(int[] arr, int start, int end, int[] result) {// 只剩下一个数字,停止拆分if (start == end) return;int middle = (start + end) / 2;// 拆分左边区域,并将归并排序的结果保存到 result 的 [start, middle] 区间mergeSort(arr, start, middle, result);// 拆分右边区域,并将归并排序的结果保存到 result 的 [middle + 1, end] 区间mergeSort(arr, middle + 1, end, result);// 合并左右区域到 result 的 [start, end] 区间merge(arr, start, end, result);
}// 将 result 的 [start, middle] 和 [middle + 1, end] 区间合并
private static void merge(int[] arr, int start,  int end, int[] result) {int middle = (start + end) / 2;// 数组 1 的首尾位置int start1 = start;int end1 = middle;// 数组 2 的首尾位置int start2 = middle + 1;int end2 = end;// 用来遍历数组的指针int index1 = start1;int index2 = start2;// 结果数组的指针int resultIndex = start1;while (index1 <= end1 && index2 <= end2) {if (arr[index1] <= arr[index2]) {result[resultIndex++] = arr[index1++];} else {result[resultIndex++] = arr[index2++];}}// 将剩余数字补到结果数组之后while (index1 <= end1) {result[resultIndex++] = arr[index1++];}while (index2 <= end2) {result[resultIndex++] = arr[index2++];}// 将 result 操作区间的数字拷贝到 arr 数组中,以便下次比较for (int i = start; i <= end; i++) {arr[i] = result[i];}
}

        在这份代码中,我们统一使用 result 数组作为递归过程中的临时数组,所以merge 函数接收的参数不再是两个数组,而是 result 数组中需要合并的两个数组的首尾下标。根据首尾下标可以分别计算出两个有序数组的首尾下标 start1、end1、start2、end2,之后的过程就和之前合并两个有序数组的代码类似了。

        这份代码还可以再精简一下,我们可以去掉一些不会改变的临时变量。比如 start1 始终等于 start,end2 始终等于 end,end1 始终等于 middle。并且分析可知,resultIndex 的值始终等于 start 加上 index1 和 index2 移动的距离。即:

resultIndex = start + (index1 - start1) + (index2 - start2)

将 start1 == start 代入,化简得:

resultIndex = index1 + index2 - start2

所以最终的归并排序代码如下:

public static void mergeSort(int[] arr) {if (arr.length == 0) return;int[] result = new int[arr.length];mergeSort(arr, 0, arr.length - 1, result);
}// 对 arr 的 [start, end] 区间归并排序
private static void mergeSort(int[] arr, int start, int end, int[] result) {// 只剩下一个数字,停止拆分if (start == end) return;int middle = (start + end) / 2;// 拆分左边区域,并将归并排序的结果保存到 result 的 [start, middle] 区间mergeSort(arr, start, middle, result);// 拆分右边区域,并将归并排序的结果保存到 result 的 [middle + 1, end] 区间mergeSort(arr, middle + 1, end, result);// 合并左右区域到 result 的 [start, end] 区间merge(arr, start, end, result);
}// 将 result 的 [start, middle] 和 [middle + 1, end] 区间合并
private static void merge(int[] arr, int start, int end, int[] result) {int end1 = (start + end) / 2;int start2 = end1 + 1;// 用来遍历数组的指针int index1 = start;int index2 = start2;while (index1 <= end1 && index2 <= end) {if (arr[index1] <= arr[index2]) {result[index1 + index2 - start2] = arr[index1++];} else {result[index1 + index2 - start2] = arr[index2++];}}// 将剩余数字补到结果数组之后while (index1 <= end1) {result[index1 + index2 - start2] = arr[index1++];}while (index2 <= end) {result[index1 + index2 - start2] = arr[index2++];}// 将 result 操作区间的数字拷贝到 arr 数组中,以便下次比较while (start <= end) {arr[start] = result[start++];}
}

原地归并排序?


        现在的归并排序看起来仍"美中不足",那就是仍然需要开辟额外的空间,能不能实现不开辟额外空间的归并排序呢?好像是可以做到的。在一些文章中,将这样的归并排序称之为 In-Place Merge Sort,直译为原地归并排序。

代码实现思路主要有两种:

public static void mergeSort(int[] arr) {if (arr.length == 0) return;mergeSort(arr, 0, arr.length - 1);
}// 对 arr 的 [start, end] 区间归并排序
private static void mergeSort(int[] arr, int start, int end) {// 只剩下一个数字,停止拆分if (start == end) return;int middle = (start + end) / 2;// 拆分左边区域mergeSort(arr, start, middle);// 拆分右边区域mergeSort(arr, middle + 1, end);// 合并左右区域merge(arr, start, end);
}// 将 arr 的 [start, middle] 和 [middle + 1, end] 区间合并
private static void merge(int[] arr, int start, int end) {int end1 = (start + end) / 2;int start2 = end1 + 1;// 用来遍历数组的指针int index1 = start;int index2 = start2;while (index1 <= end1 && index2 <= end) {if (arr[index1] <= arr[index2]) {index1++;} else {// 右边区域的这个数字比左边区域的数字小,于是它站了起来int value = arr[index2];int index = index2;// 前面的数字不断地后移while (index > index1) {arr[index] = arr[index - 1];index--;}// 这个数字坐到 index1 所在的位置上arr[index] = value;// 更新所有下标,使其前进一格index1++;index2++;end1++;}}
}

这段代码在合并 arr 的 [start, middle] 区间和 [middle + 1, end] 区间时,将两个区间较小的数字移动到 index1 的位置,并且将左边区域不断后移,目的是给新插入的数字腾出位置。最后更新两个区间的下标,继续合并更新后的区间。

第二种实现思路:

public static void mergeSort(int[] arr) {if (arr.length == 0) return;mergeSort(arr, 0, arr.length - 1);
}// 对 arr 的 [start, end] 区间归并排序
private static void mergeSort(int[] arr, int start, int end) {// 只剩下一个数字,停止拆分if (start == end) return;int middle = (start + end) / 2;// 拆分左边区域mergeSort(arr, start, middle);// 拆分右边区域mergeSort(arr, middle + 1, end);// 合并左右区域merge(arr, start, end);
}// 将 arr 的 [start, middle] 和 [middle + 1, end] 区间合并
private static void merge(int[] arr, int start, int end) {int end1 = (start + end) / 2;int start2 = end1 + 1;// 用来遍历数组的指针int index1 = start;while (index1 <= end1 && start2 <= end) {if (arr[index1] > arr[start2]) {// 将 index1 和 start2 下标的数字交换exchange(arr, index1, start2);if (start2 != end) {// 调整交换到 start2 上的这个数字的位置,使右边区域继续保持有序int value = arr[start2];int index = start2;// 右边区域比 arr[start2] 小的数字不断前移while (index < end && arr[index + 1] < value) {arr[index] = arr[index + 1];index++;}// 交换到右边区域的这个数字找到了自己合适的位置,坐下arr[index] = value;}}index1++;}
}private static void exchange(int[] arr, int i, int j) {int temp = arr[i];arr[i] = arr[j];arr[j] = temp;
}

        这段代码在合并区间时,同样是将两个区间中较小的数字移到 index1 的位置,不过采用的是两个区间的首个数字直接交换的思路,交换完成后,将交换到右边区间的数字不断后移,以使得右边区间继续保持有序。

        这两种思路看起来都很美好,但这真的实现了原地归并排序吗?

                分析代码可以看出,这样实现的归并本质上是插入排序!前文已经说到,在插入排序中,腾出位置是一个比较复杂的过程,而且这个过程必然导致增加一轮遍历。在这两份代码中,每一次合并数组时,都使用了两层循环,目的就是不断腾挪位置以插入新数字,可以看出这里合并的效率是非常低的。这两种排序算法的时间复杂度都达到了 (On²) 级,不能称之为归并排序。它们只是借用了归并排序的递归框架而已。

        也就是说,所谓的原地归并排序事实上并不存在,许多算法书籍中都没有收录这种算法。它打着归并排序的幌子,卖的是插入排序的思想,实际排序效率比归并排序低得多。

时间复杂度 & 空间复杂度

        归并排序的复杂度比较容易分析,拆分数组的过程中,会将数组拆分 logn 次,每层执行的比较次数都约等于 n 次,所以时间复杂度是 O(nlogn)。

        空间复杂度是 O(n),主要占用空间的就是我们在排序前创建的长度为 n 的 result 数组。

        分析归并的过程可知,归并排序是一种稳定的排序算法。其中,对算法稳定性非常重要的一行代码是:

if (arr[index1] <= arr[index2]) {result[index1 + index2 - start2] = arr[index1++];
}

        在这里我们通过arr[index1] <= arr[index2]来合并两个有序数组,保证了原数组中,相同的元素相对顺序不会变化,如果这里的比较条件写成了arr[index1] < arr[index2],则归并排序将变得不稳定。

        总结起来,归并排序分成两步,一是拆分数组,二是合并数组,它是分治思想的典型应用。分治的意思是“分而治之”,分的时候体现了二分的思想,“一尺之棰,日取其半,logn 世竭”,治是一个滚雪球的过程,将 1 个数字组成的有序数组合并成一个包含 2 个数字的有序数组,再将 2 个数字组成的有序数组合并成包含 4 个数字的有序数组...如《活着》一书中的经典名句:“小鸡长大了就变成了鹅;鹅长大了,就变成了羊;羊再长大了,就变成了牛...”

由于性能较好,且排序稳定,归并排序应用非常广泛,Arrays.sort() 源码中的 TimSort就是归并排序的优化版。

相关文章:

排序算法:归并排序

约翰冯诺伊曼在 1945 年提出了归并排序。在讲解归并排序之前&#xff0c;我们先一起思考一个问题&#xff1a;如何将两个有序的列表合并成一个有序的列表&#xff1f; 将两个有序的列表合并成一个有序的列表 这太简单了&#xff0c;笔者首先想到的思路就是&#xff0c;将两个列…...

Hbase-技术文档-spring-boot整合使用hbase--简单操作增删改查--提供封装高可用的模版类

使用spring-boot项目来整合使用hbase。 引入依赖 <dependency><groupId>org.apache.hbase</groupId><artifactId>hbase-client</artifactId><version>2.4.3</version> </dependency> 依赖声明表示将把Apache HBase客户端库…...

基于Pytorch的神经网络部分自定义设计

一、基础概念&#xff08;学习笔记&#xff09; &#xff08;1&#xff09;训练误差和泛化误差[1] 本质上&#xff0c;优化和深度学习的目标是根本不同的。前者主要关注的是最小化目标&#xff0c;后者则关注在给定有限数据量的情况下寻找合适的模型。训练误差和泛化误差通常不…...

持续更新串联记忆English words

&#xff08;一&#xff09;这是一组关于“服装搭配”的单词。通过在记忆中检索&#xff0c;回忆起隐藏的信息吧~ >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>今日单词>>&…...

postgresql 内核源码分析 btree索引的增删查代码基本原理流程分析,索引膨胀的原因在这里

B-Tree索引代码流程分析 ​专栏内容&#xff1a; postgresql内核源码分析手写数据库toadb并发编程 ​开源贡献&#xff1a; toadb开源库 个人主页&#xff1a;我的主页 管理社区&#xff1a;开源数据库 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&…...

详细了解G1、了解G1、G1垃圾收集器详解、G1垃圾回收器简单调优

4.详细了解G1&#xff1a; 4.1.一&#xff1a;什么是垃圾回收 4.2.了解G1 4.3.G1 Yong GC 4.4.G1 Mix GC 4.5.三色标记算法 4.6.调优实践 5.G1垃圾收集器详解 5.1.G1垃圾收集器 5.2.G1的堆内存划分 5.3.G1的运行过程 5.4.三色标记 5.4.1.漏标问题 5.5.记忆集与卡表 5.6.安全点与…...

vue项目中 package.json 详解

在 Vue 项目中&#xff0c;package.json 是一个重要的配置文件&#xff0c;它包含了项目的名称、版本、作者、依赖等信息。下面是一份详细的 Vue 项目 package.json 配置说明&#xff1a; 1.name&#xff1a;项目的名称&#xff0c;用于标识项目&#xff0c;例如&#xff1a;&q…...

为什么要进行管网水位监测,管网水位监测的作用是什么

管网水位监测是城市排水系统管理的重要手段&#xff0c;对于保障城市排水设施安全运行和提升城市管理水平具有重要意义。通过对排水管网的水位进行实时监测和分析&#xff0c;能够及时发现问题并采取措施&#xff0c;提高排水系统的运行效率和管理水平。本文将详细介绍为什么要…...

webpack学习笔记

1. webpack基本概念 webpack&#xff1a; JavaScript 应用程序的静态模块打包器&#xff0c;是目前最为流行的JavaScript打包工具之一。webpack会以一个或多个js文件为入口&#xff0c;递归检查每个js模块的依赖&#xff0c;从而构建一个依赖关系图&#xff0c;然后依据该关系…...

解析代理IP在跨境电商和社媒营销中的关键作用

跨境电商和社媒营销领域的从业者深知&#xff0c;代理IP的价值愈发凸显。在推广营销的过程中&#xff0c;频繁遇到因IP关联而封禁账号的情况&#xff0c;或因使用不安全IP而导致异常问题。 这些问题促使人们开始高度重视代理IP的作用。但实际上&#xff0c;代理IP究竟是何物&a…...

Unity 之 Start 与Update 方法的区别

文章目录 当谈论Unity中的 Start和 Update方法时&#xff0c;我们实际上是在讨论MonoBehaviour类中的两个常用方法&#xff0c;用于编写游戏逻辑。这两个方法在不同的时机被调用&#xff0c;因此您可以根据需要选择在哪个方法中编写特定的代码。 Start 方法&#xff1a; Start…...

Spring Boot中如何编写优雅的单元测试

单元测试是指对软件中的最小可测试单元进行检查和验证。在Java中&#xff0c;单元测试的最小单元是类。通过编写针对类或方法的小段代码&#xff0c;来检验被测代码是否符合预期结果或行为。执行单元测试可以帮助开发者验证代码是否正确实现了功能需求&#xff0c;以及是否能够…...

三星Galaxy S23与iPhone 15的对比分析:谁会胜出?

三星Galaxy S23与iPhone 15的对决将于下个月进入高潮,这将是今年智能手机中最大的一场较量。毕竟,这是两家领先的移动设备制造商的旗舰手机。他们的手机的比较将在很大程度上决定谁能获得最佳手机的称号。 我们已经知道有利于三星Galaxy S23的情况,该产品自春季以来一直在推…...

MySQL索引 事物 存储引擎

一 索引 索引的概念 索引就是一种帮助系统能够更快速的查找信息的结构 索引的作用 索引的副作用 创建索引的规则 MySQL的优化 哪些字段/场景适合创建索引 哪些不适合 小字段唯一性强的字段更新不频繁&#xff0c;但查询率比较高的字段表记录超过 300行主键&#xff0c;外键…...

【谷粒学院】报错记录

无法从Nacos获取动态配置 原先gulimall-common中SpringCloud Alibaba的版本是2.1.0.RELEASE&#xff0c;无法从Nacos中获取配置文件信息 <dependencyManagement><dependencies><dependency><groupId>com.alibaba.cloud</groupId><artifactId&…...

微积分基本概念

微分 函数的微分是指对函数的局部变化的一种线性描述。微分可以近似地描述当函数自变量的取值作足够小的改变时&#xff0c;函数的值是怎样改变的。。对于函数 y f ( x ) y f(x) yf(x) 的微分记作&#xff1a; d y f ′ ( x ) d x d_y f^{}(x)d_x dy​f′(x)dx​ 微分和…...

【业务功能篇78】微服务-前端后端校验- 统一异常处理-JSR-303-validation注解

5. 前端校验 我们在前端提交的表单数据&#xff0c;我们也是需要对提交的数据做相关的校验的 Form 组件提供了表单验证的功能&#xff0c;只需要通过 rules 属性传入约定的验证规则&#xff0c;并将 Form-Item 的 prop 属性设置为需校验的字段名即可 校验的页面效果 前端数据…...

pytorch的用法

...

Qt 设置窗口背景

窗口背景无非两种&#xff1a;背景色、背景图片。Qt中窗口背景如何设置&#xff1f; 一、QPalette设置背景 二、实现paintEvent&#xff0c;使用QPainter来绘制背景 三、使用QSS来设置背景 关于QSS的使用不想多说&#xff0c;一般我不用QSS设置窗口背景&#xff0c;也不建议…...

大模型是什么?泰迪大模型能够解决企业哪些痛点?

什么是大模型&#xff1f; 大模型是指模型具有庞大的参数规模和复杂程度的机器学习模型。在深度学习领域&#xff0c;大模型通常是指具有数百万到数十亿参数的神经网络模型。这些模型需要大量的计算资源和存储空间来训练和存储&#xff0c;并且往往需要进行分布式计算和特殊…...

Android Wi-Fi 连接失败日志分析

1. Android wifi 关键日志总结 (1) Wi-Fi 断开 (CTRL-EVENT-DISCONNECTED reason3) 日志相关部分&#xff1a; 06-05 10:48:40.987 943 943 I wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED bssid44:9b:c1:57:a8:90 reason3 locally_generated1解析&#xff1a; CTR…...

数据链路层的主要功能是什么

数据链路层&#xff08;OSI模型第2层&#xff09;的核心功能是在相邻网络节点&#xff08;如交换机、主机&#xff09;间提供可靠的数据帧传输服务&#xff0c;主要职责包括&#xff1a; &#x1f511; 核心功能详解&#xff1a; 帧封装与解封装 封装&#xff1a; 将网络层下发…...

相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)

【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...

OPENCV形态学基础之二腐蚀

一.腐蚀的原理 (图1) 数学表达式&#xff1a;dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一&#xff0c;腐蚀跟膨胀属于反向操作&#xff0c;膨胀是把图像图像变大&#xff0c;而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...

七、数据库的完整性

七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...

【Nginx】使用 Nginx+Lua 实现基于 IP 的访问频率限制

使用 NginxLua 实现基于 IP 的访问频率限制 在高并发场景下&#xff0c;限制某个 IP 的访问频率是非常重要的&#xff0c;可以有效防止恶意攻击或错误配置导致的服务宕机。以下是一个详细的实现方案&#xff0c;使用 Nginx 和 Lua 脚本结合 Redis 来实现基于 IP 的访问频率限制…...

在树莓派上添加音频输入设备的几种方法

在树莓派上添加音频输入设备可以通过以下步骤完成&#xff0c;具体方法取决于设备类型&#xff08;如USB麦克风、3.5mm接口麦克风或HDMI音频输入&#xff09;。以下是详细指南&#xff1a; 1. 连接音频输入设备 USB麦克风/声卡&#xff1a;直接插入树莓派的USB接口。3.5mm麦克…...

边缘计算网关提升水产养殖尾水处理的远程运维效率

一、项目背景 随着水产养殖行业的快速发展&#xff0c;养殖尾水的处理成为了一个亟待解决的环保问题。传统的尾水处理方式不仅效率低下&#xff0c;而且难以实现精准监控和管理。为了提升尾水处理的效果和效率&#xff0c;同时降低人力成本&#xff0c;某大型水产养殖企业决定…...

动态规划-1035.不相交的线-力扣(LeetCode)

一、题目解析 光看题目要求和例图&#xff0c;感觉这题好麻烦&#xff0c;直线不能相交啊&#xff0c;每个数字只属于一条连线啊等等&#xff0c;但我们结合题目所给的信息和例图的内容&#xff0c;这不就是最长公共子序列吗&#xff1f;&#xff0c;我们把最长公共子序列连线起…...

【Java】Ajax 技术详解

文章目录 1. Filter 过滤器1.1 Filter 概述1.2 Filter 快速入门开发步骤:1.3 Filter 执行流程1.4 Filter 拦截路径配置1.5 过滤器链2. Listener 监听器2.1 Listener 概述2.2 ServletContextListener3. Ajax 技术3.1 Ajax 概述3.2 Ajax 快速入门服务端实现:客户端实现:4. Axi…...