ASCII 直方图(Histogram)详解

一、程序做了什么

把一堆数据按区间分组统计,然后用 ## 字符在控制台画出柱状图。

输入: N=5, l=0, r=1, 数据: 0.1 0.15 0.3 0.55 0.72 0.91 0.05 0.88
输出:
 ##
 ##
 ##                          ##
 ##          ##      ##      ##
-------- --------  --------  --------  --------
  3        1        1        1        2

二、整体流程

输入 N, l, r

计算区间宽度
width = (r-l)/N

逐个读入数据 val

val 在 [l, r) 内?

算出所在区间 k
counts[k]++

忽略

还有数据?

找最大计数 max_count

逐行从上往下画直方图

打印分隔线和计数

三、第一步:划分区间

对应代码

double width = (r - l) / N;  // 每个区间的宽度

例子:N=5, l=0, r=1

width = 1 − 0 5 = 0.2 \text{width} = \frac{1 - 0}{5} = 0.2 width=510=0.2
产生 5 个区间:

区间编号 k:   0         1         2         3         4
范围:      [0, 0.2)  [0.2, 0.4) [0.4, 0.6) [0.6, 0.8) [0.8, 1.0)
数轴:
0    0.2    0.4    0.6    0.8    1.0
|------|------|------|------|------|
  k=0    k=1    k=2    k=3    k=4

注意最后一个区间是左闭右开 [ 0.8 , 1.0 ) [0.8, 1.0) [0.8,1.0),值恰好等于 r r r 的数据会被忽略。

四、第二步:数据分桶(统计频次)

核心问题

给一个数据 val,如何快速判断它落在第几个区间?

公式

k = ⌊ val − l width ⌋ k = \lfloor \frac{\text{val} - l}{\text{width}} \rfloor k=widthvall
就是算 val 离左端点 l 有多远,除以区间宽度,取整数部分。

对应代码

while (std::cin >> val) {
    if (val >= l && val < r) {           // 先检查是否在总范围内
        int k = (int)((val - l) / width); // 算区间编号
        if (k >= N) k = N - 1;           // 防止浮点精度越界
        counts[k]++;                     // 对应区间计数 +1
    }
}

手算演示(N=5, l=0, r=1, width=0.2)

val=0.10:  k = (int)(0.10 / 0.2) = (int)(0.5)  = 0   --> counts[0]++
val=0.15:  k = (int)(0.15 / 0.2) = (int)(0.75) = 0   --> counts[0]++
val=0.30:  k = (int)(0.30 / 0.2) = (int)(1.5)  = 1   --> counts[1]++
val=0.55:  k = (int)(0.55 / 0.2) = (int)(2.75) = 2   --> counts[2]++
val=0.72:  k = (int)(0.72 / 0.2) = (int)(3.6)  = 3   --> counts[3]++
val=0.91:  k = (int)(0.91 / 0.2) = (int)(4.55) = 4   --> counts[4]++
val=0.05:  k = (int)(0.05 / 0.2) = (int)(0.25) = 0   --> counts[0]++
val=0.88:  k = (int)(0.88 / 0.2) = (int)(4.4)  = 4   --> counts[4]++

最终 counts 数组:

counts[0] = 3    区间 [0.0, 0.2)  有 0.10, 0.15, 0.05
counts[1] = 1    区间 [0.2, 0.4)  有 0.30
counts[2] = 1    区间 [0.4, 0.6)  有 0.55
counts[3] = 1    区间 [0.6, 0.8)  有 0.72
counts[4] = 2    区间 [0.8, 1.0)  有 0.91, 0.88

数据落入区间的过程图

val = 0.10

k = 0

val = 0.15

val = 0.05

counts[0] = 3

val = 0.30

k = 1

counts[1] = 1

val = 0.55

k = 2

counts[2] = 1

val = 0.72

k = 3

counts[3] = 1

val = 0.91

k = 4

val = 0.88

counts[4] = 2

五、为什么要 if (k >= N) k = N - 1

浮点数计算有精度误差。比如:

val = 0.9999999999999999, width = 0.2
(val - l) / width = 4.999999999999999
(int)(4.999999999999999) = 4     --> 正常
但如果精度恰好让结果变成 5.000000000000001:
(int)(5.000000000000001) = 5     --> 越界! 数组只有 0~4

加上 if (k >= N) k = N - 1 就能兜底,把这种边界情况归入最后一个区间。

六、第三步:找最大计数

对应代码

int max_count = *std::max_element(counts.begin(), counts.end());

std::max_element 返回一个迭代器(指针),指向最大元素。前面加 * 取出它的值。

本例中

counts = {3, 1, 1, 1, 2}
max_count = 3

找最大值是为了后面归一化——让最高的柱子刚好占满 10 行。

七、第四步:画直方图(最烧脑的部分)

思路

用文本模拟柱状图,从上往下逐行打印。每一行判断每个柱子在这一行该不该画 ##

对应代码

