news 2026/9/10 18:21:20

二维变换矩阵详解:从齐次坐标到Canvas实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
二维变换矩阵详解:从齐次坐标到Canvas实战

二维变换在计算机图形学里属于那种“看起来简单、用起来全是坑”的知识点。很多初学者第一次接触时,觉得不就是平移、旋转、缩放嘛,高中数学都学过。但真到写代码时,会发现旋转方向不对、缩放中心跑到原点去了、复合变换的结果完全不是预期,甚至矩阵乘法的顺序都能把人绕晕。这篇文章我就从图形学课程里“二维变换”这一节出发,把矩阵推导、齐次坐标、复合变换、坐标系切换这些内容完整过一遍,并结合实际代码演示怎么在Canvas里实现一套可复用的变换工具。适合正在学图形学基础、准备考试或者第一次写渲染相关代码的朋友。

1. 换个视角看二维变换:为什么所有书上都在讲矩阵

二维变换的数学基础就是线性代数,但课本不会直接告诉你为什么图形学里非要用矩阵。这里我先给出结论:因为矩阵能同时表达变换和坐标系的切换,而且矩阵乘法天然支持组合。这两个特性决定了几十年来图形学底层一直都在用矩阵,从OpenGL的固定管线到现代GPU的顶点着色器,基本思路没变过。

1.1 线性变换与矩阵的自然对应

如果我们有一个二维点 (x, y),想对它做旋转、缩放、错切这类操作,本质上都是在做线性变换。线性变换有一个重要性质,就是它只依赖点的坐标分量,并且满足两条规则:

  • 加法保持:T(p + q) = T(p) + T(q)
  • 数乘保持:T(kp) = kT(p)

也就是说,只要确定了变换T对坐标轴单位向量 (1,0) 和 (0,1) 的作用效果,这个变换就被唯一确定了。把这两个结果作为列向量拼成一个2x2矩阵,就得到了这个线性变换的矩阵表示。例如绕原点逆时针旋转θ角的变换,单位向量i = (1,0)旋转后变成 (cosθ, sinθ),单位向量j = (0,1)旋转后变成 (-sinθ, cosθ),所以旋转矩阵就是:

[ cosθ -sinθ ] [ sinθ cosθ ]

这一段的推导很关键,因为很多初学者会直接把矩阵背下来,但遇到问"为什么第一列是cosθ而不是sinθ"时就卡住了。实际上你只需要想清楚单位向量旋转后落到哪里,矩阵的每一列就是答案。后面我讲复合变换时,也会反复用到这种"看列向量"的思维方式。

1.2 齐次坐标:为什么非得加一个维

平移变换本身不是线性变换,因为它会把原点移动到新的位置,不满足 T(0,0)=0 这个线性变换的基本条件。如果只用2x2矩阵,平移就只能被表示为向量加法,也就是 p' = Mp + t。这样在组合多个变换时就很麻烦,因为平移和旋转、缩放的表达式形式不统一,没法直接写成纯矩阵乘法。

解决思路就是升维。把二维平面放进三维空间,让原来的点 (x, y) 表示成 (x, y, 1),原来表示方向的向量则用 (x, y, 0)。这样平移就可以写成一个3x3矩阵:

[ 1 0 tx ] [ 0 1 ty ] [ 0 0 1 ]

用这个矩阵去乘 (x, y, 1),得到 (x+tx, y+ty, 1),正好完成平移。同理,旋转和缩放也能写成3x3齐次矩阵,把它们的右下角设为1、其余位置补0即可。有了齐次坐标后,所有二维变换都能统一成 p' = M·p 这种矩阵乘法的形式,复合变换 —— 比如“先旋转再平移” —— 就变成了简单的矩阵乘法,比如 p' = T·R·p。这是一个非常优雅的统一,也是所有正经图形学教材费很大篇幅讲齐次坐标的原因。

1.3 矩阵乘法顺序:行向量还是列向量

写代码的时候最容易踩的坑就是矩阵乘法的方向。数学教材通常默认列向量,也就是先进行的变换写在左边,后进行的变换写在右边,比如先旋转R再平移T,最终矩阵是 T·R,点在右边:p' = T·R·p。但有些图形库和数学库习惯用行向量,比如DirectX早期的固定管线和某些着色器代码,这时候点写在矩阵左边:p' = p·R·T,矩阵乘法的顺序看起来刚好反过来,实际效果却是完全一样的。

