详解动态规划算法
动态规划
- 一、动态规划的核心思想
- 二、动态规划的步骤
- 1. 定义状态(State)
- 2. 确定状态转移方程(State Transition Equation)
- 3. 确定边界条件(Base Case)
- 4. 填表(Table Filling)或递归计算
- 三、动态规划的两种常见实现方式
- 1. 自顶向下(备忘录法)
- 2. 自底向上(迭代法)
- 四、动态规划的经典应用
- 1. 背包问题
- 问题描述:
- 动态规划解法:
- 状态转移方程:
- 初始化:
- 时间复杂度:
- 代码实现:
- 代码解释:
- 解释:
- 其他优化:
- 2. 最长公共子序列(LCS)
- 问题描述:
- 动态规划解法:
- 1. 状态定义:
- 2. 状态转移方程:
- 3. 边界条件:
- 4. 最终结果:
- 动态规划的空间优化:
- 代码实现:
- 代码解析:
- 示例分析:
- 时间和空间复杂度:
- 五、动态规划的优化技巧
- 六、总结
一、动态规划的核心思想
动态规划的核心思想是将复杂问题分解为简单的子问题,并通过存储子问题的解来避免重复计算,从而节省计算时间。动态规划通常适用于满足以下两个条件的问题:
- 最优子结构(Optimal Substructure):一个问题的最优解可以由其子问题的最优解构成。
- 重叠子问题(Overlapping Subproblems):问题可以分解为许多重复的子问题,而这些子问题的解会被多次利用。
二、动态规划的步骤
解决动态规划问题时,通常遵循以下步骤:
1. 定义状态(State)
首先,我们需要定义一个状态,用来描述问题的子结构。在实际操作中我们通常会定义一个数组或表格来存储子问题的结果。要注意状态的定义应该尽量简单,但又能包含足够的信息来从中推导出最终的解。
2. 确定状态转移方程(State Transition Equation)
状态转移方程是解决动态规划问题的关键,它描述了如何从一个子问题的解推导出更大问题的解。状态转移方程通常是递推公式,能够根据已解决的子问题来构造当前问题的解。
3. 确定边界条件(Base Case)
对于最小的子问题(也就是递归的基础情形),需要明确给出其解,这些就是边界条件。在实现时,这些边界条件是动态规划算法的起点,通常是一些初始值。。
4. 填表(Table Filling)或递归计算
- 自底向上:通过循环填充表格(通常是二维数组或一维数组),从最小的子问题开始,逐步计算出最终的解。
- 自顶向下:通过递归调用子问题,并使用备忘录(Memoization)存储已计算的结果,避免重复计算。
三、动态规划的两种常见实现方式
1. 自顶向下(备忘录法)
在自顶向下的方法中,我们通过递归来解决问题,每当遇到一个子问题时,首先检查它是否已经被计算过。如果已经计算过,则直接返回存储的结果;如果没有计算过,则进行递归计算,并将结果存储起来,以便以后使用。
# 定义一个计算斐波那契数列的函数,使用记忆化递归优化性能
def fibonacci(n, memo={}):# 如果 n 已经在 memo 字典中,直接返回 memo[n],避免重复计算if n in memo:return memo[n]# 基本情况:斐波那契数列的前两项分别是 0 和 1if n <= 1:return n# 递归计算第 n 项的值:# 第 n 项等于第 (n-1) 项和第 (n-2) 项的和# 并将结果存储到 memo 字典中,供后续调用时直接使用memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)# 返回计算结果return memo[n]# 调用函数计算斐波那契数列的第 10 项
print(fibonacci(10)) # 输出 55
这个例子使用了递归,且用 memo 字典存储已经计算过的斐波那契数。
2. 自底向上(迭代法)
自底向上的方法通常通过迭代来逐步填充动态规划表格。首先解决最小的子问题,然后逐步解决更大的子问题,直到最后得到原问题的解。
# 定义一个计算斐波那契数列的函数,使用动态规划(DP)实现
def fibonacci(n):# 基本情况:如果 n 小于等于 1,直接返回 n# 因为斐波那契数列的第 0 项是 0,第 1 项是 1if n <= 1:return n# 初始化一个长度为 (n+1) 的列表 dp,用于存储斐波那契数列的每一项# dp[i] 表示斐波那契数列的第 i 项dp = [0] * (n + 1)# 显式设置斐波那契数列的第 1 项为 1dp[1] = 1# 使用循环从第 2 项开始计算到第 n 项# 每一项 dp[i] 等于前两项 dp[i-1] 和 dp[i-2] 的和for i in range(2, n + 1):dp[i] = dp[i - 1] + dp[i - 2]# 返回斐波那契数列的第 n 项return dp[n]# 调用函数计算斐波那契数列的第 10 项
print(fibonacci(10)) # 输出 55
这里使用一个一维数组 dp 存储每个斐波那契数,从 dp[0] 到 dp[n],并通过迭代计算出最终的结果。
四、动态规划的经典应用
1. 背包问题
问题描述:
- 给定
n个物品,每个物品有重量和价值。 - 背包的最大容量是
C,要求在不超过容量限制的情况下,使得背包中的物品总价值最大。
动态规划解法:
动态规划的核心思想是使用一个二维数组 dp[i][w] 来表示前 i 个物品放入容量为 w 的背包时的最大价值。
状态转移方程:
-
如果当前物品的重量大于背包的剩余容量,那么当前物品不能放入背包。此时最大价值等于不放当前物品时的最大价值:
d p [ i ] [ w ] = d p [ i − 1 ] [ w ] dp[i][w] = dp[i-1][w] dp[i][w]=dp[i−1][w]
-
如果当前物品的重量小于等于背包的剩余容量,我们可以选择放入或不放入当前物品:
- 不放入当前物品,最大价值为
dp[i-1][w]。 - 放入当前物品时,最大价值为
dp[i-1][w - weight[i-1]] + value[i-1](即背包容量减去当前物品的重量,然后加上当前物品的价值)。
所以状态转移方程为:
d p [ i ] [ w ] = max ( d p [ i − 1 ] [ w ] , d p [ i − 1 ] [ w − w e i g h t [ i − 1 ] ] + v a l u e [ i − 1 ] ) dp[i][w] = \max(dp[i-1][w], dp[i-1][w - weight[i-1]] + value[i-1]) dp[i][w]=max(dp[i−1][w],dp[i−1][w−weight[i−1]]+value[i−1])
- 不放入当前物品,最大价值为
初始化:
dp[0][w] = 0:表示没有物品时,不管背包容量如何,最大价值都是0。dp[i][0] = 0:表示背包容量为0时,不管有多少物品,最大价值都是0。
时间复杂度:
- 由于
dp数组的大小是(n+1) * (C+1),其中n是物品的数量,C是背包的容量,所以时间复杂度是O(n * C)。
代码实现:
def knapsack(weights, values, C):"""0-1背包问题的动态规划解法:param weights: 物品的重量列表:param values: 物品的价值列表:param C: 背包的最大容量:return: 背包的最大价值"""n = len(weights) # 物品的数量# 创建一个二维dp数组,初始化为0dp = [[0] * (C + 1) for _ in range(n + 1)]# dp[i][w] 表示前i个物品在背包容量为w时的最大价值# 动态规划填充dp数组for i in range(1, n + 1): # 遍历每个物品for w in range(1, C + 1): # 遍历每个可能的背包容量if weights[i - 1] <= w:# 如果当前物品的重量小于等于当前背包容量# 选择放入当前物品或者不放入当前物品dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1])# dp[i - 1][w] 表示不放入当前物品的最大价值# dp[i - 1][w - weights[i - 1]] + values[i - 1] 表示放入当前物品的最大价值else:# 当前物品太重,不能放入背包dp[i][w] = dp[i - 1][w] # 继承上一个物品的最大价值# 返回背包容量为C时的最大价值return dp[n][C]# 示例测试
weights = [2, 3, 4, 5] # 物品的重量
values = [3, 4, 5, 6] # 物品的价值
C = 5 # 背包的最大容量print(knapsack(weights, values, C)) # 输出 7
代码解释:
- 初始化 dp 数组:
dp[i][w]表示前i个物品放入容量为w的背包时的最大价值。dp数组的大小为(n+1) * (C+1),初始化为0。 - 填充 dp 数组:从
i = 1到n,从w = 1到C,对于每一个物品,我们判断是否能将该物品放入当前容量为w的背包。如果能放,则选择放与不放之间的最大值。 - 最终结果:
dp[n][C]即为考虑所有n个物品和容量为C时背包的最大价值。
解释:
假设有 n = 4 个物品,分别为:
- 物品1:重量
2,价值3 - 物品2:重量
3,价值4 - 物品3:重量
4,价值5 - 物品4:重量
5,价值6
背包的容量为 C = 5。
动态规划算法通过逐步选择物品,最终得出背包最大价值为 7,即选择物品1(重量 2,价值 3)和物品2(重量 3,价值 4),总重量为 5,总价值为 7。
其他优化:
- 空间优化:可以使用一维数组优化空间,从
dp[i][w]改为dp[w],每次更新时从右往左更新,避免覆盖还未使用的值。
2. 最长公共子序列(LCS)
最长公共子序列(Longest Common Subsequence, LCS)是指在给定的两个字符串中,找到一个既是这两个字符串的子序列,又是它们的最长的一个子序列。子序列的定义是:从原字符串中删除某些字符(可以不删除任何字符)而不改变其他字符的顺序,得到的新字符串就是子序列。
问题描述:
给定两个字符串 X 和 Y,要求找出它们的最长公共子序列的长度。
动态规划解法:
用一个二维数组 dp[i][j] 来表示前 i 个字符和前 j 个字符的最长公共子序列的长度。
1. 状态定义:
dp[i][j]表示字符串X的前i个字符和字符串Y的前j个字符的最长公共子序列的长度。
2. 状态转移方程:
-
如果
X[i-1] == Y[j-1]:这意味着当前两个字符相同,当前最长公共子序列长度可以通过将X[i-1]或Y[j-1]加入到之前的最长公共子序列中得到。此时:d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i-1][j-1] + 1 dp[i][j]=dp[i−1][j−1]+1
-
如果
X[i-1] != Y[j-1]:当前两个字符不同,意味着我们无法将这两个字符加入到公共子序列中。因此,最大值来自于之前的两种选择(要么不考虑X[i-1],要么不考虑Y[j-1]):d p [ i ] [ j ] = max ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) dp[i][j] = \max(dp[i-1][j], dp[i][j-1]) dp[i][j]=max(dp[i−1][j],dp[i][j−1])
3. 边界条件:
dp[0][j] = 0:当字符串X长度为 0 时,和任何字符串Y的最长公共子序列长度为 0。dp[i][0] = 0:当字符串Y长度为 0 时,和任何字符串X的最长公共子序列长度为 0。
4. 最终结果:
最终的结果,即两个字符串的最长公共子序列的长度,将存储在 dp[len(X)][len(Y)] 中。
动态规划的空间优化:
虽然 dp 数组是二维的,但我们每次计算时仅依赖于前一行的值,因此可以通过滚动数组将空间复杂度从 O(m * n) 降低为 O(n)。
代码实现:
def longestCommonSubsequence(X, Y):"""动态规划求解最长公共子序列(LCS)的长度:param X: 第一个字符串:param Y: 第二个字符串:return: LCS 的长度"""m = len(X) # 第一个字符串的长度n = len(Y) # 第二个字符串的长度# 初始化 dp 数组,dp[i][j] 表示 X[0..i-1] 和 Y[0..j-1] 的 LCS 长度dp = [[0] * (n + 1) for _ in range(m + 1)]# 填充 dp 数组for i in range(1, m + 1): # 遍历第一个字符串的每个字符for j in range(1, n + 1): # 遍历第二个字符串的每个字符if X[i - 1] == Y[j - 1]: # 如果当前字符相同# LCS 长度等于去掉这两个字符后的 LCS 长度加1dp[i][j] = dp[i - 1][j - 1] + 1else: # 如果当前字符不同# LCS 长度等于去掉一个字符后的 LCS 长度的最大值dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])# 返回 LCS 的长度,即 dp[m][n]return dp[m][n]# 示例测试
X = "ABCBDAB"
Y = "BDCAB"
print(longestCommonSubsequence(X, Y)) # 输出 4
代码解析:
- 初始化 dp 数组:
dp[i][j]表示字符串X的前i个字符和字符串Y的前j个字符的最长公共子序列的长度。初始化时,dp[i][0]和dp[0][j]都为 0。 - 状态转移:我们通过嵌套的
for循环遍历所有的i和j,根据X[i-1]和Y[j-1]是否相等来更新dp[i][j]。 - 返回结果:最终的最长公共子序列的长度在
dp[m][n]位置,即X和Y完整字符串的最长公共子序列长度。
示例分析:
假设 X = "ABCBDAB" 和 Y = "BDCAB":
- 比较
X[0]和Y[0]:A和B不同,dp[1][1] = 0。 - 比较
X[0]和Y[1]:A和D不同,dp[1][2] = 0。 - 比较
X[0]和Y[2]:A和C不同,dp[1][3] = 0。 - 比较
X[0]和Y[3]:A和A相同,dp[1][4] = 1。 - 比较
X[1]和Y[0]:B和B相同,dp[2][1] = 1。 - 比较
X[1]和Y[1]:B和D不同,dp[2][2] = 1。
…
最终的 dp 表格会被填充如下(仅显示关键部分):
| B | D | C | A | B | ||
|---|---|---|---|---|---|---|
| A | 0 | 0 | 0 | 0 | 1 | 1 |
| B | 0 | 1 | 1 | 1 | 1 | 2 |
| C | 0 | 1 | 1 | 1 | 1 | 2 |
| B | 0 | 1 | 2 | 2 | 2 | 3 |
| D | 0 | 1 | 2 | 2 | 2 | 3 |
| A | 0 | 1 | 2 | 2 | 3 | 3 |
| B | 0 | 1 | 2 | 2 | 3 | 4 |
最终 dp[7][5] = 4,表示字符串 X = "ABCBDAB" 和 Y = "BDCAB" 的最长公共子序列长度为 4,最长公共子序列为 "BCAB"。
时间和空间复杂度:
- 时间复杂度:
O(m * n),其中m和n分别是两个字符串的长度,因为我们需要遍历整个dp数组。 - 空间复杂度:
O(m * n),我们需要一个m * n的二维数组来存储中间结果。
如果我们需要空间优化,可以通过滚动数组的技巧将空间复杂度降为 O(min(m, n))。
五、动态规划的优化技巧
-
空间优化:当动态规划表格很大时,可以通过优化空间来减少空间复杂度。例如,在计算背包问题时,我们只需要保留上一行的结果,因此可以将二维数组压缩为一维数组。
-
状态压缩:将多维数组压缩成一维数组,尤其是在状态之间仅依赖于前一状态的情况。
六、总结
动态规划是解决具有最优子结构和重叠子问题的优化问题的一种强大工具。通过定义合适的状态和状态转移方程,可以将问题的复杂度从指数级降低到多项式级。掌握动态规划的技巧和方法,将能有效地解决很多经典的计算机算法问题,如背包问题、最长公共子序列、编辑距离等。
相关文章:
详解动态规划算法
动态规划 一、动态规划的核心思想二、动态规划的步骤1. 定义状态(State)2. 确定状态转移方程(State Transition Equation)3. 确定边界条件(Base Case)4. 填表(Table Filling)或递归计…...
DTO 命名规范指南
在项目实践中,将查询对象和返回对象都使用 DTO 后缀是可以的,但通常有更清晰的命名规范,帮助区分两者的作用。 🚨 推荐的命名规范 请求数据(查询参数、请求体等) → 使用 Request / Query 后缀返回数据&a…...
C++编写Redis客户端
目录 安装redis-plus-plus库 编辑 编译Credis客户端 redis的通用命令使用 get/set exists del keys expire /ttl type string类型核心操作 set和get set带有超时时间 set带有NX string带有XX mset mget getrange和setrange incr和decr list类型核心操作…...
数据开发面试: 项目介绍示例
快照表 快照表(Snapshot Table)是数据仓库中用来存储某一时间点的数据状态的表。这种表通常包含在特定时间点上业务实体的静态数据,它记录了业务在某一特定时刻的“快照”视图。快照表通常用于存储那些不经常变化的数据,或者即使…...
记录一下Django的密码重置(忘记密码)
一. Django默认的密码重置 1.路由 # url.pyfrom django.contrib.auth import views as auth_viewsurlpatterns [# 密码重置path(password_reset/, auth_views.PasswordResetView.as_view(), namepassword_reset),# 用户输入邮箱后,跳转到此页面path(password_res…...
【运维篇】KubeSphere-02(经验汇总)
一、使用建议 1.对于数据库、对像存储比较重的要不能丢失,有异地存储备份需求的有状态服务,不建议采用k8s进行部署,会导致运维难度更大。 2.对于中间件如redis、MQ、harbor、seata、nacos、zookeeper可采用k8s部署。 3.对于无状态服务tomc…...
MySQL 5.7.40 主从同步配置教程
MySQL 主从同步能有效提升数据冗余备份与负载均衡。下面我将以 MySQL 5.7.40 版本为例,详细讲解如何进行主从同步配置。 MySQL 5.7.40 主从同步配置教程 一、环境准备 假设我们有两台服务器,一台作为主服务器(Master)ÿ…...
Qt:多线程
目录 初识Qt多线程 QThread常用API QThread的使用 Qt中的锁 条件变量和信号量 初识Qt多线程 Qt 多线程 和 Linux 中的线程本质是一个东西 Linux 中学过的 多线程 APl,Linux 系统提供的 pthread 库 Qt 中针对系统提供的线程 API 重新封装了 C11 中,…...
算法系列之广度优先搜索解决妖怪和尚过河问题
在算法学习中,广度优先搜索(BFS)是一种常用的图搜索算法,适用于解决最短路径问题、状态转换问题等。本文将介绍如何利用广度优先搜索解决经典的“妖怪和尚过河问题”。 问题描述 有三个妖怪和三个和尚需要过河。他们只有一条小船…...
详解常用集合和映射中的线程安全问题
1. 前言 在 Java 中,集合和映射是常用的数据结构,它们分为线程安全和线程不安全两类。我们常用的集合包括:ArrayList、HashSet、CopyOnWriteArrayList、CopyOnWriteArraySet。常用的映射包括:HashMap、ConcurrentHashMap、Hashta…...
计算机毕业设计SpringBoot+Vue.js车辆管理系统(源码+文档+PPT+讲解)
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...
【js逆向】iwencai国内某金融网站实战
地址:aHR0cHM6Ly93d3cuaXdlbmNhaS5jb20vdW5pZmllZHdhcC9ob21lL2luZGV4 在搜索框中随便输入关键词 查看请求标头,请求头中有一个特殊的 Hexin-V,它是加密过的;响应数据包中全是明文。搞清楚Hexin-V的值是怎么生成的,这个值和cooki…...
安卓设备root检测与隐藏手段
安卓设备root检测与隐藏手段 引言 安卓设备的root权限为用户提供了深度的系统控制能力,但也可能带来安全风险。因此,许多应用(如银行软件、游戏和流媒体平台)会主动检测设备是否被root,并限制其功能。这种对抗催生了ro…...
【音视频 | AAC】AAC编码库faac介绍、使用步骤、例子代码
😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀 🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C、数据结构、音视频🍭 🤣本文内容🤣&a…...
Unity摄像机跟随物体
功能描述 实现摄像机跟随物体,并使物体始终保持在画面中心位置。 实现步骤 创建脚本:在Unity中创建一个新的C#脚本,命名为CameraFollow。 代码如下: using UnityEngine;public class CameraFollow : MonoBehaviour {public Tran…...
dp_走方格(包含dfs分析,记忆化搜索)
类似题目解析:dp_最长上升子序列(包含dfs分析,记忆化搜索)-CSDN博客 题目链接:2067. 走方格 - AcWing题库 题目图片: 分析题目(dfs) 这个题目说有一个行为n行,列为m列…...
软考 中级软件设计师 考点笔记总结 day01
文章目录 软考1.0上午考点下午考点 软考1.11、数值及其转换2、计算机内数据表示2.1、定点数 - 浮点数2.2、奇偶校验 和 循环冗余校验 (了解)2.3、海明码 (掌握)2.4、机器数 软考1.0 上午考点 软件工程基础知识: 开发模型、设计原则、测试方…...
如何用Kimi生成PPT?秒出PPT更高效!
做PPT是不是总是让你头疼?😩 快速制作出专业的PPT,今天我们要推荐两款超级好用的AI工具——Kimi 和 秒出PPT!我们来看看哪一款更适合你吧!🚀 🥇 Kimi:让PPT制作更轻松 Kimi的生成效…...
K8S学习之基础十八:k8s的灰度发布和金丝雀部署
灰度发布 逐步扩大新版本的发布范围,从少量用户逐步扩展到全体用户。 特点是分阶段发布、持续监控、逐步扩展 适合需要逐步验证和降低风险的更新 金丝雀部署 将新版本先部署到一小部分用户或服务器,观察其表现,再决定是否全面推广。 特点&…...
Java 深度复制对象:从基础到实战
目录 一、深度复制的概念二、实现深度复制的方法1. 使用序列化2. 手动实现深度复制 三、总结 在 Java 编程中,对象的复制是一个常见的需求。然而,简单的复制操作(如直接赋值)只会复制对象的引用,而不是创建一个新的对象…...
【前端】webstorm创建一个导航页面:HTML、CSS 和 JavaScript 的结合
文章目录 前言一、项目结构二、HTML 结构三、CSS 样式四、JavaScript 功能五、现代化风格优化htmlcssjavascript运行效果 总结 前言 在现代网页开发中,一个良好的导航栏是提升用户体验的重要组成部分。在这篇文章中,我将向您展示如何创建一个简单而完整…...
AI编程: 一个案例对比CPU和GPU在深度学习方面的性能差异
背景 字节跳动正式发布中国首个AI原生集成开发环境工具(AI IDE)——AI编程工具Trae国内版。 该工具模型搭载doubao-1.5-pro,支持切换满血版DeepSeek R1&V3, 可以帮助各阶段开发者与AI流畅协作,更快、更高质量地完…...
第11章 web应用程序安全(网络安全防御实战--蓝军武器库)
网络安全防御实战--蓝军武器库是2020年出版的,已经过去3年时间了,最近利用闲暇时间,抓紧吸收,总的来说,第11章开始学习利用web应用程序安全,主要讲信息收集、dns以及burpsuite,现在的资产测绘也…...
MySQL复习笔记
MySQL复习笔记 1.MySQL 1.1什么是数据库 数据库(DB, DataBase) 概念:数据仓库,软件,安装在操作系统(window、linux、mac…)之上 作用:存储数据,管理数据 1.2 数据库分类 关系型数据库&#…...
GitHub上传项目
总结(有基础的话直接执行这几步,就不需要再往下看了): git init 修改git的config文件:添加:[user]:name你的github用户名 email你注册github的用户名 git branch -m master main git remote add origin 你的URL gi…...
自我训练模型:通往未来的必经之路?
摘要 在探讨是否唯有通过自我训练模型才能掌握未来的问题时,文章强调了底层技术的重要性。当前,许多人倾向于关注应用层的便捷性,却忽视了支撑这一切的根本——底层技术。将模型简单视为产品是一种短视行为,长远来看,理…...
qt 操作多个sqlite文件
qt 操作多个sqlite文件 Chapter1 qt 操作多个sqlite文件1. 引入必要的头文件2. 创建并连接多个SQLite数据库3. 代码说明4. 注意事项 Chapter2 qt 多线程操作sqlite多文件1. 引入必要的头文件2. 创建数据库操作的工作线程类3. 在主线程中创建并启动多个工作线程4. 代码说明5. 运…...
【每日学点HarmonyOS Next知识】多继承、swiper容器、事件传递、滚动安全区域、提前加载网络图片
1、HarmonyOS ArkTS如何让一个类可以具备其他多个类的能力? ArkTS如何让一个类可以具备其他多个类的能力,类似于多继承。 接口支持多继承。类不支持,其只支持单继承。 (报错:Classes can only extend a single class…...
DIY Tomcat:手写一个简易Servlet容器
在Java Web开发领域,Tomcat堪称经典,它作为Servlet容器,承载着无数Web应用的运行。今天,我将带大家一同探索如何手写一个简易的Tomcat,深入理解其底层原理。 一、背景知识 在开始之前,我们需要对几个关键…...
如何在Ubuntu上直接编译Apache Doris
以下是在 Ubuntu 22.04 上直接编译 Apache Doris 的完整流程,综合多个版本和环境的最佳实践: 注意:Ubuntu的数据盘VMware默认是20G,编译不够用,给到50G以上吧 一、环境准备 1. 安装系统依赖 # 基础构建工具链 apt i…...