int bar_height = 10;  // 最高柱子占 10 行
for (int row = bar_height; row >= 1; row--) {       // 从第10行画到第1行
    for (int k = 0; k < N; k++) {                   // 每行遍历所有区间
        int bar = (max_count > 0)
                  ? (counts[k] * bar_height + max_count - 1) / max_count
                  : 0;
        std::cout << (bar >= row ? " ## " : "    "); // 够高就画,不够就空
    }
    std::cout << std::endl;
}

归一化公式详解

int bar = (counts[k] * bar_height + max_count - 1) / max_count;

这是向上取整的整数除法。等价于数学上的:
bar = ⌈ counts [ k ] × bar_height max_count ⌉ \text{bar} = \lceil \frac{\text{counts}[k] \times \text{bar\_height}}{\text{max\_count}} \rceil bar=max_countcounts[k]×bar_height
作用:把 counts[k] 映射到 0~10 的高度。最大的 count 一定映射到 10。

手算本例的 bar 值

bar_height = 10, max_count = 3:

k=0: counts=3  bar = ceil(3*10/3)  = ceil(10)   = 10
k=1: counts=1  bar = ceil(1*10/3)  = ceil(3.33) = 4
k=2: counts=1  bar = ceil(1*10/3)  = ceil(3.33) = 4
k=3: counts=1  bar = ceil(1*10/3)  = ceil(3.33) = 4
k=4: counts=2  bar = ceil(2*10/3)  = ceil(6.67) = 7

用整数公式验证 k=1:

(1 * 10 + 3 - 1) / 3 = 12 / 3 = 4    (整数除法,正确)

逐行绘制过程

bar 值: {10, 4, 4, 4, 7}
判断规则:bar >= row 时画 ##,否则画空格。

row=10:  10>=10? Y   4>=10? N   4>=10? N   4>=10? N   7>=10? N
          ##                                          
row=9:   10>=9?  Y   4>=9?  N   4>=9?  N   4>=9?  N   7>=9?  N
          ##
row=8:   10>=8?  Y   4>=8?  N   4>=8?  N   4>=8?  N   7>=8?  N
          ##
row=7:   10>=7?  Y   4>=7?  N   4>=7?  N   4>=7?  N   7>=7?  Y
          ##                                           ##
row=6:   10>=6?  Y   4>=6?  N   4>=6?  N   4>=6?  N   7>=6?  Y
          ##                                           ##
row=5:   10>=5?  Y   4>=5?  N   4>=5?  N   4>=5?  N   7>=5?  Y
          ##                                           ##
row=4:   10>=4?  Y   4>=4?  Y   4>=4?  Y   4>=4?  Y   7>=4?  Y
          ##      ##      ##      ##      ##
row=3:   10>=3?  Y   4>=3?  Y   4>=3?  Y   4>=3?  Y   7>=3?  Y
          ##      ##      ##      ##      ##
row=2:   10>=2?  Y   4>=2?  Y   4>=2?  Y   4>=2?  Y   7>=2?  Y
          ##      ##      ##      ##      ##
row=1:   10>=1?  Y   4>=1?  Y   4>=1?  Y   4>=1?  Y   7>=1?  Y
          ##      ##      ##      ##      ##

最终效果

 ##                          
 ##                          
 ##                          
 ##                      ##  
 ##                      ##  
 ##                      ##  
 ##      ##      ##      ##  
 ##      ##      ##      ##  
 ##      ##      ##  ##  ##  
 ##      ##      ##  ##  ##  
----  ----  ----  ----  ----
 3    1     1     1     2

八、向上取整的整数除法

为什么不直接用 counts[k] * 10 / 3

C++ 整数除法是向下取整(截断):

普通整数除法(向下取整):
  1 * 10 / 3 = 10 / 3 = 3      (实际 3.33,截断成 3)
向上取整的整数除法:
  (1 * 10 + 3 - 1) / 3 = 12 / 3 = 4    (把 3.33 变成 4)

通用公式

对正整数 a a a b b b
⌈ a b ⌉ = a + b − 1 b (整数除法) \lceil \frac{a}{b} \rceil = \frac{a + b - 1}{b} \quad \text{(整数除法)} ba=ba+b1(整数除法)
为什么成立:

a 能整除 b 时:   a=6, b=3  -->  (6+3-1)/3 = 8/3 = 2   (正确,6/3=2)
a 不能整除 b 时: a=7, b=3  -->  (7+3-1)/3 = 9/3 = 3   (正确,ceil(7/3)=3)
                 a=8, b=3  -->  (8+3-1)/3 = 10/3 = 3   (正确,ceil(8/3)=3)

加上 b − 1 b-1 b1 的效果是:如果有余数,就把商"推"上去一个。

九、打印分隔线和计数

对应代码

// 分隔线
for (int k = 0; k < N; k++) std::cout << "----";
std::cout << std::endl;
// 各区间的原始计数
for (int k = 0; k < N; k++) {
    std::cout << " " << counts[k] << "  ";
}
std::cout << "\n(各区间计数)" << std::endl;