如果你在写自己的数学库,建议一开始就统一采用列向量习惯,也就是矩阵左乘点。因为主流图形学资料、OpenGL、GLM、Three.js的数学模块基本都是这个约定。确认一个库使用的约定,最简单的方法是查看文档里矩阵构造函数的参数顺序,或者在源码里找 translate、rotate 这类方法的内部实现。我在调试时也习惯写一个极小的测试:对 (0,0) 点应用“先平移(1,0)再旋转90°”,如果结果是 (0,1) 还是 (-1,0),就能立刻判断出库的约定。

2. 四类基础变换的矩阵推导与直观理解

基础变换就四种:平移、旋转、缩放、错切。虽然有些教程把反射单列,但反射本质上可以看成缩放的一种极端情况,某轴缩放因子为-1。下面我把每个变换的矩阵、几何意义、以及推导过程都过一遍。

2.1 平移

平移矩阵已经在上面给出了。它的作用就是把坐标系原点移动到 (tx, ty),本质上是对齐次坐标第三维的修改。在代码里,平移通常非常高频,比如你想把物体挪到画布中央,或者实现键盘控制移动。

平移矩阵里唯一的变量是 tx 和 ty。注意平移与旋转、缩放有一个重要区别:平移不会改变一个向量的方向。在齐次坐标里,方向向量 (x, y, 0) 与平移矩阵相乘后,(x, y) 仍然不变,第三个分量0也保持不变。这个特性在后面讲“向量与点的区别”时会再次用到。

2.2 旋转

旋转矩阵推导前面讲过,我再补充一个生活化类比。想象平面里放了一个点,它到原点的距离是 r,与x轴夹角为α,于是坐标是 (r cosα, r sinα)。把它绕原点逆时针旋转θ角,新的夹角变成 α+θ,用两角和差公式展开后就能得到经典的旋转公式:

x' = x cosθ - y sinθ y' = x sinθ + y cosθ

把系数写到矩阵里,就是前面给出的2x2矩阵,升到齐次坐标后在右下角补1。逆时针在数学坐标(y轴向上)里是正方向,但在很多图形API里屏幕坐标的y轴是向下的,所以视觉上正角度旋转变成了顺时针。这个方向问题我会在后面的常见问题部分专门解释。

2.3 缩放

缩放矩阵是:

[ sx 0 0 ] [ 0 sy 0 ] [ 0 0 1 ]

直接乘上点坐标就能得到 (sx·x, sy·y)。当 sx=sy 时是等比缩放,图像形状不变;不相等时则会把图形拉伸变形。一个特别容易忽略的细节是:浮点数的缩放因子可能导致坐标值逐渐远离原始精度。比如反复缩放一个物体,每次乘以1.1,经过几十次以后,坐标的小数误差会被放大,表现就是物体轻微漂移或抖动。工业界的做法是保留物体的原始模型坐标,每次从原始值重新计算变换后的坐标,而不是在上一轮结果上继续变换。

缩放还有负值的情况:sx=-1, sy=1 实际上是沿y轴做镜像。这也是反射变换的本质。在制作角色动画时,常利用负缩放做左右翻转,但要注意这会导致三角形绕序改变,影响背面剔除和光照法线方向。

2.4 错切

错切矩阵稍微冷门一点,但在图形学中其实很常见。水平错切的形式是:

[ 1 shx 0 ] [ 0 1 0 ] [ 0 0 1 ]

它表示横坐标根据纵坐标做线性移动:x' = x + shx·y,y不变。你可以想象一堆纸牌,每张纸牌按高度平移不同的距离。错切用在模拟倾斜字体、地形编辑、以及一些特殊的动画过渡效果中。它虽然不如旋转和缩放常用,但一旦遇到需要“扭动”形状的场景,会非常方便。比如在2D游戏里做一个被风吹动的草丛,底层叶片每帧做微小的水平错切,效果比直接整体旋转更自然。

3. 复合变换的核心:先做还是后做,结果天差地别

