Jiahong 的个人博客

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

Math——辛普森悖论

本文介绍辛普森悖论


辛普森悖论

  • 辛普森悖论指的是在分组比较中占优的一方,在合并数据后反而处于劣势的现象

药物效果示例

  • 场景场景 :比较两种药物(A 和 B)对轻症和重症患者的治愈率

  • 分组数据 :

    • 轻症患者组 :
      • 药 A:治疗 10 人 ,治愈 9 人 -> 治愈率 90%
      • 药 B:治疗 100 人 ,治愈 80 人 -> 治愈率 80%
    • 重症患者组 :
      • 药 A:治疗 100 人 ,治愈 30 人 -> 治愈率 30%
      • 药 B:治疗 10 人 ,治愈 2 人 -> 治愈率 20%
  • 分组结论 :

    • 轻症组:药 A 的治愈率(90%)> 药 B(80%)
    • 重症组:药 A 的治愈率(30%)> 药 B(20%)
  • 合并数据 :

    • 药 A:总治愈 39 人(9 + 30),总治疗 110 人 -> 治愈率 35.5%
    • 药 B:总治愈 82 人(80 + 2),总治疗 110 人 -> 治愈率 74.5%
  • 悖论出现 :

  • 尽管药 A 在每个分组的治愈率都更高,但合并后药 B 的总治愈率却显著优于药 A。这是因为药 B 主要用于治愈率高的轻症患者(样本量 100 vs. 10),而药 A 更多用于治愈率低的重症患者(样本量 100 vs. 10),导致整体结果反转

核心原因 :

  • 数据分组中存在混杂变量(此处为病情严重程度),且各组样本量差异巨大,合并时权重不同引发悖论

什么药是真正优秀的?

  • 药 A才是真正疗效更好的,因为在任何场景下(不论轻症还是重症下),都是药 A 效果更好,之所以合并到一起统计出现悖论,是因为医生开药时存在刻意倾向导致的,给轻症患者更多的开了药 B,这相当于强行提高了药 B 的治愈率
  • 其他类似问题也出现在不同学院男女录取率上

Math——调和级数的和

调和级数的和是发散的,并不收敛,但是这又是一个非常常用的级数,本文总结了调和级数的和的求法


一个事实

  • 调和级数的和与 n 的自然对数的差值收敛到欧拉-马歇罗尼常数
    $$
    \begin{align}
    lim_{n \to \infty}(\sum_{i=1}^{n}\frac{1}{i} - ln(n)) = \gamma
    \end{align}
    $$
    • \(\gamma\) 为欧拉-马歇罗尼常数
      • (欧拉常数又称欧拉-马斯克若尼常数,近似值为γ≈0.57721 56649 01532 86060 65120 90082 40243 10421 59335)

证明

  • 事实上,调和级数的和为
    $$
    \begin{align}
    \sum_{i=1}^{n}\frac{1}{i} = ln(n) + \gamma + \epsilon_{n}
    \end{align}
    $$
    • \(\gamma\) 为欧拉-马歇罗尼常数
    • \(\epsilon_{n}\) 约等于 \(\frac{1}{2n}\),随着n的不断增大, \(\epsilon_{n}\) 趋于0

补充

  • 两个不同的调和数之间的差值永远不是整数
  • 除了n=1时以外,没有任何一个调和数是整数

Math——马尔可夫链及其平稳分布


核心定义

  • 一句话定义:平稳分布在马尔可夫链长期演化后系统达到的稳态概率分布
  • 对于一个离散时间马尔可夫链(Markov Chain) ,若存在一个概率分布 \(\pi\) 满足以下方程:
    $$
    \pi = \pi P
    $$
    • \(P\) 是转移概率矩阵(\(P_{ij}\) 表示从状态 \(i\) 转移到状态 \(j\) 的概率)
    • \(\pi\) 是行向量,表示每个状态的概率
  • 则称 \(\pi\) 为该马尔可夫链的平稳分布(Stationary Distribution)。直观上,这意味着系统达到 \(\pi\) 后,状态分布不再随时间改变