每个区间占 4 个字符宽度(----## 都是 4 字符),保持对齐。

十、从上往下画 vs 从下往上画

为什么要从上往下(row 从大到小)

控制台输出只能从上往下逐行打印,不能回头修改已经打印的行。

row = 10(最高行)

打印第10行:只有最高的柱子会画

row = 9

打印第9行

...

row = 1(最低行)

打印第1行:所有非零柱子都会画

打印分隔线和数字

如果从 row=1 开始画,最底部会先出现,柱子就倒过来了。

类比

想象你在纸上画柱状图:

你实际画的顺序(从下往上叠方块):      程序打印的顺序(从上往下逐行):
  第3步画 -> ##                         第1行打印 -> ##
  第2步画 -> ## ##                      第2行打印 -> ## ##
  第1步画 -> ## ## ##                   第3行打印 -> ## ## ##
             ---------                              ---------

虽然画的方向相反,但最终结果一样。程序通过先判断每行该画什么,再打印来实现。

十一、完整运行示例

输入

5 0 1
0.1
0.15
0.3
0.55
0.72
0.91
0.05
0.88

第一行 5 0 1 是参数(5个区间,范围 0 到 1),后面每行一个数据值。

处理过程

+-------+-------------------+--------+-----------+
| 数据  | (val-l)/width     | k      | 区间       |
+-------+-------------------+--------+-----------+
| 0.10  | 0.10/0.2 = 0.50   | 0      | [0, 0.2)  |
| 0.15  | 0.15/0.2 = 0.75   | 0      | [0, 0.2)  |
| 0.30  | 0.30/0.2 = 1.50   | 1      | [0.2,0.4) |
| 0.55  | 0.55/0.2 = 2.75   | 2      | [0.4,0.6) |
| 0.72  | 0.72/0.2 = 3.60   | 3      | [0.6,0.8) |
| 0.91  | 0.91/0.2 = 4.55   | 4      | [0.8,1.0) |
| 0.05  | 0.05/0.2 = 0.25   | 0      | [0, 0.2)  |
| 0.88  | 0.88/0.2 = 4.40   | 4      | [0.8,1.0) |
+-------+-------------------+--------+-----------+
counts = {3, 1, 1, 1, 2}
max_count = 3
bar    = {10, 4, 4, 4, 7}

输出

===== 直方图 =====
 ##                          
 ##                          
 ##                          
 ##                      ##  
 ##                      ##  
 ##                      ##  
 ##      ##      ##      ##  
 ##      ##      ##      ##  
 ##      ##      ##  ##  ##  
 ##      ##      ##  ##  ##  
----  ----  ----  ----  ----
 3    1     1     1     2
(各区间计数)

十二、Godbolt 上的输入方式

在 Godbolt 中,把所有输入放在 Stdin 框里,一次性粘贴:

5 0 1
0.1
0.15
0.3
0.55
0.72
0.91
0.05
0.88

不需要按 Ctrl+D,Godbolt 会自动在数据末尾发送 EOF。

1.1.33 矩阵库(Matrix Library)

思路理解

线性代数中的基本运算:
向量点积(两个向量对应元素相乘再求和):
dot ( x , y ) = ∑ i x i ⋅ y i \text{dot}(\mathbf{x}, \mathbf{y}) = \sum_{i} x_i \cdot y_i dot(x,y)=ixiyi
矩阵乘法 m × n m \times n m×n 乘以 n × p n \times p n×p m × p m \times p m×p):
C [ i ] [ j ] = ∑ k A [ i ] [ k ] ⋅ B [ k ] [ j ] C[i][j] = \sum_{k} A[i][k] \cdot B[k][j] C[i][j]=kA[i][k]B[k][j]
矩阵转置(行列互换):
A T [ i ] [ j ] = A [ j ] [ i ] A^T[i][j] = A[j][i] AT[i][j]=A[j][i]
矩阵×向量
( b ) i = ∑ j A [ i ] [ j ] ⋅ x j (\mathbf{b})_i = \sum_{j} A[i][j] \cdot x_j (b)i=jA[i][j]xj
向量×矩阵
( b ) j = ∑ i y i ⋅ A [ i ] [ j ] (\mathbf{b})_j = \sum_{i} y_i \cdot A[i][j] (b)j=iyiA[i][j]

C++ 完整代码

