news 2026/9/11 21:54:45

【LibreCAD 2D架构】从图形坐标到屏幕坐标:RS_GraphicView 的坐标变换与视图控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【LibreCAD 2D架构】从图形坐标到屏幕坐标:RS_GraphicView 的坐标变换与视图控制


🫧 励志不掉头发的内向程序员:个人主页

✨️ 个人专栏: 《C++语言》《Linux学习》
🌅偶尔悲伤,偶尔被幸福所完善


👓️博主简介:


@[TOC](文章目录)

前言

回顾上一篇结尾:

paintEvent → GraphicView 刷新 → 遍历实体 → 调用 draw() → RS_Painter → QPainter。

但是有的人可能回有问题了:RS_Line 的起点是 (10, 20),屏幕上画出来的却是 (250, 180)。这个差异是怎么来的?所以本篇我们要回答三个问题:

  1. 谁负责把 CAD 坐标变成屏幕坐标?
  2. 用的什么公式?
  3. 缩放、平移又是如何改变这套映射的?


一、两套坐标系:CAD 世界坐标 vs 屏幕坐标

1.1、CAD 坐标(世界坐标)

类型:`RS_Vector`,成员是 `double`。 方向:数学坐标系,Y 轴向上。 单位:图纸单位(mm / inch 等,由图纸决定)。 原点:由图纸内容决定,不一定在视口中心。

在 LibreCAD 里,一条直线、一个圆到底画在图纸的什么位置,是用 CAD 坐标描述的。这套坐标最典型的表现就是 librecad/src/lib/engine/rs_vector.h 里的 RS_Vector:

classRS_Vector{public:doublex=0.;doubley=0.;doublez=0.;boolvalid=false;};

它有几个明显特征:

用 double 存储,是一套连续、精确的几何坐标,而不是离散的像素。 单位是图纸单位,比如毫米或英寸,具体由图纸决定。 Y 轴向上,符合我们学数学时的习惯。 原点不是屏幕左上角,而是图纸坐标系的原点,可能在绘图区中间,也可能在很远的地方。

比如第 7 篇里创建的直线,就保存了两个 CAD 坐标:

startPoint = (10, 20) endPoint = (100, 80)

这两个数字描述的是“这条直线在图纸里的几何位置”。


1.2、屏幕坐标

类型:`QWidget` / `QPainter` 使用的像素坐标,通常是 `int`。 方向:屏幕坐标系,Y 轴向下。 原点:窗口左上角 `(0, 0)`。

但用户最终看到的是窗口,是屏幕上的一堆像素。Qt 在真正绘制时,QPainter 使用的是屏幕坐标:

单位:像素,通常是 int。 原点:窗口左上角 (0, 0)。 X 轴:向右增大。 Y 轴:向下增大。

也就是说,屏幕坐标的 Y 方向和 CAD 坐标是相反的。同一个几何点,到了屏幕上就变成了另一种表达:

CAD 坐标 (10, 20) 屏幕坐标 (250, 180)

1.3、同一个点的两种表达

CAD 坐标:(10, 20) 屏幕坐标:(250, 180) 两者不是简单加一个常量就能互相转换的。

为了直观对比,可以这样理解:

CAD世界坐标屏幕坐标
数据类型RS_Vector,double像素,通常 int
原点图纸原点窗口左上角
Y 轴方向向上向下
单位图纸单位像素
是否随缩放变化不变化会变化

注意最后一行的关键点:同一条直线,CAD 坐标始终是 (10,20) -> (100,80),不会因为你滚轮缩放而改变;但它在屏幕上的像素位置会变。这说明 CAD 坐标是“图形的本质”,屏幕坐标只是“图形的某一次显示”。


1.4、为什么不能直接画

单位不同(图纸单位 vs 像素)。 方向不同(Y 轴一个向上一个向下)。 还受缩放比例、平移位置、窗口大小共同影响。

既然直线已经存了 (10,20) 和 (100,80),能不能直接把这几个数当成屏幕像素传给 QPainter?
不能。原因有三个:

单位不一样:一个是图纸单位,一个是像素。 方向不一样:CAD 的 Y 轴向上,屏幕的 Y 轴向下。 还要叠加缩放和平移:用户缩放后,1 个图纸单位可能对应 2 个像素,也可能对应 0.5 个像素; 平移后,图纸原点也会偏离屏幕原点。