关键性质

  • 存在性与唯一性 :

    • 若马尔可夫链是不可约马尔可夫链(Irreducible Markov Chain)(不可约马尔可夫链的所有状态互通),且满足正常返(Positive Recurrent)特性(即每个状态会被无限次访问),则平稳分布存在且唯一
    • 对于有限状态的不可约链,平稳分布一定存在
  • 长期行为 :

    • 无论初始分布如何,若链满足一定条件(如非周期性),长期后的状态分布会收敛到平稳分布:
      $$
      \lim_{n \to \infty} \mu P^n = \pi
      $$
      其中 \(\mu\) 是初始分布
  • 细致平衡条件(Detailed Balance):

    • 若分布 \(\pi\) 满足 \(\pi_i P_{ij} = \pi_j P_{ji}\)(对所有 \(i,j\)),则 \(\pi\) 是平稳分布。满足此条件的链称为可逆马尔可夫链

计算方法

  • 解线性方程组 :
    • 通过 \(\pi = \pi P\) 和 \(\sum_i \pi_i = 1\) 联立求解
    • 例如,对两状态链:
      $$
      \begin{cases}
      \pi_1 = \pi_1 P_{11} + \pi_2 P_{21} \\
      \pi_2 = \pi_1 P_{12} + \pi_2 P_{22} \\
      \pi_1 + \pi_2 = 1
      \end{cases}
      $$
  • 其他求解方法:特征向量法

例子

  • 假设天气模型(晴/雨)的转移矩阵,描述了晴雨天转换的概率:
    $$
    P = \begin{bmatrix}
    0.9 & 0.1 \\
    0.5 & 0.5
    \end{bmatrix}
    $$
  • 解 \(\pi P = \pi\) 得 \(\pi = [\frac{5}{6}, \frac{1}{6}]\),即长期下有5/6概率为晴天,1/6为雨天

数据存储——Parquet文件格式简单介绍


Parquet 整体说明

  • Parquet(Apache Parquet)是一种列式存储的二进制文件格式,专为大数据处理场景设计,具有高效压缩和编码特性
  • Parquet 尤其适合分析型工作负载,能显著提升查询速度并降低存储成本

Parquet 文件结构

  • Parquet文件由多个行组(Row Groups)组成,每个行组包含:
  • 列块(Column Chunks) :存储实际列数据
  • 元数据 :记录统计信息(如min/max),用于加速查询

Parquet 文件读取示例

  • Spark :df.write.parquet("path") / spark.read.parquet("path")
  • Pandas :pd.read_parquet("file.parquet", engine="pyarrow")
  • Hive :CREATE TABLE ... STORED AS PARQUET

附录:文件读取和分析示例

  • 使用 Python 代码读取并解析文件格式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import pandas as pd

    def read_and_inspect_parquet(file_path):
    try:
    # 读取Parquet文件
    df = pd.read_parquet(file_path)

    print("成功读取Parquet文件!")
    print("\n===== 数据基本信息 =====")
    df.info() # 显示数据框的基本信息,包括列名、数据类型、非空值数量等

    for col in df.columns:
    print("col:", col)
    print(df[col][0])

    print("\n===== 前5行数据 =====")
    print(df.head()) # 显示前5行数据,查看数据格式

    print("\n===== 数据统计信息 =====")
    print(df.describe()) # 显示数值型列的统计信息

    return df

    except FileNotFoundError:
    print(f"错误: 找不到文件 '{file_path}'")
    except Exception as e:
    print(f"读取Parquet文件时发生错误: {str(e)}")
    return None

    if __name__ == "__main__":
    parquet_file_path = "./gsm8k/test.parquet"

    # 读取并查看Parquet文件
    dataframe = read_and_inspect_parquet(parquet_file_path)

附录:列式存储 vs 行式存储

  • 行式存储(如CSV、JSON):按行存储数据,适合整行读取的场景(如单条记录查询)
  • 列式存储(如Parquet):按列存储数据,适合只读取部分列或聚合分析的场景(如计算某列的平均值)