#include <iostream>
#include <vector>
#include <cassert>
#include <iomanip>
// 类型别名,让代码更简洁
using Vec = std::vector<double>;
using Mat = std::vector<std::vector<double>>;
// ==============================
// 向量点积:x · y = Σ(x[i] * y[i])
// ==============================
double dot(const Vec& x, const Vec& y) {
    assert(x.size() == y.size());  // 两向量必须等长
    double sum = 0.0;
    for (size_t i = 0; i < x.size(); i++) {
        sum += x[i] * y[i];        // 对应元素相乘累加
    }
    return sum;
}
// ==============================
// 矩阵乘矩阵:C = A × B
// A 是 m×n,B 是 n×p,C 是 m×p
// ==============================
Mat mult_mat_mat(const Mat& A, const Mat& B) {
    size_t m = A.size();           // A 的行数
    size_t n = A[0].size();        // A 的列数(= B 的行数)
    size_t p = B[0].size();        // B 的列数
    assert(B.size() == n);         // 维度匹配检查
    // 初始化结果矩阵 C,全为 0
    Mat C(m, Vec(p, 0.0));
    for (size_t i = 0; i < m; i++) {
        for (size_t j = 0; j < p; j++) {
            for (size_t k = 0; k < n; k++) {
                // C[i][j] += A[i][k] * B[k][j]
                C[i][j] += A[i][k] * B[k][j];
            }
        }
    }
    return C;
}
// ==============================
// 矩阵转置:A^T[i][j] = A[j][i]
// ==============================
Mat transpose(const Mat& A) {
    size_t m = A.size();
    size_t n = A[0].size();
    // 结果是 n×m 的矩阵
    Mat AT(n, Vec(m));
    for (size_t i = 0; i < m; i++) {
        for (size_t j = 0; j < n; j++) {
            AT[j][i] = A[i][j];   // 行列互换
        }
    }
    return AT;
}
// ==============================
// 矩阵×向量:b = A × x
// A 是 m×n,x 是长度 n,b 是长度 m
// ==============================
Vec mult_mat_vec(const Mat& A, const Vec& x) {
    size_t m = A.size();
    size_t n = A[0].size();
    assert(x.size() == n);
    Vec b(m, 0.0);
    for (size_t i = 0; i < m; i++) {
        for (size_t j = 0; j < n; j++) {
            b[i] += A[i][j] * x[j];
        }
    }
    return b;
}
// ==============================
// 向量×矩阵:b = y × A
// y 是长度 m,A 是 m×n,b 是长度 n
// ==============================
Vec mult_vec_mat(const Vec& y, const Mat& A) {
    size_t m = A.size();
    size_t n = A[0].size();
    assert(y.size() == m);
    Vec b(n, 0.0);
    for (size_t j = 0; j < n; j++) {
        for (size_t i = 0; i < m; i++) {
            b[j] += y[i] * A[i][j];
        }
    }
    return b;
}
// 工具函数:打印矩阵
void print_mat(const Mat& M) {
    for (const auto& row : M) {
        for (double v : row)
            std::cout << std::setw(8) << std::fixed << std::setprecision(2) << v;
        std::cout << "\n";
    }
}
// 工具函数:打印向量
void print_vec(const Vec& v) {
    std::cout << "[";
    for (size_t i = 0; i < v.size(); i++) {
        std::cout << std::fixed << std::setprecision(2) << v[i];
        if (i + 1 < v.size()) std::cout << ", ";
    }
    std::cout << "]\n";
}
int main() {
    // === 测试点积 ===
    Vec x = {1, 2, 3};
    Vec y = {4, 5, 6};
    std::cout << "点积 [1,2,3]·[4,5,6] = " << dot(x, y) << "\n";
    // 期望:1*4 + 2*5 + 3*6 = 32
    // === 测试矩阵乘法 ===
    Mat A = {{1, 2}, {3, 4}};
    Mat B = {{5, 6}, {7, 8}};
    std::cout << "\nA × B =\n";
    print_mat(mult_mat_mat(A, B));
    // 期望:[[19,22],[43,50]]
    // === 测试转置 ===
    Mat C = {{1, 2, 3}, {4, 5, 6}};
    std::cout << "\n转置 [[1,2,3],[4,5,6]] =\n";
    print_mat(transpose(C));
    // 期望:[[1,4],[2,5],[3,6]]
    // === 测试矩阵×向量 ===
    Vec v = {1, 2};
    std::cout << "\n[[1,2],[3,4]] × [1,2] = ";
    print_vec(mult_mat_vec(A, v));
    // 期望:[5, 11]
    // === 测试向量×矩阵 ===
    std::cout << "[1,2] × [[1,2],[3,4]] = ";
    print_vec(mult_vec_mat(v, A));
    // 期望:[7, 10]
    return 0;
}

向量与矩阵运算详解

目录

一、向量点积(Dot Product)

定义

两个等长向量,对应元素相乘再求和,结果是一个标量(单个数字)
x ⃗ ⋅ y ⃗ = ∑ i = 0 n − 1 x i ⋅ y i \vec{x} \cdot \vec{y} = \sum_{i=0}^{n-1} x_i \cdot y_i x y =i=0n1xiyi

对应代码

double dot(const Vec& x, const Vec& y) {
    assert(x.size() == y.size());  // 两向量必须等长
    double sum = 0.0;
    for (size_t i = 0; i < x.size(); i++) {
        sum += x[i] * y[i];       // 对应元素相乘累加
    }
    return sum;
}

手算演示

x = [1, 2, 3]
y = [4, 5, 6]
点积 = 1*4 + 2*5 + 3*6
     = 4   + 10  + 18
     = 32

逐步累加过程