单个变换没什么挑战,难点在于组合多个变换。而组合的关键就是矩阵乘法的顺序。这一节我会讲清楚为什么要严格注意顺序,以及如何推导出复合矩阵。

3.1 矩阵乘法没有交换律:一个场景感受一下

假设你想让一个正方形先绕着原点旋转45°,然后平移到 (100, 0)。如果你先平移后旋转,即 T·R 与 R·T 是两个完全不同的结果。我们看具体的数学表达:

  • 先旋转后平移:p' = T·R·p。这个过程中点先围绕原点转,然后再整体移动,符合“原地旋转后再移动”的预期。
  • 先平移后旋转:p' = R·T·p。点先到新位置,再围绕原点旋转,结果就是整个图形不仅移动了,还绕原点甩了一圈。

这种差异非常直观。实际建模时,绕物体中心旋转后再定位到场景中,与先定位再围绕世界原点旋转,两者语义完全不同。所以矩阵乘法的顺序是图形学里反复强调的基础。

3.2 绕任意点旋转的矩阵推导

我们经常遇到的需求不是绕原点旋转,而是绕物体中心或某个指定点 (cx, cy) 旋转。标准解法分三步:

  1. 先把坐标系平移到指定点,即平移矩阵 T(-cx, -cy)。
  2. 绕原点旋转,即旋转矩阵 R(θ)。
  3. 把坐标系移回去,即平移矩阵 T(cx, cy)。

最终矩阵是:

M = T(cx, cy) · R(θ) · T(-cx, -cy)

注意顺序:先写的是最后一步。实际应用时,如果图形库里的矩阵乘法函数是左乘顺序,就按这个顺序从右往左理解。很多同学会问“为什么不能直接对矩形中心做旋转”。因为旋转矩阵的定义默认围绕原点,要让旋转中心变成任意点,就需要先把所有坐标减去中心,让中心临时成为原点,旋转后再加回去。这也是复合变换中最经典的三段式思路:先平移到原点,做旋转变换,再平移回去。这个思路不仅适用于旋转,还适用于缩放、错切,甚至任意顺序的复合变换。

3.3 一个实际例子:绕中心旋转并缩放

假设画布上有一张100x100的图片,中心在 (200, 150),你想让它同时绕中心旋转30°并按1.5倍缩放。如果直接写一个旋转矩阵和一个缩放矩阵相乘,默认都是绕原点。正确的复合矩阵是:

M = T(200, 150) · R(30°) · S(1.5, 1.5) · T(-200, -150)

注意这里旋转和缩放都在原点完成,最后再平移到中心。缩放与旋转的顺序根据需求决定,一般是先缩放后旋转,这样物体形状在自己坐标系里缩放,再整体旋转。假如先旋转后缩放,旋转后的形状会被再次拉伸,产生斜向的变形,视觉效果可能不符合预期。

我在实际代码里遇到的一个高频错误是:把 T(-cx,-cy)、R、S、T(cx,cy) 四个矩阵相乘时,代码写反了乘法顺序。所以一个建议是封装辅助函数,比如rotateAroundPoint(x, y, angle),内部强制规定三段式顺序。这样能避免在每次调用时都去纠结矩阵顺序。

4. 实操:用Canvas实现一个简单的二维变换库

理论讲完了,现在动手写代码。我以浏览器里的Canvas 2D为例,手写一个Matrix3类,内置平移、旋转、缩放方法,并实现一个简单的坐标变换。Canvas原生API其实也有setTransformtransformtranslaterotatescale这些方法,但自己实现一遍更容易理解和排查,也方便后续和WebGL、CSS transform做对照。

4.1 矩阵类的设计

我会用一个长度为9的一维数组存3x3矩阵,按列主序存储,和OpenGL习惯保持一致。列主序的意思就是数组里第0、1、2个元素是第一列,第3、4、5个是第二列,第6、7、8个是第三列。这个存储方式在与shader或WebGL交互时可以直接用,减少转换。