趣味题——优惠券收集问题


题目描述

  • 餐馆有12生肖优惠券,每天给小明等概率随机发放一张,问: 小明集齐12中生肖优惠券的天数
  • 相关延伸: 掷骰子, 要求每个面都出现一次, 求投掷次数的期望

问题求解

  • 思路:
    • 把天数 \(X\) 这个随机变量变成12个随机变量的和$$X = \sum_{i=1}^{12}x_{i}$$
    • 于是可以把期望分解为12个随机变量的期望和$$E(X) = \sum_{i=1}^{12}E(x_{i})$$
  • 第一种优惠券(12种中的任意一种均可):
    • 第一天随机拿到一个优惠券一定能够对应其中某一个生肖,概率为 \(p_{1} = 1\)
    • 期望天数 \(E(x_{1})\) 与概率的关系为 \(E(x_{1})\cdot p_{1} = 1\)
      • 一个直观的说明,为什么 \(E(x_{1})\cdot p_{1} = 1\):由于每一天成功的概率为 \(p_{1}\),不成功概率为 \(1-p_{1}\),显然为二项分布,由二项分布的期望应该为1次 ,即 \(np_{1}=1\),其中n就是我们天数的期望(注意:二项分布的期望与这里的天数期望不同,后者为使得二项分布期望为1所需要的天数)
      • 上面的证明不够严谨,严谨的数学证明如下:
        • 假设期望天数是 \(E\),每天成功的概率为 \(p\),下面我们求成功和不成功的概率分布
        • 第一天成功的概率为 \(p\),成功时只需要 \(1\) 天即可, 即有 \(p\) 的概率需要 \(1\) 天
        • 第一天没成功的概率为 \(1-p\),不成功时需要将当前日子(\(1\) 天)算上,然后回到原点,重新开始,即有 \(1-p\) 的概率需要 \(1+E\) 天
        • 期望公式
          $$E = p*1 + (1-p)(1+E)$$
        • 化简为:
          $$pE = 1$$
    • 需要天数的期望为$$E(x_{1}) = \frac{1}{p_{1}} = 1$$
  • 第二种优惠券(11种中的任意一种均可):
    • 每天随机收到的优惠券有 \(p_{2} = \frac{11}{12}\) 的概率满足剩余的11个生肖中的一个
    • 期望天数 \(E(x_{2})\) 与概率的关系为 \(x_{2}\cdot p_{2} = 1\)
    • 需要的天数期望为$$E(x_{2}) = \frac{1}{p_{2}} = \frac{12}{11}$$
  • …
  • 以此类推可得:
    $$
    \begin{align}
    E(X) &= \sum_{i=1}^{n}E(x_{i}) \\
    &= \sum_{i=0}^{11}\frac{12}{12-i} \\
    &= \sum_{i=1}^{12}\frac{12}{i} \\
    &= 12\sum_{i=1}^{12}\frac{1}{i} \\
    \end{align}
    $$
  • 由于调和级数的和与n的自然对数有关系,详情参考调和级数的和
    $$
    \begin{align}
    \sum_{i=1}^{n}\frac{1}{i} = ln(n) + \gamma + \epsilon_{n}
    \end{align}
    $$
    • \(\gamma\) 为欧拉-马歇罗尼常数,近似值为γ≈0.57721
    • \(\epsilon_{n}\) 约等于 \(\frac{1}{2n}\),随着n的不断增大, \(\epsilon_{n}\) 趋于0
  • 所以有
    $$
    \begin{align}
    E(X) &= 12\sum_{i=1}^{12}\frac{1}{i} \\
    &\approx 12(ln(12)+\gamma+\frac{1}{24})
    \end{align}
    $$
  • 将12生肖推广到n,有
    $$
    \begin{align}
    E(X) &= n\sum_{i=1}^{n}\frac{1}{i} \\
    &\approx n(ln(n)+\gamma+\frac{1}{2n}) \\
    &\approx nln(n)
    \end{align}
    $$

趣味题——蓄水池采样算法