+------+------+------+---------+---------+
| i    | x[i] | y[i] | x[i]*y[i] | sum   |
+------+------+------+---------+---------+
| 初始 |  -   |  -   |    -    |   0     |
|  0   |  1   |  4   |    4    |   4     |
|  1   |  2   |  5   |   10    |  14     |
|  2   |  3   |  6   |   18    |  32     |
+------+------+------+---------+---------+
                          结果:    32

ASCII 图示

x:  [ 1 ,  2 ,  3 ]
      |    |    |       对应位置相乘
y:  [ 4 ,  5 ,  6 ]
      |    |    |
      v    v    v
    1*4  2*5  3*6
    = 4  =10  =18
      \    |    /
       \   |   /        全部加起来
        v  v  v
       4+10+18 = 32     --> 一个数字

二、矩阵乘矩阵

定义

A A A m × n m \times n m×n 矩阵, B B B n × p n \times p n×p 矩阵,结果 C C C m × p m \times p m×p 矩阵。
C [ i ] [ j ] = ∑ k = 0 n − 1 A [ i ] [ k ] ⋅ B [ k ] [ j ] C[i][j] = \sum_{k=0}^{n-1} A[i][k] \cdot B[k][j] C[i][j]=k=0n1A[i][k]B[k][j]
一句话理解:C 的第 i 行第 j 列 = A 的第 i 行 点乘 B 的第 j 列

维度匹配规则

A: m × [n]       A 的列数
B: [n] × p       必须等于 B 的行数
C: m × p         结果的大小

n 必须相等

A
m x n

B
n x p

C
m x p

对应代码

Mat mult_mat_mat(const Mat& A, const Mat& B) {
    size_t m = A.size();        // A 的行数
    size_t n = A[0].size();     // A 的列数
    size_t p = B[0].size();     // B 的列数
    assert(B.size() == n);      // B 的行数必须等于 A 的列数
    Mat C(m, Vec(p, 0.0));      // 结果矩阵,m行p列,初始全0
    for (size_t i = 0; i < m; i++) {       // 遍历 C 的每一行
        for (size_t j = 0; j < p; j++) {   // 遍历 C 的每一列
            for (size_t k = 0; k < n; k++) { // 累加求和
                C[i][j] += A[i][k] * B[k][j];
            }
        }
    }
    return C;
}

三重循环的含义

外层 i: 选 C 的第几行
即 A 的第几行

中层 j: 选 C 的第几列
即 B 的第几列

内层 k: 累加 A[i][k]*B[k][j]
即 A 的第i行 点乘 B 的第j列

得到 C[i][j]

手算演示

A = | 1  2 |    B = | 5  6 |
    | 3  4 |        | 7  8 |
C = A x B = ?

C[0][0]:A 的第0行 点乘 B 的第0列

A 第0行: [1, 2]
B 第0列: [5, 7]  (竖着取)
C[0][0] = 1*5 + 2*7 = 5 + 14 = 19

C[0][1]:A 的第0行 点乘 B 的第1列

A 第0行: [1, 2]
B 第1列: [6, 8]
C[0][1] = 1*6 + 2*8 = 6 + 16 = 22

C[1][0]:A 的第1行 点乘 B 的第0列

A 第1行: [3, 4]
B 第0列: [5, 7]
C[1][0] = 3*5 + 4*7 = 15 + 28 = 43

C[1][1]:A 的第1行 点乘 B 的第1列

A 第1行: [3, 4]
B 第1列: [6, 8]
C[1][1] = 3*6 + 4*8 = 18 + 32 = 50

完整图示

        B 的列
        第0列 第1列
         |     |
         v     v
A       [5]   [6]
的  [1, 2]-->[19]  [22]     A第0行 分别点乘 B的第0列、第1列
行  [3, 4]-->[43]  [50]     A第1行 分别点乘 B的第0列、第1列
         [7]   [8]
结果: C = | 19  22 |
          | 43  50 |

内层循环 k 的展开(以 C[1][0] 为例)

i=1, j=0:
  k=0:  C[1][0] += A[1][0] * B[0][0]  =  3 * 5  = 15    sum=15
  k=1:  C[1][0] += A[1][1] * B[1][0]  =  4 * 7  = 28    sum=43
        A[1][0]  A[1][1]           B[0][0]  B[1][0]
          |        |                 |        |
          3        4                 5        7
          |        |                 |        |
          +---*----+                 +---*----+
              |                          |
           A 的第1行                  B 的第0列
              |                          |
              +--------点乘--------------+
                         |
                   3*5 + 4*7 = 43

三、矩阵转置

定义

把矩阵的行和列互换。原来的第 i i i 行第 j j j 列,变成第 j j j 行第 i i i 列。
A T [ j ] [ i ] = A [ i ] [ j ] A^T[j][i] = A[i][j] AT[j][i]=A[i][j]
m × n m \times n m×n 的矩阵转置后变成 n × m n \times m n×m

对应代码

Mat transpose(const Mat& A) {
    size_t m = A.size();       // 原来 m 行
    size_t n = A[0].size();    // 原来 n 列
    Mat AT(n, Vec(m));         // 转置后 n 行 m 列
    for (size_t i = 0; i < m; i++) {
        for (size_t j = 0; j < n; j++) {
            AT[j][i] = A[i][j];   // 行列互换
        }
    }
    return AT;
}