class Mat3 { constructor(...args) { if (args.length === 9) { this.m = new Float32Array(args); } else { this.m = new Float32Array([1,0,0, 0,1,0, 0,0,1]); } } multiply(other) { const a = this.m; const b = other.m; const out = new Float32Array(9); for (let col = 0; col < 3; col++) { for (let row = 0; row < 3; row++) { let sum = 0; for (let k = 0; k < 3; k++) { sum += a[k * 3 + row] * b[col * 3 + k]; } out[col * 3 + row] = sum; } } return new Mat3(...out); } static translation(tx, ty) { return new Mat3(1,0,0, 0,1,0, tx,ty,1); } static rotation(angle) { const c = Math.cos(angle); const s = Math.sin(angle); return new Mat3(c, s, 0, -s, c, 0, 0, 0, 1); } static scaling(sx, sy) { return new Mat3(sx, 0, 0, 0, sy, 0, 0, 0, 1); } applyToPoint(x, y) { const m = this.m; const w = m[2] * x + m[5] * y + m[8]; return { x: (m[0] * x + m[3] * y + m[6]) / w, y: (m[1] * x + m[4] * y + m[7]) / w }; } }

这段代码里有几个细节值得注意:

  • 数组里第8个元素是右下角,传参时我把它写成1,表示齐次坐标的w分量。applyToPoint里计算了w,虽然是二维变换,w通常恒为1,但保留除法可以应对某些特殊矩阵,也让代码更容易移植到三维。
  • multiply的循环顺序要小心,我写成了a[k * 3 + row] * b[col * 3 + k],这里 a 是左矩阵,b 是右矩阵。如果搞反索引,结果矩阵就会错,调试时非常隐蔽。
  • Float32Array的好处是内存紧凑、计算快,比普通数组更贴近GPU数据布局。在做大量点时,性能差异会很明显。

4.2 用变换矩阵绘制图形

光有矩阵还不够,还要把它应用到画布上。Canvas 2D提供了ctx.setTransform(a, b, c, d, e, f),其中 a、b、c、d、e、f 对应矩阵的前两行或前两列,具体取决于你的存储方式。由于我使用列主序,映射关系是:

a = m[0], b = m[1], c = m[3], d = m[4], e = m[6], f = m[7]

下面这个函数把刚才的Mat3直接设置到Canvas上:

function applyMat3(ctx, mat) { const m = mat.m; ctx.setTransform(m[0], m[1], m[3], m[4], m[6], m[7]); }

设置好矩阵后,后续的fillRectarcmoveTo等绘图命令就会自动使用这个变换。比如:

const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); // 先画一个原始矩形 ctx.fillStyle = 'rgba(255,0,0,0.3)'; ctx.fillRect(50, 50, 100, 80); // 构造复合变换:绕矩形中心旋转30度并放大1.5倍 const centerX = 100, centerY = 90; let mat = Mat3.translation(centerX, centerY); mat = mat.multiply(Mat3.rotation(Math.PI / 6)); mat = mat.multiply(Mat3.scaling(1.5, 1.5)); mat = mat.multiply(Mat3.translation(-centerX, -centerY)); applyMat3(ctx, mat); ctx.fillStyle = 'rgba(0,0,255,0.3)'; ctx.fillRect(50, 50, 100, 80); ctx.setTransform(1, 0, 0, 1, 0, 0);

这里先画红色矩形,再画经过变换的蓝色矩形。重点看两个矩形的位置关系:蓝色矩形应该以 (100, 90) 为中心旋转放大,并且不会偏离原来的中心。如果代码出错,一个典型症状就是蓝色矩形跑到了画布左上角,那通常是平移矩阵或者中心计算出了问题。

4.3 物体坐标系与世界坐标系的转换

这个例子背后其实隐含了图形学里特别重要的概念:物体坐标系与世界坐标系。矩形在定义时是“在原点附近画的”,通过矩阵变换把它放到世界坐标系统里的某个位置。也就是说,绘制代码不需要改变,只需要改变变换矩阵就能让物体出现在不同地方、不同姿态。这在实际开发里价值巨大,例如同一份几何数据可以派生出多个实例,各自拥有独立矩阵,这在骨骼动画、粒子系统、批量渲染中都是基础思想。

我建议初学者在画布上做一个交互实验:通过滑动条调节旋转角度、缩放系数和平移量,实时观察图形变化。这个实验能帮你建立“矩阵改变 → 图像变化”的直觉。

4.4 用矩阵栈管理多个物体