整体说明

  • 蓄水池采样 (Reservoir Sampling) 算法是一种在数据流中进行随机抽样的方法
  • 它的独特之处在于,不知道数据流的总长度 ,但可以实现对样本等概率采样
  • 场景:处理一个无法放入内存的巨大文件,或者一个实时传入的无限数据流
    • 在这种情况下,需要从这个流中随机选择 \(k\) 个元素,并且要确保每个元素被选中的概率是相等的

算法流程

  • 蓄水池采样算法的核心思想是,随着数据流的不断输入,动态地更新一个大小为 \(k\) 的“蓄水池”
  • 以下是该算法的详细流程:
  • 第一步:初始化蓄水池
    • 创建一个大小为 \(k\) 的数组或列表,这就是我们的蓄水池 (Reservoir)
      • 1)从数据流中读取前 \(k\) 个元素
      • 2)将这 \(k\) 个元素直接放入蓄水池中
  • 第二步:处理后续元素
    • 从数据流中继续读取第 \(i\) 个元素,其中 \(i > k\)。对于每一个新元素,执行以下操作:
      • 1)生成一个 \(1\) 到 \(i\) 之间的随机整数 \(j\)
      • 2)如果 \(j\) 满足条件 \(1 \le j \le k\),则将当前元素替换蓄水池中索引为 \(j\) 的元素
      • 3)如果 \(j > k\),则不做任何操作,直接丢弃该元素
  • 第三步:重复
    • 1)重复第二步,直到数据流结束
    • 2)当数据流处理完毕后,蓄水池中剩下的 \(k\) 个元素就是我们最终的随机样本

算法证明

  • 蓄水池采样算法之所以有效,是因为它能确保数据流中的每个元素被选入蓄水池的概率都是 \(k/N\) ,其中 \(N\) 是数据流的总长度。我们通过数学归纳法来简单理解一下
  • 初始阶段 :前 \(k\) 个元素被直接放入蓄水池,因此它们被选中的概率是 \(1\)
  • 第 \(i\) 个元素 (\(i > k\)):
    • 第 \(i\) 个元素被选中的概率是 \(k/i\)。这是因为我们生成了一个 \(1\) 到 \(i\) 的随机数,只有当它落在前 \(k\) 个位置时,第 \(i\) 个元素才会被选中并替换掉蓄水池中的一个元素
    • 关键是保持之前元素的概率不变。我们来看一下,在第 \(i\) 步,蓄水池中某个旧元素被保留的概率
      • 在第 \(i-1\) 步结束时,蓄水池中的每个元素被选中的概率是 \(k/(i-1)\)
      • 当第 \(i\) 个元素进入时,这个旧元素被替换的概率是 \(1/i\)(因为它有 \(1/i\) 的概率被选中并替换掉蓄水池中的一个随机位置)
      • 所以,一个旧元素被保留的概率是 \(1 - 1/i = (i-1)/i\)
      • 那么,这个旧元素在第 \(i\) 步结束后仍然留在蓄水池中的概率是:
        $$P(\text{留在蓄水池}) = P(\text{在第 }i-1 \text{ 步被选中}) \times P(\text{在第 }i \text{ 步被保留}) = \frac{k}{i-1} \times \frac{i-1}{i} = \frac{k}{i}$$
    • 通过这个过程,我们可以看到,在处理完第 \(i\) 个元素后,所有 \(i\) 个元素(包括新来的和之前的)被选中的概率都精确地保持在 \(k/i\)。当数据流结束时,假设总长度为 \(N\),则每个元素被选中的概率都是 \(k/N\)

趣味题——寻找最优者

寻找最优者:不可逆的做出选择,如何使得选到最优者的概率最大,又称为1/e法则,“秘书问题”或“相亲问题“


题目描述

  • 由于无人知道缘分何时到来,假设人的一生会遇到100个异性,这100个人参差不齐,其中有个人是最好的,100人随机的排着队出现,而我们对每个人必须决定是否接受,而且一旦决定后不能修改,那么用什么策略选择更有机会选到最好的那一个呢?