手算演示

原矩阵 A(2行3列):
    列0  列1  列2
行0 [ 1,   2,   3 ]
行1 [ 4,   5,   6 ]
转置后 AT(3行2列):
    列0  列1
行0 [ 1,   4 ]      <-- 原来的列0变成了行0
行1 [ 2,   5 ]      <-- 原来的列1变成了行1
行2 [ 3,   6 ]      <-- 原来的列2变成了行2

赋值过程

A[0][0]=1 --> AT[0][0]=1     A[0][1]=2 --> AT[1][0]=2     A[0][2]=3 --> AT[2][0]=3
A[1][0]=4 --> AT[0][1]=4     A[1][1]=5 --> AT[1][1]=5     A[1][2]=6 --> AT[2][1]=6

直观理解:沿对角线翻转

原矩阵:              沿 \ 对角线翻转:
  1  2  3                1  4
  4  5  6                2  5
                         3  6

想象把矩阵写在一张透明纸上,沿左上到右下的对角线翻过去。

行列互换
A[i][j] --> AT[j][i]

A
2 x 3
| 1 2 3 |
| 4 5 6 |

AT
3 x 2
| 1 4 |
| 2 5 |
| 3 6 |

四、矩阵乘向量

定义

A A A m × n m \times n m×n 矩阵, x ⃗ \vec{x} x 是长度 n n n 的向量,结果 b ⃗ \vec{b} b 是长度 m m m 的向量。
b [ i ] = ∑ j = 0 n − 1 A [ i ] [ j ] ⋅ x [ j ] b[i] = \sum_{j=0}^{n-1} A[i][j] \cdot x[j] b[i]=j=0n1A[i][j]x[j]
一句话:结果的第 i 个元素 = A 的第 i 行 点乘 x

对应代码

Vec mult_mat_vec(const Mat& A, const Vec& x) {
    size_t m = A.size();       // A 的行数
    size_t n = A[0].size();    // A 的列数
    assert(x.size() == n);     // x 的长度必须等于 A 的列数
    Vec b(m, 0.0);             // 结果向量,长度 m
    for (size_t i = 0; i < m; i++) {       // 对 A 的每一行
        for (size_t j = 0; j < n; j++) {   // 逐元素相乘累加
            b[i] += A[i][j] * x[j];
        }
    }
    return b;
}

手算演示

A = | 1  2 |     x = | 1 |
    | 3  4 |         | 2 |

b[0]:A 的第0行 点乘 x

[1, 2] · [1, 2] = 1*1 + 2*2 = 1 + 4 = 5

b[1]:A 的第1行 点乘 x

[3, 4] · [1, 2] = 3*1 + 4*2 = 3 + 8 = 11
结果: b = [5, 11]

图示

矩阵 A          向量 x        结果 b
| 1  2 |   x   | 1 |    =    | 5  |     1*1+2*2 = 5
| 3  4 |       | 2 |         | 11 |     3*1+4*2 = 11
  ---            -              --
  2列         长度2           长度2(=行数)

可以理解为矩阵乘法的特殊情况

把向量看成 n × 1 n \times 1 n×1 的矩阵:

| 1  2 |   x   | 1 |    =    | 5  |
| 3  4 |       | 2 |         | 11 |
 (2x2)        (2x1)          (2x1)

所以矩阵乘向量其实就是矩阵乘矩阵,只不过右边的矩阵只有一列。

五、向量乘矩阵

定义

y ⃗ \vec{y} y 是长度 m m m 的向量, A A A m × n m \times n m×n 矩阵,结果 b ⃗ \vec{b} b 是长度 n n n 的向量。
b [ j ] = ∑ i = 0 m − 1 y [ i ] ⋅ A [ i ] [ j ] b[j] = \sum_{i=0}^{m-1} y[i] \cdot A[i][j] b[j]=i=0m1y[i]A[i][j]
一句话:结果的第 j 个元素 = y 点乘 A 的第 j 列

对应代码

Vec mult_vec_mat(const Vec& y, const Mat& A) {
    size_t m = A.size();       // A 的行数
    size_t n = A[0].size();    // A 的列数
    assert(y.size() == m);     // y 的长度必须等于 A 的行数
    Vec b(n, 0.0);             // 结果向量,长度 n
    for (size_t j = 0; j < n; j++) {       // 对 A 的每一列
        for (size_t i = 0; i < m; i++) {   // 逐元素相乘累加
            b[j] += y[i] * A[i][j];
        }
    }
    return b;
}

注意循环顺序

和"矩阵乘向量"对比:

矩阵 x 向量:  外层 i(遍历行),内层 j(遍历列)
               b[i] += A[i][j] * x[j]
               结果的每个元素 = A的一行 点乘 x
向量 x 矩阵:  外层 j(遍历列),内层 i(遍历行)
               b[j] += y[i] * A[i][j]
               结果的每个元素 = y 点乘 A的一列

手算演示

y = [1, 2]     A = | 1  2 |
                    | 3  4 |