在复杂场景里,往往需要管理多个物体,每个物体有自己的局部变换,同时物体还有父子关系,比如汽车的车身和车轮。这时矩阵栈就登场了。Canvas原生提供了save()restore()来保存和恢复状态,但如果你想用自己的矩阵实现,思路是维护一个数组:

const matrixStack = []; function pushMatrix(m) { matrixStack.push(m); } function popMatrix() { return matrixStack.pop(); }

画父子关系物体的逻辑是:先push父物体的矩阵,乘上子物体的局部变换,再绘制子物体,最后pop。这样子在子物体内部写的坐标是相对父物体的,修改父物体的变换即可整体移动子物体。这和三维场景里骨骼动画的层级思路一模一样。

5. 点、向量的不同变换规则与坐标系切换

二维变换不只是操作点,有时还要变换方向向量,比如法线向量、速度方向。在2D里,方向向量用齐次坐标表示为 (x, y, 0)。因为第三个分量为0,所以平移矩阵不会影响它,只有旋转、缩放、错切这类线性变换才起作用。这种区分很关键,例如在图形学光照计算里,面法线通常不能直接使用顶点的变换矩阵,否则缩放不均等时法线方向会错误。

5.1 法线向量的变换

假设一个三角形经过非等比缩放,顶点都在新位置,但如果你直接把缩放矩阵应用到原始法线上,得到的新法线往往不再与缩放后的平面垂直。这时正确的做法是使用原来变换矩阵的逆转置矩阵。在二维里,如果变换矩阵是 M,那么法线应该用 (M^{-1})^T 来变换。很多教材在三维部分讲这个问题,但二维同样存在。

用一个简单的例子验证:原始法线为 (1, 0),缩放矩阵为 S(2, 1)。直接把 S 作用到法线得到 (2, 0),方向仍然是水平方向。但原始平面的切线是 (0,1) 方向,缩放后切线是 (0,1),仍然垂直?这里例子恰好碰巧,因为缩放是各轴独立,且法线方向与缩放轴对齐。换一个斜方向法线,问题就会暴露。实际操作中,如果只做旋转和平移,法线可以直接乘变换矩阵;一旦涉及非等比缩放,就要用逆转置矩阵。

5.2 从世界坐标到屏幕坐标

图形学里另一个常见变换是坐标系切换。比如你用鼠标点击屏幕,想判断点了哪个物体,需要把屏幕坐标转换到世界坐标。在Canvas里,如果已经设置了变换矩阵,把鼠标事件里的clientX/clientY转成画布坐标后,还要乘以当前变换矩阵的逆矩阵,才能得到拾取时的逻辑坐标。

function invertMat3(mat) { const m = mat.m; const a = m[0], b = m[1], c = m[3], d = m[4], e = m[6], f = m[7]; const det = a * d - b * c; if (Math.abs(det) < 1e-12) return null; const invDet = 1 / det; return new Mat3( d * invDet, -b * invDet, 0, -c * invDet, a * invDet, 0, (c * f - d * e) * invDet, (b * e - a * f) * invDet, 1 ); }

这个函数的推导来自2x2矩阵的求逆公式加上平移项的修正。如果你试过鼠标拾取但位置总是偏移,大概率是忘了乘逆矩阵。这类问题调试方法也很简单:画一个固定物体,点击它的中心,看拾取结果是否等于该物体自身的逻辑坐标。

5.3 坐标变换与变换矩阵的同一性

最后强调一个概念:变换矩阵既可以理解为“对点做变换”,也可以理解为“坐标系之间的映射”。这两种说法是等价的。比如旋转矩阵,既可以看作把点绕原点旋转了θ角,也可以看作把坐标系逆时针旋转了θ角。做变换时我用同一个矩阵左乘点,但解释不同。这个双重视角在组合多个坐标系、实现相机系统时非常有用。

6. 常见问题与排查技巧实录

这一节是我写代码和带学生时整理的易错点,每一条都对应过真实报错或者画面异常。

6.1 常见问题速查表