解决方案

策略设想

  • 拒绝前 k 个人,然后从第 k+1 个开始,只要 优于前面的所有人 ,则接受
  • k 值的设定比较难,太小了容易找到不好的,太多了容易错过最好的

证明

  • 假设最优者出现在第 \(i(k< i \leq n)\) 个(事件A),那么最优者被选中(事件B)的概率为前i-1个中的最优者在前k个人中的概率
    $$ P(B|A) = \frac{k}{i-1} $$
    • \(P(B|A) = P(最优者被选中|第 i 个为最优者)\)
  • 最优者出现在第 \(i\) 个的概率为
    $$P(B) = \frac{1}{n}$$
  • 最优者被选中的概率为
  • 接下来的证明用到了调和级数的和,详情参考调和级数的和
    $$
    \begin{align}
    \sum_{i=1}^{n}\frac{1}{i} = ln(n) + \gamma + \epsilon_{n}
    \end{align}
    $$
    • \(\gamma\) 为欧拉-马歇罗尼常数,近似值为γ≈0.57721,下面的推导都不需要精确解的,所以可以使用该公式
    • \(\epsilon_{n}\) 约等于 \(\frac{1}{2n}\),随着n的不断增大, \(\epsilon_{n}\) 趋于0
      $$
      \begin{align}
      P(B) &= \sum_{A}P(B,A) \\
      &= \sum_{A}P(B|A)P(A) \\
      &= \sum_{i=k+1}^{n}\frac{k}{i-1}\frac{1}{n} \\
      &= \sum_{i=k+1}^{n}\frac{k}{n}\frac{1}{i-1} \\
      &= \frac{k}{n}\sum_{i=k+1}^{n}\frac{1}{i-1} \\
      &= \frac{k}{n}\sum_{i=k}^{n-1}\frac{1}{i} \\
      &= \frac{k}{n}(\sum_{i=1}^{n-1}\frac{1}{i} - \sum_{i=1}^{k-1}\frac{1}{i}) \\
      &\approx \frac{k}{n}((ln(n-1)+C)-(ln(k-1)+C)) \\
      &= \frac{k}{n}ln(\frac{n-1}{k-1}) \\
      \end{align}
      $$
  • 当n和k足够大时有
    • (不能证明 k 也会足够大,但是直觉上 n 足够大 k 也会足够大,因为 n 足够大时往往需要拒绝更多的人)
      $$\frac{n-1}{k-1} = \frac{n}{k}$$
  • 令 \(x=\frac{k}{n}\),则有
    $$
    \begin{align}
    f(x) &= xln\frac{1}{x} \\
    {f}’(x) &= ln(\frac{1}{x})+(-\frac{1}{x^2}\cdot x \cdot x) \\
    &= ln(\frac{1}{x})-1
    \end{align}
    $$
  • 求 \(f(x)\) 的最大值,令 \({f}’(x)=0\) 有
    $$ ln(\frac{1}{x})-1 = 0 $$
  • 解方程得
    $$ x = \frac{1}{e} $$
  • 也就是说当 \(\frac{k}{n} = \frac{1}{e} \) 时,找到最优者的概率最大

策略描述

  • 拒绝前 \(\frac{n}{e}\) 个人(也就是拒绝前37%的人),然后从第 \(\frac{n}{e}+1\) 个开始,只要优于前面的所有人 ,则接受
  • 这种方式可以保证我们有最大的概率找到最优者

趣味题——绳子剪三段后能构成三角形的概率

初始题目为1米长的绳子剪成三段后能够构成三角形的概率,实际上绳子的长度可以是任意的,解法都一样


题目描述

  • 一根绳子长度为 n,剪三段后能构成三角形的概率是多少?
  • 由于长度与概率无关,所以接下来我们把绳子当做长度为 1 米来处理