b[0]:y 点乘 A 的第0列

A 的第0列: [1, 3]
y · A的第0列 = 1*1 + 2*3 = 1 + 6 = 7

b[1]:y 点乘 A 的第1列

A 的第1列: [2, 4]
y · A的第1列 = 1*2 + 2*4 = 2 + 8 = 10
结果: b = [7, 10]

图示

                   矩阵 A
                  列0  列1
                  |     |
向量 y            v     v
[1, 2]  x    | 1  [2] |    =    [7, 10]
              | 3  [4] |
                                 ^   ^
                                 |   |
                          y·列0=7  y·列1=10

可以理解为矩阵乘法的特殊情况

把向量看成 1 × m 1 \times m 1×m 的矩阵(行向量):

              | 1  2 |
[ 1,  2 ]  x | 3  4 |  =  [ 7,  10 ]
  (1x2)        (2x2)         (1x2)

六、五种运算的关系

总览

+-------------------+---------------+---------------+------------------+
| 运算              | 左操作数       | 右操作数       | 结果              |
+-------------------+---------------+---------------+------------------+
| 点积 dot          | 向量(n)       | 向量(n)       | 标量(1个数)       |
| 矩阵x矩阵        | 矩阵(m x n)   | 矩阵(n x p)   | 矩阵(m x p)      |
| 矩阵转置          | 矩阵(m x n)   |    无          | 矩阵(n x m)      |
| 矩阵x向量         | 矩阵(m x n)   | 向量(n)       | 向量(m)           |
| 向量x矩阵         | 向量(m)       | 矩阵(m x n)   | 向量(n)           |
+-------------------+---------------+---------------+------------------+

它们之间的联系

y*A = (AT*y)T

矩阵 x 矩阵
C[i][j] = sum(A[i][k]*B[k][j])

矩阵 x 向量
B 只有1列的特殊情况

向量 x 矩阵
A 只有1行的特殊情况

点积
A 只有1行 且 B 只有1列

转置
行列互换

维度匹配的核心规则

左边的列数 = 右边的行数,结果取左边的行数 x 右边的列数

(m x [n]) * ([n] x p) = (m x p)
      ^         ^
      |         |
      +-必须相等-+

不满足这个条件就会触发 assert 失败。

七、代码中的 C++ 细节

using 类型别名

using Vec = std::vector<double>;              // Vec 就是 vector<double>
using Mat = std::vector<std::vector<double>>; // Mat 就是二维 vector

省得每次写一长串类型名。

const 引用参数

double dot(const Vec& x, const Vec& y)
//         ^^^^^    ^
//         不修改   引用(不拷贝)

const Vec& 的含义:

不加 const 和 &:   void f(Vec x)      --> 拷贝一份,浪费内存和时间
加 & 不加 const:   void f(Vec& x)     --> 不拷贝,但函数内可以修改原数据
加 const 和 &:     void f(const Vec& x) --> 不拷贝,也不能修改,最安全高效

assert 断言

assert(x.size() == y.size());  // 如果条件为 false,程序立即崩溃并报错

用于捕捉编程错误(比如传入维度不匹配的矩阵)。发布版本可以用 -DNDEBUG 编译选项关闭所有断言。

size_t 类型

for (size_t i = 0; i < x.size(); i++)

size_t 是无符号整数类型,和 vector::size() 的返回类型一致。如果用 int 会产生"有符号和无符号比较"的编译警告。

八、完整运行结果

点积 [1,2,3]·[4,5,6] = 32
A x B =
   19.00   22.00
   43.00   50.00
转置 [[1,2,3],[4,5,6]] =
    1.00    4.00
    2.00    5.00
    3.00    6.00
[[1,2],[3,4]] x [1,2] = [5.00, 11.00]
[1,2] x [[1,2],[3,4]] = [7.00, 10.00]

九、矩阵乘法不满足交换律

注意最后两个测试的结果不同:

矩阵 x 向量:  A * x = [5, 11]
向量 x 矩阵:  x * A = [7, 10]

这说明矩阵乘法不满足交换律 A × B ≠ B × A A \times B \neq B \times A A×B=B×A)。

A * x:  每个结果元素 = A的一行 点乘 x
x * A:  每个结果元素 = x 点乘 A的一列
取的"方向"不同,结果自然不同。

1.1.34 过滤问题分析(Filtering)

思路理解

输入: N N N [ 0 , 1 ) [0, 1) [0,1) 之间的实数。
问题是:哪些操作必须把所有数存下来,哪些只需固定大小的变量

任务 是否需要存所有数 理由
打印最大值和最小值 X 不需要 维护两个变量 max, min 即可,逐个更新
打印中位数 √ 需要 中位数要知道排序后的第 N/2 个数,必须存全部
打印第 k 小的值X(k < 100) X 不需要 只需维护大小为 k 的最大堆(固定大小)
打印平方和 X 不需要 维护累积变量 ∑ x i 2 \sum x_i^2 xi2,逐个加即可
打印平均值 X 不需要 维护 sumcount,最后 sum/count
打印大于平均值的数的比例 √ 需要 必须先算出平均值,再第二次扫描——需要存全部数
升序打印 √ 需要 排序需要所有数据
随机顺序打印 √ 需要 洗牌必须知道所有数据