所以中间一定需要一个“翻译官”,负责把 CAD 坐标翻译成屏幕坐标;反过来,也要把鼠标点击的屏幕坐标翻译回 CAD 坐标。

在 LibreCAD 里,这个翻译官就是 RS_GraphicView,它保存着一组视图参数,并提供了 toGui 和 toGraph 两套方法来完成双向变换。下一节我们就来看这套映射具体是怎么实现的。



二、RS_GraphicView:视图就是一台“坐标转换器”

2.1、RS_GraphicView 的角色

它是视图层的抽象基类,继承 `QWidget`。 不是某个具体 Qt 控件,而是给 `QC_GraphicView` 这类真实控件提供公共视图能力。 核心职责:保存“当前视图状态”,并把坐标在两个坐标系之间来回转换。

在 LibreCAD 里,负责管理视图状态、完成坐标变换的核心类就是 RS_GraphicView,定义在 rs_graphicview.h 里。

classRS_GraphicView:publicQWidget{Q_OBJECT...};

需要注意的是,RS_GraphicView 并不是某个具体的 Qt 控件。它更像一个“视图层的公共接口”:凡是跟 CAD 绘图区有关、又和具体窗口实现无关的能力,都放在这个基类里,真正负责创建窗口、响应重绘的,是继承它的 QC_GraphicView 这类具体控件。

所以我们可以先把它抽象成一句话:RS_GraphicView 负责记住“当前图纸是怎么显示在窗口上的”,并提供 CAD 坐标和屏幕坐标之间的双向转换。


2.2、三个核心成员

rs_graphicview.h

RS_Vector factor=RS_Vector(1.,1.);intoffsetX=0;intoffsetY=0;

它记住的这套“显示状态”,其实只靠三个变量就能描述:

RS_Vector factor=RS_Vector(1.,1.);intoffsetX=0;intoffsetY=0;

在这个代码中:

factor:缩放系数。 offsetX:水平平移量。 offsetY:垂直平移量。

这三个量合在一起,就决定了一个 CAD 点会被画到屏幕的哪个像素上。


2.3、factor 是什么

factor:缩放系数,含义是“每个图纸单位对应多少个屏幕像素”。 factor.x 和 factor.y 可以不同,用于非等比显示。

factor 的含义是“每个图纸单位对应多少个屏幕像素”。比如:

factor = (2, 2)

就表示 1 个图纸单位在屏幕上画成 2 个像素,这时图形看起来是放大的。

factor=(0.5,0.5)

则表示 1 个图纸单位只占 0.5 个像素,图形看起来是缩小的。它之所以是一个 RS_Vector,而不是一个 double,是因为 LibreCAD 允许 X 和 Y 两个方向使用不同的缩放比例:

factor.x=2factor.y=1

这种非等比显示,在打印预览或某些特殊视图下会用到。


2.4、offset 是什么

offsetX / offsetY:平移偏移量。 可以理解为“CAD 原点映射到屏幕后,相对屏幕原点偏移了多少像素”。

offsetX 和 offsetY 是平移量,可以粗略理解成 CAD 原点 (0, 0) 映射到屏幕后,相对屏幕原点偏移了多少像素。用户按住鼠标拖动画布时,图形本身没有动,变的就是这两个 offset。它决定了“图纸的哪个位置正好显示在窗口中间”。


2.5、一套线性映射

整体规律:

屏幕坐标 = factor * CAD 坐标 + offset

但 Y 方向还要做一次翻转,这正是下一节要展开的重点。把 factor 和 offset 放在一起,坐标变换的总体规律就是:

屏幕坐标 = factor × CAD 坐标 + offset

但是有一个细节不能直接照搬公式:屏幕的 Y 轴是向下的,CAD 的 Y 轴是向上的。所以 Y 方向不能简单乘完就加,而是要先翻转。



三、正向变换:toGui(CAD → 屏幕)

3.1、核心代码

RS_GraphicView 提供了一组 toGui 方法,负责把 CAD 坐标转换成屏幕坐标。核心实现位于 rs_graphicview.cpp:

RS_VectorRS_GraphicView::toGui(RS_Vector v)const{returnRS_Vector(toGuiX(v.x),toGuiY(v.y));}doubleRS_GraphicView::toGuiX(doublex)const{returnx*factor.x+offsetX;}doubleRS_GraphicView::toGuiY(doubley)const{return-y*factor.y+getHeight()-offsetY;}doubleRS_GraphicView::toGuiDX(doubled)const{returnd*factor.x;}doubleRS_GraphicView::toGuiDY(doubled)const{returnd*factor.y;}

先看最直观的 X 方向:

toGuiX(x)=x*factor.x+offsetX

它完全符合我们上一节说的规律:先按缩放系数放大,再加上平移量。


3.2、为什么 toGuiY 有负号

CAD 中 Y 轴向上,屏幕中 Y 轴向下。 乘以 -factor.y 就是完成 Y 轴翻转。

再看 Y 方向:

toGuiY(y)=-y*factor.y+getHeight()-offsetY

这里的 -y 是整个公式最关键的细节。

CAD 坐标中,Y 越大表示越靠上。 屏幕坐标中,Y 越大表示越靠下。

两个方向正好相反,所以要乘一个负号,把 CAD 的“向上增长”翻转成屏幕的“向下增长”。


3.3、为什么还要加 getHeight()

屏幕原点在左上角,CAD 原点通常在图纸左下方向。 加上 getHeight() 是为了把“向上增长”的 CAD 坐标换算成“向下增长”的屏幕坐标。

如果只有 -y * factor.y,得到的是一个“向下为正”的相对量,但还没有确定这个点落在窗口的哪个高度上。
屏幕原点在左上角,而 CAD 原点通常对应图纸的左下方向。为了把“以图纸原点为基准的位置”换算成“以窗口左上角为基准的位置”,就需要加上窗口高度 getHeight()。

所以 toGuiY 完整地做了三件事:

先乘以 factor.y:完成单位换算和缩放 再取负号:完成 Y 轴方向翻转 最后加 getHeight() 并减 offsetY:完成原点对齐和平移

3.4、距离变换为什么没有 offset

toGuiDX / toGuiDY 只乘 factor,不加 offset。 因为距离是两个坐标的差值,平移量会被抵消。

toGuiDX 和 toGuiDY 只乘 factor,没有加 offset:

toGuiDX(d)=d*factor.xtoGuiDY(d)=d*factor.y

原因在于,这两个函数转换的是“距离”,而不是“位置”。距离是两个坐标的差值。比如两个屏幕点的 X 差是:

(x2*factor.x+offsetX)-(x1*factor.x+offsetX)=(x2-x1)*factor.x

两个 offsetX 一减就抵消了,所以距离变换只关心缩放,不关心平移。



四、反向变换:toGraph(屏幕 → CAD)

4.1、什么时候需要反向变换

用户点击绘图区时,QMouseEvent给的是屏幕像素。但画线、捕捉、框选都需要 CAD 坐标。所以鼠标事件进入 Action 后,必须先做一次屏幕 → 图形的转换。

前面讲的 toGui,解决的是“CAD 图形怎么显示到屏幕上”。但在绘图过程中,还有一个完全相反的方向:
用户在窗口里点击了一下鼠标,程序拿到的只是一个屏幕像素点,怎么知道这个点对应图纸里的哪个 CAD 坐标?

这就是反向变换要做的事。用户画线时,第一次点击的 QMouseEvent 里只有类似 (120, 510) 这样的屏幕坐标;如果不把它转回 CAD 坐标,RS_ActionDrawLine 就没法记录真正的起点和终点。


4.2、核心代码

RS_GraphicView 提供的反向变换方法叫 toGraph,实现同样在 rs_graphicview.cpp:

RS_VectorRS_GraphicView::toGraph(intx,inty)const{returnRS_Vector(toGraphX(x),toGraphY(y));}doubleRS_GraphicView::toGraphX(intx)const{return(x-offsetX)/factor.x;}doubleRS_GraphicView::toGraphY(inty)const{return-(y-getHeight()+offsetY)/factor.y;}

先看 X 方向:

toGraphX(x)=(x-offsetX)/factor.x

它正好是 toGuiX 的逆过程:先减去平移量,再除以缩放系数。