解决方案

  • 三段绳子的长度分别为 x,y,1-x-y
  • 首先需要满足
    $$
    \begin{align}
    x &> 0 \\
    y &> 0\\
    1-x-y &> 0 \\
    \end{align}
    $$
    • 以 x 为横坐标,y 为纵坐标画图得到阴影区域面积为 \(\frac{1}{2}\)
  • 图片来源于:https://blog.csdn.net/fanoluo/article/details/40374571*
  • 进一步分析三段绳子能生成组成三角形的概率
    $$
    \begin{align}
    x+y &> 1-x-y \\
    y + 1-x-y &> x\\
    x + 1-x-y &> y \\
    \end{align}
    $$
    • 进一步分析得到阴影部分面积为 \(\frac{1}{8}\)
  • 图片来源于:https://blog.csdn.net/fanoluo/article/details/40374571*
  • 所以有
    $$P = \frac{1/8}{1/2} = \frac{1}{4}$$

Python——做算法题时应该注意的细节

和Java类似,Python实现了丰富的基本数据结构,但是使用方式和Java有所不同


字符类型

  • Python中没有Java以及C++一样的字符类型,只有字符串类型

字符和数字的比较

  • 'a'表示的不是字符’a’, 而是字符串”a”,也就是说0 < 'a'这样的写法是不可以的,因为'a'不是一个数字
  • 若非要和数字比较,方式如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    string = "12_abc*ABZ-="
    alphanumeric_str = ""
    for char in string:
    if (ord(char) >= ord('0') and ord(char) <= ord('9'))\
    or (ord(char) >= ord('a') and ord(char) <= ord('z'))\
    or (ord(char) >= ord('A') and ord(char) <= ord('Z')):
    alphanumeric_str += char
    print(alphanumeric_str)

    # # output:
    # 12abcABZ

集合类型的复制

浅拷贝

以list为例

1
2
3
4
5
6
7
8
l = [1, 2, 3]
# method 1
lcopy = l[:] # notice: ls.append(l[:]), 添加的是副本!!!
# method 2
lcopy = l.copy()
# method 3
import copy
lcopy = copy.copy(l)

深拷贝

1
2
3
4
import copy

l = [1, 2, 3, [4, 5, 6]]
l = copy.deepcopy(l)

format函数的使用

  • format是用于格化式字符串的函数

  • format是字符串自带的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # format string
    print "{} {}".format("hello", "world")
    print "{0} {1}".format("hello", "world")
    print "{1} {0} {1}".format("hello", "world")
    # output
    'hello world'
    'hello world'
    'world hello world'

    # format number
    print "{:.2f}".format(3.1415926)
  • 其他使用方式之后再补充


字符串到数字的转换

1
2
3
4
5
6
7
8
9
# string to integer, int(str, base=10)
a = "101"
integer = int(a, 10)
integer = int(a, 2)
integer = int(a, 7)

# string to float, float(str)
b = "123.4"
f = float(b)

进制转换

转换示例

1
2
3
4
5
6
7
8
9
# binary, octonary, hexadecimal to decimal(from string)
dec_num = int("1001", 2) # or int("0b1001", 2)
dec_num = int("657", 8) # or int("0657", 8)
dec_num = int("ff", 16) # or int("0xff", 16)

# decimal to binary, octonary, hexadecimal(or string)
bin_num = bin(123) # or str(bin(123))
oct_num = oct(123) # or str(oct(123))
hex_num = hex(123) # or str(hex(123))

总结:

  • 词的表示为十进制(123), 二进制(0b110), 八进制(0567), 十六进制(0xff1)
  • 词的转换函数如下:
十进制 二进制 八进制 十六进制
表示 123 0b110 0567 0xff1
函数 int bin oct hex

map函数的使用

定义

  • Python3

    1
    map(function, iterable, ...) -> map
  • Python2

    1
    map(function, iterable, ...) -> list

示例

1
2
3
4
5
6
7
8
9
10
11
12
# Python2
print map(lambda x: x**2, [1, 2, 3])
# output:
[1, 4, 9]

print map(lambda x, y: x+y, [1, 2, 3], [5, 6, 7])
# output:
[6, 8, 10]

# Python3
result = map(lambda x: x**2, [1, 2, 3])
print(list(result))