C++ 完整代码(演示全部情况)

#include <iostream>
#include <vector>
#include <algorithm>   // sort, nth_element
#include <numeric>     // accumulate
#include <queue>       // priority_queue
#include <cstdlib>     // rand
int main() {
    // 读取所有输入(实际场景下是流式读取)
    std::vector<double> data;
    std::cout << "请输入若干 [0,1) 之间的实数(Ctrl+D 结束):" << std::endl;
    double val;
    while (std::cin >> val) {
        data.push_back(val);
    }
    int N = (int)data.size();
    if (N == 0) { std::cout << "没有输入!" << std::endl; return 0; }
    // ===================================================
    // 1. 打印最大值和最小值(不需要存所有数,这里演示用已有数据)
    // ===================================================
    double cur_max = data[0], cur_min = data[0];
    for (double x : data) {
        if (x > cur_max) cur_max = x;
        if (x < cur_min) cur_min = x;
    }
    std::cout << "\n1. 最大值: " << cur_max << ",最小值: " << cur_min << std::endl;
    // ===================================================
    // 2. 打印中位数(需要存所有数)
    // ===================================================
    std::vector<double> sorted_data = data;
    std::sort(sorted_data.begin(), sorted_data.end());
    double median;
    if (N % 2 == 1) {
        median = sorted_data[N / 2];
    } else {
        median = (sorted_data[N / 2 - 1] + sorted_data[N / 2]) / 2.0;
    }
    std::cout << "2. 中位数: " << median << std::endl;
    // ===================================================
    // 3. 第 k 小的值(k < 100,只需固定大小的堆)
    // ===================================================
    int k = std::min(3, N);  // 演示取第3小
    // 用大小为 k 的最大堆,维护当前最小的 k 个数
    std::priority_queue<double> max_heap;
    for (double x : data) {
        max_heap.push(x);
        if ((int)max_heap.size() > k) max_heap.pop();  // 超过 k 个就踢掉最大的
    }
    std::cout << "3. 第" << k << "小的值: " << max_heap.top() << std::endl;
    // ===================================================
    // 4. 平方和(不需要存所有数)
    // ===================================================
    double sum_sq = 0.0;
    for (double x : data) sum_sq += x * x;
    std::cout << "4. 平方和: " << sum_sq << std::endl;
    // ===================================================
    // 5. 平均值(不需要存所有数)
    // ===================================================
    double sum = 0.0;
    for (double x : data) sum += x;
    double avg = sum / N;
    std::cout << "5. 平均值: " << avg << std::endl;
    // ===================================================
    // 6. 大于平均值的数的比例(需要存所有数,因为先算均值再扫描)
    // ===================================================
    int above_avg = 0;
    for (double x : data) {
        if (x > avg) above_avg++;
    }
    std::cout << "6. 大于平均值的比例: " << (100.0 * above_avg / N) << "%" << std::endl;
    // ===================================================
    // 7. 升序打印(需要排序,存所有数)
    // ===================================================
    std::cout << "7. 升序: ";
    for (double x : sorted_data) std::cout << x << " ";
    std::cout << std::endl;
    // ===================================================
    // 8. 随机顺序打印(需要洗牌,存所有数)
    // ===================================================
    std::vector<double> shuffled = data;
    // Fisher-Yates 洗牌算法
    for (int i = N - 1; i > 0; i--) {
        int j = rand() % (i + 1);
        std::swap(shuffled[i], shuffled[j]);
    }
    std::cout << "8. 随机顺序: ";
    for (double x : shuffled) std::cout << x << " ";
    std::cout << std::endl;
    return 0;
}

总结对照表

┌──────────┬────────────────────────────┬──────────────────────────────────┐
│ 题号     │ 核心思想                   │ 关键技术点                       │
├──────────┼────────────────────────────┼──────────────────────────────────┤
│ 1.1.26   │ 三次比较+交换              │ 临时变量 t,手动 swap            │
│ 1.1.27   │ 记忆化消除重复递归         │ 二维 memo 数组,DP 自底向上     │
│ 1.1.28   │ 排序后去重                 │ 相邻比较,跳过重复              │
│ 1.1.29   │ 二分找上下界               │ lower_bound / upper_bound 思想  │
│ 1.1.30   │ GCD 判断互质               │ 欧几里得辗转相除法              │
│ 1.1.31   │ 圆上布点+随机连线          │ 极坐标转直角坐标,随机数        │
│ 1.1.32   │ 区间统计+柱状图            │ 桶计数,ASCII 可视化           │
│ 1.1.33   │ 线性代数基本运算           │ 点积/矩阵乘法/转置              │
│ 1.1.34   │ 分析哪些需要存储所有数据   │ 流式处理 vs. 需要全局信息      │
└──────────┴────────────────────────────┴──────────────────────────────────┘
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