4.3、和正向互为逆运算

toGui:乘 factor、加 offset。 toGraph:减 offset、除以 factor。

两者正好相反。把正向和反向放在一起对比,规律非常清楚:

正向:屏幕X = CAD_X * factor.x + offsetX 反向:CAD_X = (屏幕X - offsetX) / factor.x

Y 方向同理,只是多了一个负号翻转:

正向:屏幕Y = -CAD_Y * factor.y + getHeight() - offsetY 反向:CAD_Y = -(屏幕Y - getHeight() + offsetY) / factor.y

可以用上一节的小算例验证。已知:

factor.x = 2, factor.y = 2 offsetX = 100, offsetY = 50 getHeight() = 600

正向把 CAD (10, 20) 转成了屏幕 (120, 510)。现在把这个屏幕点反向转回去:

toGraphX(120) = (120 - 100) / 2 = 10 toGraphY(510) = -(510 - 600 + 50) / 2 = 20

又回到了 CAD (10, 20)。这就是“互为逆运算”的含义。



五、缩放:本质是修改 factor 和 offset

5.1、zoomIn 的实现思路

zoomIn 的核心设计思路主要是:先取缩放中心,若未指定就用鼠标位置getMousePosition()。核心是调用zoomWindow(),用两个经过缩放的角点重新计算视图参数。最后redraw()

缩放看起来是图形“变大变小”,但从代码层面看,它并没有改动任何一个实体,只是在重新计算 factor 和 offset。rs_graphicview.cpp 里的 zoomIn 核心逻辑是这样的:

voidRS_GraphicView::zoomIn(doublef,constRS_Vector&center){if(f<1.0e-6){RS_DEBUG->print(RS_Debug::D_WARNING,"RS_GraphicView::zoomIn: invalid factor");return;}RS_Vector c=center;if(!c.valid){// 没有指定缩放中心时,取当前鼠标位置c=getMousePosition();}zoomWindow(toGraph(0,0).scale(c,RS_Vector(1.0/f,1.0/f)),toGraph(getWidth(),getHeight()).scale(c,RS_Vector(1.0/f,1.0/f)));redraw();}

可以分成三步理解:

  1. 先确定缩放中心 c,通常是鼠标位置。
  2. 把当前视口的两个角点,围绕中心 c 按 1/f 收缩。
  3. 把收缩后的两个角点交给 zoomWindow,由它反推出新的 factor / offset。

5.2、zoomOut 就是 zoomIn 的倒数

voidRS_GraphicView::zoomOut(doublef,constRS_Vector&center){...zoomIn(1/f,center);}

放大和缩小没有两套独立逻辑。zoomOut 的实现非常短:

voidRS_GraphicView::zoomOut(doublef,constRS_Vector&center){if(f<1.0e-6){...return;}zoomIn(1/f,center);}

缩小 2 倍,本质上就是放大 1/2 倍。这样“放大”和“缩小”就复用了同一套代码。


5.3、zoomWindow:由两个角点反推 factor / offset

关键思路:

zoomX=getWidth()/(v2.x-v1.x)zoomY=getHeight()/(v2.y-v1.y)

然后由zoomX / zoomY反推出新的factoroffset,使指定矩形刚好铺满视口。

真正干活的是 zoomWindow。它的作用可以概括为:给定 CAD 空间里的两个角点 v1、v2,调整 factor / offset,让这个矩形区域正好铺满当前窗口。关键计算在 rs_graphicview.cpp:

zoomX=getWidth()/(v2.x-v1.x);zoomY=getHeight()/(v2.y-v1.y);

然后根据是否保持比例,决定 factor.x 和 factor.y 的取值,再由角点重新算出 offsetX / offsetY,最后 redraw()。这就是“框选放大”的底层原理:你用鼠标框出的两个角点,最终变成了新的 factor 和 offset。


5.4、zoomAuto:自动适应全部图形

先调用container->calculateBorders()计算所有实体的包围盒。再根据包围盒反推factor / offset,实现“全部显示”。“缩放到全图”对应的是 zoomAuto。它的思路是:

if(container){container->calculateBorders();autoconstdV=container->getMax()-container->getMin();sx=std::max(dV.x,0.);sy=std::max(dV.y,0.);fx=(getWidth()-borderLeft-borderRight)/sx;fy=(getHeight()-borderTop-borderBottom)/sy;if(keepAspectRatio){fx=fy=std::min(fx,fy);}setFactorX(fx);setFactorY(fy);centerOffsetX();centerOffsetY();redraw();}

它的计算也很有规律:

  1. 先让容器计算所有实体的包围盒 calculateBorders()。
  2. 用包围盒的宽高 sx / sy,除以窗口可用宽高,得到两个方向的缩放系数。
  3. 保持比例时,取较小的那个系数,保证整个图形不会被裁掉。
  4. 最后居中并重绘。

所以 zoomAuto 就是“根据图形大小反推视图参数”。



六、平移:只改 offset

6.1、zoomPan 的实现

平移比缩放还要简单。缩放要同时调整 factor 和 offset,而平移只动 offset。rs_graphicview.cpp 里的实现非常短:

voidRS_GraphicView::zoomPan(intdx,intdy){offsetX+=dx;offsetY-=dy;adjustOffsetControls();redraw();}

用户按住鼠标拖动时,传入的 dx / dy 就是鼠标移动的距离,zoomPan 把它们累加到 offset 上,然后重绘。


6.2、为什么 offsetY 是减 dx / dy

注意这里有个方向问题:

offsetX+=dx;offsetY-=dy;

X 方向是加,Y 方向却是减。原因还是两套坐标系的 Y 轴方向相反:

  • 鼠标在屏幕上向下拖,dy 是正数。
  • 此时我们希望图纸内容也跟着往下走,也就是 CAD 原点相对屏幕向下移动。
  • 但屏幕 Y 轴向下、CAD Y 轴向上,所以要用减法来抵消这个方向差异。

理解不了细节也没关系,记住结论即可:平移仍然只改 offsetX / offsetY,不会改 factor,更不会动任何实体。


6.3、zoomScroll:键盘/滚轮方向

上下左右滚动只改变offsetX / offsetY。除了鼠标拖动,zoomScroll 也属于平移,用于处理上下左右滚动。它同样只是在改 offset:

voidRS_GraphicView::zoomScroll(RS2::Direction direction){switch(direction){caseRS2::Up:offsetY-=50;break;caseRS2::Down:offsetY+=50;break;caseRS2::Right:offsetX+=50;break;caseRS2::Left:offsetX-=50;break;...}redraw();}

可以看到,每次滚动就是让对应方向的 offset 变化 50 个像素。


6.4、平移同样不动图形数据

平移只改变视图参数,不改变实体坐标。把“缩放”和“平移”放到一起,就能得到一个统一的结论:

操作改动的变量是否修改实体
缩放zoomIn / zoomOut factor + offset
平移zoomPan / zoomScroll 只改 offset

无论是缩放还是平移,最终都只是:

修改视图参数 factor / offset -> 调用 redraw() -> 用新的参数重新执行 toGui ->重新绘制

图形数据始终不变。这就是 LibreCAD 里“数据与视图分离”最直观的体现:实体负责保存几何信息,视图负责决定以什么比例、什么位置把它显示出来。



七、把两条链路完整串起来

7.1、输入链路:屏幕 → 图形

画线时,程序真正走的是这样一条链路:

用户点击鼠标 -> Qt 生成 QMouseEvent(屏幕像素坐标)-> EventHandler 分发给当前 Action -> snapPoint() 获取绘图点 -> toGraph 反向变换 -> 得到 RS_Vector 的 CAD 坐标 -> RS_ActionDrawLine 记录起点 / 终点 -> new RS_Line(…)

这条链路解决的是:用户点在哪里,图纸上的哪个坐标被选中了。


7.2、输出链路:图形 → 屏幕

画完线后,程序要把这条线显示出来,走的是另一条完全相反的链路:

RS_Line(保存 CAD 坐标)-> GraphicView 触发刷新 -> 遍历 Graphic 中的实体 -> 调用 RS_Line::draw() -> view->toGui 正向变换 -> 得到屏幕像素坐标 -> RS_PainterQt::drawLine() -> QPainter 最终输出

这条链路解决的是:图纸上的这条线,应该在窗口的哪些像素上出现。


7.3、关键代码佐证

在 rs_line.cpp 里,RS_Line::draw 就把“正向变换”这一步写得很清楚:

voidRS_Line::draw(RS_Painter*painter,RS_GraphicView*view,double&patternOffset){...RS_Vector pStart{view->toGui(endPoints.at(0))};RS_Vector pEnd{view->toGui(endPoints.at(1))};...painter->drawLine(pStart,pEnd);}

注意这里的两个动作:

  1. view->toGui(…):把 CAD 端点变成屏幕坐标。
  2. painter->drawLine(pStart, pEnd):把屏幕坐标交给绘图系统输出。

7.4、核心认知

串完整条链路后,可以得出本篇最重要的一句话:RS_Line 保存的 CAD 坐标从头到尾都没有变;缩放、平移改变的只是 RS_GraphicView 里的 factor 和 offset。

这正是 LibreCAD 架构优雅的地方:

  • 实体负责“是什么”:RS_Line 保存两个端点。
  • 容器负责“属于谁”:Graphic 管理这些实体。
  • 视图负责“在哪里、何时画”:RS_GraphicView 保存视图参数并决定刷新。
  • 绘图接口负责“怎么画”:RS_Painter / QPainter 完成像素输出。

四个层次各司其职,所以无论怎么缩放、平移,甚至切换打印预览,底层的图形数据都保持稳定,只是视图参数不同而已。



总结

本篇围绕 RS_GraphicView,补上了 LibreCAD 绘图链路里“坐标变换”这一环。从表面上看,用户只是点击鼠标、看到一条线画出来;但从源码看,中间始终绕不开两套坐标系之间的双向转换。
可以提炼成几个要点:

  1. LibreCAD 中存在两套坐标:CAD 世界坐标用 double 表示、Y 轴向上;屏幕坐标用像素表示、Y 轴向下。
  2. RS_GraphicView 只靠三个变量就定义了整个视图:factor、offsetX、offsetY。
  3. toGui 负责把 CAD 坐标变成屏幕坐标,核心公式是 x * factor + offset,Y 方向还要翻转。
  4. toGraph 负责把鼠标点击的屏幕坐标变回 CAD 坐标,是 toGui 的逆运算。
  5. 缩放同时修改 factor 和 offset,平移只修改 offset,两者都不会改动任何实体。
  6. 整个过程中,RS_Line 保存的几何数据始终不变,变的只是视图参数,最后通过 redraw() 重新渲染。

所以整条链路可以概括为:

鼠标输入 -> toGraph 转成 CAD 坐标 -> Action 创建实体 -> Graphic 保存实体 -> GraphicView 刷新 -> toGui 转成屏幕坐标 -> RS_Painter/QPainter 输出像素

理解这一层之后,LibreCAD 的“数据与视图分离”就不再是一句空话,而是体现在 factor、offset 和 toGui / toGraph 这几个具体实现里。

🎇坚持到这里已经很厉害啦,辛苦啦🎇
ʕ • ᴥ • ʔ
づ♡ど
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 21:54:17

AI短剧六步工作流:一个人从梗概做到成片

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 21:53:30

CMSIS-FreeRTOS深度解析:ARM官方封装的工程逻辑与实战陷阱

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 21:53:23

2025-2026护眼台灯选购指南:硬指标详解、品牌横测与实测避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 21:52:47

二叉搜索树与KV结构的实现与优化实践

1. 二叉搜索树与KV结构基础解析二叉搜索树&#xff08;BST&#xff09;作为数据结构领域的经典之作&#xff0c;本质上是一个维护元素有序性的二叉树结构。每个节点最多拥有两个子节点&#xff0c;且遵循"左小右大"的基本规则——对于任意节点&#xff0c;其左子树所…

作者头像 李华
网站建设 2026/9/11 21:50:47

实验三 抓包协议分析(基于eNSP)

一、实验目的了解TCP/IP协议的协议栈&#xff0c;尤其是数据链路层、网络层和传输层协议的PDU格式。二、实验内容每台电脑的IP地址是不一样的&#xff0c;实验报告请保证原创&#xff0c;谢绝雷同&#xff01;谢绝雷同&#xff01;&#xff01;1、熟悉Wareshark抓包软件的应用。…

作者头像 李华