Python——常见函数总结


chain()

  • chain() 是 itertools 模块提供的一个非常实用的函数,它能够把多个可迭代对象连接起来,形成一个连续的迭代器

  • 基本用法为:

    1
    2
    from itertools import chain
    chain(*iterables) # 参数是多个可迭代对象(注意是展开形式)
  • 链接多个可迭代对象,可以是不同对象,对象内部的元素也可以是不同类型

    1
    2
    3
    4
    5
    6
    7
    8
    from itertools import chain

    set1 = {1, '2'}
    set2 = {3, 4}
    list1 = [5, 6]

    combined = list(chain(set1, set2, list1))
    print(combined) # 输出:[1, '2', 3, 4, 5, 6](集合元素顺序可能不同)
  • 特别说明,chain() 对可迭代对象是依次访问的,只有迭代访问完前面一个采集继续访问另一个


zip() 函数

  • 当 zip 函数输入长度不一致时,不会报错,会适配最短长度
  • 示例:
    1
    2
    3
    4
    5
    6
    for a, b in zip([1,2,3],[1,4,4,5,4,5,6,7]):
    print(a, b)

    # 1 1
    # 2 4
    # 3 4

for…else 语句

  • 在 Python 中,for...else 是一种特殊的流程控制结构,for...else 的核心是通过 else 子句判断循环是否“自然完成”,而非被 break 强制终止,这在需要验证“遍历完整性”的场景中非常实用

  • 其基本语法为:

    1
    2
    3
    4
    for 变量 in 可迭代对象:
    循环体(当迭代继续时执行)
    else:
    else子句(当循环正常结束时执行)
  • 核心逻为:

    • else 子句的执行条件 :当 for 循环完整遍历了所有元素(即没有通过 break 语句提前退出循环),则循环结束后会执行 else 中的代码
    • else 子句不执行的情况 :如果循环中遇到 break 语句并跳出循环(提前终止),则 else 子句不会执行
  • 示例 1:循环正常结束(执行 else)

    1
    2
    3
    4
    5
    6
    7
    8
    for i in range(3):
    print(i)
    else:
    print("循环正常结束,执行 else") # 循环完整遍历了 `range(3)` 的所有元素(0、1、2),没有 `break`,因此 `else` 被执行
    # 0
    # 1
    # 2
    # 循环正常结束,执行 else
  • 示例 2:循环被 break 终止(不执行 else)

    1
    2
    3
    4
    5
    6
    7
    8
    for i in range(3):
    print(i)
    if i == 1:
    break # 当i=1时提前退出循环
    else:
    print("循环正常结束,执行 else") # 循环在 `i=1` 时通过 `break` 提前终止,因此 `else` 子句未执行
    # 0
    # 1
  • 典型应用场景:for...else 常用于判断“是否在循环中找到目标” ,例如在列表中查找元素:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    target = 5
    nums = [1, 3, 4, 6]

    for num in nums:
    if num == target:
    print(f"找到目标:{target}")
    break
    else:
    # 若循环完整执行(未触发break),说明未找到目标
    print(f"未找到目标:{target}") # 由于 `nums` 中没有 5,循环完整遍历后执行 `else`,提示“未找到目标”

    # 未找到目标:5

dict 特殊初始化逻辑

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    default_config = { # 默认值定义
    "pass_score": 60,
    "check_mode": "all",
    "time_limit": 30
    }

    custom_config = {
    "pass_score": 75, # 覆盖默认:及格线提高到75分
    "need_detail": True # 新增:需要详细报告
    }

    # 配置合并,第一个是默认配置,第二个是新增或修改配置
    final_config = {**default_config, **custom_config}

    print(f"最终配置:{final_config}")

    # 最终配置:{'pass_score': 75, 'check_mode': 'all', 'time_limit': 30, 'need_detail': True}
  • 注:Python 3.9+ 还支持更简洁的 | 字典合并运算符(效果完全一致):

    1
    grader_params = grader_config.defaults | grader_dict
  • 运行原代码后,grader_params 的结果的是:

    1
    2
    3
    4
    5
    6
    {
    "score_threshold": 70, # 自定义覆盖了默认
    "check_type": "full", # 保留默认(无自定义冲突)
    "timeout": 30, # 保留默认
    "check_detail": True # 新增自定义配置
    }