现象原因解决方式
图形旋转方向与预期相反数学坐标y轴向上,屏幕坐标y轴向下使用负角度,或检查绘图API的坐标系定义
图形移动后总是回到原点附近平移矩阵的tx/ty设置错误,或者矩阵存储顺序不对用打印矩阵数值定位错误
绕中心旋转的图形发生漂移先平移到了(0,0),但旋转后没有平移回去确认三段式矩阵乘积顺序:T·R·T_inv
复合变换结果与手算不一致矩阵乘法顺序写反统一使用列向量左乘,并检查multiply参数顺序
非等比缩放后法线方向错误直接用变换矩阵变换法线使用逆转置矩阵
动画中坐标值越来越大到最后出现抖动反复累积变换误差用原始局部坐标重新计算变换结果

6.2 排查技巧

调试矩阵相关代码时,不要靠猜。我惯用的做法是写一个“最小验证用例”:在原点画一个10x10的小方块,用已知意义很强的矩阵去变换它,比如“顺时针旋转90°后再向右平移100”。然后从预期结果推导出数值,再打印实际结果。如果结果对不上,就逐步打印中间矩阵。这种方式比盯着成千上万行代码更高效。

另外,可以使用Canvas的网格辅助线。在画布上画十字坐标轴和间距为50的网格,能非常直观地看到物体经过变换后的位置。我所有关于2D变换的调试都离不开这个网格,它就像数学里的坐标纸,能把抽象矩阵还原成几何直觉。

7. 更深一步:把二维变换推广到三维

二维变换学扎实了,三维其实只是增加一维,而且核心思想完全一样。旋转在三维变成绕某个轴旋转,平移、缩放、错切也都有对应的4x4齐次矩阵。Unity、Unreal、OpenGL的模型矩阵本质就是这些扩展。所以二维变换不只是二维绘图的工具,更是理解三维图形学的敲门砖。

我建议学完这篇文章内容后,自己动手做一次推广练习:把Mat3改成Mat4,用同样的方法实现绕任意轴旋转,然后画一个简单的三维线框立方体。这个过程会让你对矩阵的理解上一个台阶。

最后再分享一个我在实际项目中养成的习惯:永远在代码里加上一个“单位矩阵”测试。也就是一开始先画一个未经任何变换的图形,确认它在预期位置;然后再逐级添加变换,每添加一个变换就确认一次结果。这样就算出了问题,也知道是哪一步引入的。很多人一上来就把所有变换堆在一起,结果画面完全乱了,根本不知道哪里错了,这种教训我见过太多次了。一步一步来,才是做图形学最靠谱的方式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 18:20:10

MoE、推理模型、多模态:三个维度读懂大模型选型

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

作者头像 李华
网站建设 2026/9/10 18:16:04

西门子S7-200 SMART与威纶通HMI在恒压供水系统中的应用

1. 西门子S7-200 SMART与威纶通HMI的工业组合解析 在工业自动化领域&#xff0c;西门子S7-200 SMART系列PLC&#xff08;如224XP型号&#xff09;与威纶通TK6071触摸屏的组合堪称经典配置。这套系统特别适合中小型自动化项目&#xff0c;其中恒压供水系统就是典型应用场景之一。…

作者头像 李华
网站建设 2026/9/10 18:14:17

SpringBoot汽车租赁系统毕设:业务梳理、数据库设计与答辩全解析

又到了毕设产出集中的节点&#xff0c;论坛和社群里隔三差五就有人问&#xff1a;汽车租赁系统的 SpringBoot 毕设源码&#xff0c;有没有现成的能跑起来直接交&#xff1f;我的回答一直是&#xff1a;能跑起来的源码到处都是&#xff0c;能扛住评阅老师追问的源码才是真的值钱…

作者头像 李华
网站建设 2026/9/10 18:13:33

深度优先搜索(DFS)在全排列问题中的应用与实现

1. 全排列问题与深度优先搜索的关系 全排列问题是计算机科学中一个经典的基础算法问题&#xff0c;它要求给定一组不重复的元素&#xff0c;输出所有可能的排列组合。比如对于[1,2,3]&#xff0c;它的全排列包括[1,2,3]、[1,3,2]、[2,1,3]、[2,3,1]、[3,1,2]、[3,2,1]这6种情况…

作者头像 李华
网站建设 2026/9/10 18:09:51

JAVA计算机毕设之基于 SpringBoot+Vue 的面向高校的课程质量评估系统的设计与实现 基于 SpringBoot 与 Vue 的课程评价管理(完整前后端代码+说明文档+LW,调试定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华