string 的 rindex 和 rfind 函数用法

  • rindex() 和 rfind() 是字符串(str)的内置方法,核心作用是从字符串右侧(末尾)开始查找指定子串的最后一次出现位置 ,返回其索引值

    • 两者用法高度相似,仅在“子串未找到”时的行为不同
      • rfind():未找到返回 -1
      • rindex():未找到抛出 ValueError 异常
    • 两者返回子串都是在原字符串中最后一次出现的起始索引(索引从 0 开始)
  • 完整语法

    1
    2
    3
    4
    5
    # rfind():未找到返回 -1
    str.rfind(sub, start=0, end=len(str))

    # rindex():未找到抛出 ValueError 异常
    str.rindex(sub, start=0, end=len(str))
    • sub:必需参数,要查找的子串(可以是单个字符、多个字符的字符串)
    • start:可选参数,查找的起始索引(从左往右的索引,默认 0,即从字符串开头开始限定查找范围)
    • end:可选参数,查找的结束索引(不包含 end 本身,默认 len(str),即到字符串末尾结束)
  • 选择建议:如果不确定子串是否存在,优先用 rfind()(避免异常);如果明确子串一定存在,用 rindex()(逻辑更简洁)

使用示例

  • 基础用法(无 start/end 参数):从字符串末尾开始查找子串的最后一次出现位置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    s = "hello world, hello python"

    # 查找 "hello" 的最后一次出现
    print(s.rfind("hello")) # 输出 13(第二个 "hello" 的起始索引)
    print(s.rindex("hello")) # 输出 13

    # 查找单个字符 "o" 的最后一次出现
    print(s.rfind("o")) # 输出 18("python" 中的 "o")
    print(s.rindex("o")) # 输出 18

    # 查找不存在的子串
    print(s.rfind("java")) # 输出 -1(不报错)
    # print(s.rindex("java")) # 抛出 ValueError: substring not found
  • 指定 start/end 参数(限定查找范围):start 和 end 限定“从原字符串的 [start, end) 区间内,从右往左查找”:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    s = "abcdeabcde"  # 索引 0-9

    # 限定查找范围:start=2,end=8(即子字符串 "cdeabc",索引 2-7)
    # 从该区间右侧开始找 "abc",最后一次出现是索引 5
    print(s.rfind("abc", 2, 8)) # 输出 5
    print(s.rindex("abc", 2, 8)) # 输出 5

    # 限定范围:start=0,end=5(子字符串 "abcde")
    # 该区间内 "abc" 只在索引 0 出现,从右找也是 0
    print(s.rfind("abc", 0, 5)) # 输出 0
  • 查找空字符串(特殊情况):如果 sub 是空字符串 "",两者都会返回 end - 1(因为空字符串在任何位置都存在,最后一次位置是 end 前一个索引):

    1
    2
    3
    s = "test"
    print(s.rfind("")) # 新版本输出4,3.9前的旧版本输出 3,
    print(s.rindex("", 0, 2)) # 新版本输出2,3.9前的旧版本输出 1

附录:与 index()/find() 的对比

  • find() / index():从左侧开始查找子串的第一次出现位置(未找到时 find 返回 -1,index 抛异常)
  • rfind() / rindex():从右侧开始查找子串的最后一次出现位置(未找到时行为同上)
  • 示例对比:
    1
    2
    3
    4
    5
    s = "abacada"
    print(s.find("a")) # 0(左侧第一次)
    print(s.rfind("a")) # 6(右侧第一次,即最后一次)
    print(s.index("a")) # 0
    print(s.rindex("a")) # 6
1…505152…64
Joe Zhou

Joe Zhou

Stay Hungry. Stay Foolish.

631 posts
53 tags
GitHub E-Mail
© 2026 Joe Zhou
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4