【Python】计数排序

1.1 决策树模型:比较排序的Ω(n log n)宿命 (The Decision Tree Model: The Ω(n log n) Fate of Comparison Sorts)

为了理解计数排序的革命性,我们必须首先理解它所要颠覆的“旧秩序”的边界在哪里。这个边界,可以通过一种名为**决策树(Decision Tree)**的抽象模型来清晰地描绘。

一个针对特定输入规模n的比较排序算法,其整个执行过程可以被建模为一棵二叉树。

  • 树的节点: 树的每一个内部节点,都代表一次元素之间的比较。例如,一个节点可能是 a[i] < a[j]
  • 树的边: 从一个节点出发的两条边,代表了这次比较的两种可能结果:True(是)或 False(否)。算法根据比较结果,走向下一条路径。
  • 树的叶子节点: 树的每一个叶子节点,都代表一种最终的、完全排序好的元素排列。

一个简单的例子:对3个元素 (a1, a2, a3) 进行排序

假设输入是 (a1, a2, a3)。为了得到一个排好序的序列,我们需要确定这三个元素之间的大小关系。

  • 可能的排列: 对于3个不同的元素,总共有 3! = 6 种可能的排列。例如 (a1, a2, a3), (a1, a3, a2), (a2, a1, a3) 等等。
  • 决策树的目标: 我们的排序算法,必须能够区分出这6种不同的初始排列,并最终将它们导向各自唯一正确的有序结果。因此,这棵决策树必须至少有 6 个叶子节点。

下面是对3个元素进行排序的一个可能的决策树:
(这是一个示意图,实际内容会用文字描述)

        a1 < a2 ?
       /         \
 (True)/           \(False)
      /             \
  a2 < a3 ?         a1 < a3 ?
 /    \            /      \
...   ...          ...    ...
  • 第一次比较: a1 < a2
  • 如果为True,我们知道 a1a2 前面。我们接下来需要确定a3的位置,可能需要比较a2a3
  • 如果为False,我们知道 a2a1 前面。我们接下来需要确定a3的位置,可能需要比较a1a3
  • 算法沿着这棵树一路向下,每经过一个内部节点就进行一次比较,直到抵达一个叶子节点。这个叶子节点就代表了输入序列的最终排序结果。

从决策树到时间复杂度下界

  1. 叶子节点的数量: 对于一个包含n个不同元素的输入数组,总共有 n! 种可能的初始排列。一个正确的排序算法必须能处理所有这些情况,因此,它的决策树必须至少有 n! 个叶子节点。
    [ L \ge n! ]
    其中 L 是叶子节点的数量。

  2. 树的高度: 决策树的高度 h,代表了在最坏情况下,算法需要执行的比较次数。一个高度为h的二叉树,最多拥有 2^h 个叶子节点。
    [ L \le 2^h ]

  3. 推导: 将以上两个不等式结合起来,我们得到:
    [ n! \le L \le 2^h ]
    对两边取对数:
    [ \log_2(n!) \le h ]
    根据斯特林近似公式 (Stirling’s approximation),我们知道 log(n!) 的增长率是 Θ(n log n)
    更精确地说,\log(n!) = \sum_{i=1}^{n} \log(i)
    我们可以得到一个下界:
    [ \log(n!) = \sum_{i=1}^{n} \log(i) \ge \sum_{i=n/2}^{n} \log(i) \ge \sum_{i=n/2}^{n} \log(n/2) = (n/2) \log(n/2) ]
    这个结果是 Ω(n log n)

    因此,我们得出了一个惊人的结论:
    [ h = \Omega(n \log n) ]
    这意味着,任何一个只依赖于元素间比较的排序算法,其在最坏情况下的时间复杂度,不可能优于 O(n log n)。这道“音速屏障”是由算法的“比较”本质所决定的,与具体的实现语言、硬件速度都无关。它是一个数学上的宿命。

1.2 范式转移:以键为地址的革命

既然“比较”这条路走到了尽头,那么想要获得更快的速度,唯一的出路就是——另辟蹊径,彻底抛弃比较

计数排序,以及与之同源的基数排序、桶排序,正是这次范式转移的先驱。它们的核心思想,可以被精炼地概括为:“以键为地址(Key-as-Address)”“数字的天然序(Natural Order of Numbers)”

想象一个极其简单的场景:你需要对100个学生的期末考试成绩进行排名,成绩范围是0到100分,且都是整数。

  • 比较排序的思维: 随机抽取两个学生的试卷,比较分数,分数高的放一边,分数低的放另一边… 重复这个过程,直到所有试卷都排好。
  • 计数排序的思维:
    1. 准备101个箱子,并给它们贴上标签:0号箱1号箱2号箱,…,100号箱
    2. 拿起第一份试卷,看上面的分数,比如是95分。好,把这份试卷直接扔进95号箱
    3. 拿起第二份试卷,88分,扔进88号箱
    4. …遍历完所有100份试卷。
    5. 现在,从0号箱开始,依次到101号箱,把每个箱子里的试卷拿出来。得到的序列,自然就是排好序的。

在这个过程中,我们一次都没有去比较“95分”和“88分”哪个大。我们利用了分数(键)本身作为一个“地址”或者“索引”,直接将数据(试卷)“定位”到了它该去的地方。我们利用的是数字 0, 1, 2, ..., 100 本身固有的、不证自明的顺序。

这就是计数排序思想的精髓。它绕过了决策树模型的限制,因为它根本没有构建一棵决策树。它的操作是直接寻址,而不是路径探索

革命的代价:新的约束
当然,这场革命不是没有代价的。这种“以键为地址”的强大能力,建立在两个严格的先决条件之上:

  1. 数据必须是或可映射为整数: 我们需要用数据的值来做数组的索引。这意味着,待排序的数据必须是整数,或者可以被无歧义地、稳定地映射成整数(例如字符可以映射为ASCII码)。浮点数、复杂的字符串、自定义对象等都不能直接使用。
  2. 数据范围必须是可控的: 我们需要为从最小值到最大值之间的每一个可能的值都准备一个“箱子”(即数组的槽位)。如果我们要排序的数据是手机号码(11位),即便只有100个号码,我们也需要准备一个大小接近10^11的数组,这在内存上是完全不可行的。因此,计数排序只适用于**最大值与最小值之差(即k)**不比待排序元素数量n大太多的情况。

当这些条件被满足时,计数排序就能爆发出惊人的能量,将排序的时间复杂度从O(n log n)的“超音速”级别,一举提升到O(n + k)的“光速”级别。

**1.3 计数排序的宏观蓝图 **

在深入每一行代码的细节之前,我们先从宏观上勾勒出稳定版计数排序的三个核心步骤。这个三步走的策略,是整个算法的骨架。

假设我们要对数组 A 进行排序。

  1. 第一阶段:计数 (Counting)

    • 目标: 统计出数组A中,每个不同元素出现的次数。
    • 实现: 创建一个辅助数组,我们称之为count数组。其大小为 k = max(A) - min(A) + 1
    • 流程: 遍历输入数组A。对于A中的每一个元素x,我们将count[x - min(A)]的值加一。遍历结束后,count[i]就存储了值为 i + min(A) 的元素在A中出现的总次数。
  2. 第二阶段:计算累积计数 (Cumulative Counting)

    • 目标: 将count数组从一个“频率统计”数组,转变成一个“最终位置”的指针数组。
    • 实现: 对count数组自身进行一次转换。
    • 流程: 从count数组的第二个元素开始遍历。对于每个位置i,执行 count[i] = count[i] + count[i-1]。遍历结束后,count[i]的含义变成了:小于或等于值为 i + min(A) 的元素在A中的总数量。这个值,恰好就是值为i + min(A)的元素在最终排好序的数组中,应该放置的最后一个位置的索引(从1开始计数)。
  3. 第三阶段:放置排序 (Placement)

    • 目标: 根据累积计数数组提供的位置信息,将A中的元素准确地放置到最终的输出数组B中。
    • 实现: 创建一个与A等大的输出数组B
    • 流程: 从后向前遍历输入数组A。对于A中的每一个元素x
      a. 在count数组中查找x的位置信息,即 pos = count[x - min(A)]
      b. 将元素x放置到输出数组Bpos - 1 索引处(因为数组索引从0开始)。所以 B[pos - 1] = x
      c. 更新count数组中x的位置信息,count[x - min(A)] -= 1。这样做是为了给下一个与x值相同的元素,安排在它前面的位置,从而保证稳定性
    • 完成: 当A遍历完毕,B就是一个完全排好序的、稳定的数组。

第二章:算法的解剖——从蓝图到代码的精密实现

2.1 第一阶段:计数 (The Counting Phase) - 铸造频率之镜

这是整个算法的基石。我们的目标是创建一个“频率之镜”——一个名为count_array的数组,它能精确地反映出输入数组中每个元素出现的次数。为了处理任意范围的整数(不仅仅是从0开始),我们必须首先确定数据的边界,即最小值min_value和最大值max_value。这个范围决定了我们计数数组的大小,而最小值则作为计算索引的“偏移量”。

代码实现:phase_one_counting

def phase_one_counting(input_array):
    """
    执行计数排序的第一阶段:计数。
    该函数统计输入数组中每个元素的出现频率。

    参数:
        input_array (list): 一个包含整数的输入列表。

    返回:
        一个元组,包含三个元素:
        1. count_array (list): 记录了每个元素频率的计数数组。
        2. min_value (int): 输入数组中的最小值。
        3. max_value (int): 输入数组中的最大值。
    """
    if not input_array: # 检查输入数组是否为空,处理边界情况
        return [], 0, 0 # 如果为空,则返回空的计数数组和0

    min_value = min(input_array) # 找到输入数组的最小值,作为偏移基准
    max_value = max(input_array) # 找到输入数组的最大值,以确定计数数组的大小

    # 计算计数数组所需的大小。例如,如果数据是[1, 2, 8],则范围是8-1=7,需要8个槽位(1,2,3,4,5,6,7,8)
    count_array_size = max_value - min_value + 1
    # 初始化一个全为0的计数数组
    count_array = [0] * count_array_size

    # --- 核心计数循环 ---
    # 遍历输入数组中的每一个元素
    for value in input_array:
        # 计算该元素在计数数组中对应的索引
        # 这是整个算法的关键:将元素值通过偏移量映射到数组索引
        index = value - min_value
        # 在对应索引的位置上,将计数值加一
        count_array[index] += 1
    
    return count_array, min_value, max_value

# --- 示例与追踪 ---
# 假设我们的输入数组是 A = [4, 2, 2, 8, 3, 3, 1]
A = [4, 2, 2, 8, 3, 3, 1]
print(f"原始输入数组 A: {
     A}\n")

# 执行第一阶段
counts, min_val, max_val = phase_one_counting(A)

print("--- 第一阶段:计数 完成 ---")
print(f"检测到数组最小值: {
     min_val}") # 预期输出: 1
print(f"检测到数组最大值: {
     max_val}") # 预期输出: 8
print(f"计数数组大小 (max - min + 1): {
     len(counts)}") # 预期输出: 8 - 1 + 1 = 8

# 为了更清晰地展示,我们将计数数组的索引与它代表的原始值对应起来
print("\n计数数组 (count_array) 的状态:")
print("=" * 40)
print(f"{
     '索引 (index)':<15} | {
     '代表的值 (value)':<18} | {
     '频率 (count)'}")
print("-" * 40)
for i, count in enumerate(counts):
    # 索引 i 对应的原始值是 i + min_val
    represented_value = i + min_val
    print(f"{
     i:<15} | {
     represented_value:<18} | {
     count}")
print("=" * 40)

执行追踪分析:
对于输入 A = [4, 2, 2, 8, 3, 3, 1]

  1. 确定边界: min(A)1max(A)8
  2. 创建计数数组: count_array 的大小为 8 - 1 + 1 = 8。其索引从07
  3. 遍历与计数:
    • 遇到 4index = 4 - 1 = 3count_array[3] 变为 1
    • 遇到 2index = 2 - 1 = 1count_array[1] 变为 1
    • 遇到 2index = 2 - 1 = 1count_array[1] 变为 2
    • 遇到 8index = 8 - 1 = 7count_array[7] 变为 1
    • 遇到 3index = 3 - 1 = 2count_array[2] 变为 1
    • 遇到 3index = 3 - 1 = 2count_array[2] 变为 2
    • 遇到 1index = 1 - 1 = 0count_array[0] 变为 1
  4. 最终状态:
    • count_array[0] (代表值1) = 1
    • count_array[1] (代表值2) = 2
    • count_array[2] (代表值3) = 2
    • count_array[3] (代表值4) = 1
    • count_array[4] (代表值5) = 0
    • count_array[5] (代表值6) = 0
    • count_array[6] (代表值7) = 0
    • count_array[7] (代表值8) = 1

至此,我们已经成功地将原始数组的数值信息,转换为了频率信息,存储在了一个新的维度(count_array)上。这面“频率之镜”已经铸造完成。

2.2 第二阶段:计算累积计数 (The Cumulative Counting Phase) - 绘制定位地图

现在,我们需要对这面“频率之镜”进行一次“魔法”般的转换。单纯的频率信息还不足以指导我们排序,我们需要知道每个元素在最终排好序的数组中,应该处于哪个位置。这就是第二阶段的目标:将频率数组 count_array 改造为“累积计数数组”。

转换后的 count_array[i] 将拥有一个全新的、强大的含义:它代表小于或等于其对应值(i + min_value)的元素在原始数组中一共有多少个。这个数字,恰好就是该值在排序后数组中的右边界索引(从1开始计数)。

代码实现:phase_two_cumulative_sum

def phase_two_cumulative_sum(count_array):
    """
    执行计数排序的第二阶段:计算累积和。
    这个函数会原地修改输入的计数数组。

    参数:
        count_array (list): 由第一阶段生成的频率计数数组。

    返回:
        None (函数直接在传入的列表上进行修改)。
    """
    # 从计数数组的第二个元素开始 (索引为1)
    for i in range(1, len(count_array)):
        # 当前位置的累积计数值 = 当前位置的原始计数值 + 前一个位置的累积计数值
        count_array[i] += count_array[i-1]

# --- 延续上面的示例 ---
# 我们使用第一阶段生成的 counts 数组
print("\n--- 第二阶段:计算累积计数 开始 ---")
print(f"转换前的 count_array: {
     counts}")

# 执行第二阶段,原地修改 counts 数组
phase_two_cumulative_sum(counts)

print(f"转换后的 count_array: {
     counts}")

print("\n累积计数数组 (count_array) 的新状态和含义:")
print("=" * 70)
print(f"{
     '索引 (index)':<15} | {
     '代表的值 (value)':<18} | {
     '累积计数 (含义)'}")
print("-" * 70)
for i, count in enumerate(counts):
    represented_value = i + min_val
    meaning = f"<= {
     represented_value} 的元素共有 {
     count} 个"
    print(f"{
     i:<15} | {
     represented_value:<18} | {
     meaning}")
print("=" * 70)

执行追踪分析:
我们以上一阶段的结果 counts = [1, 2, 2, 1, 0, 0, 0, 1] 为输入:

  1. i = 1: counts[1] = counts[1] + counts[0] = 2 + 1 = 3。含义:小于等于2的元素共有3个。
  2. i = 2: counts[2] = counts[2] + counts[1] = 2 + 3 = 5。含义:小于等于3的元素共有5个。
  3. i = 3: counts[3] = counts[3] + counts[2] = 1 + 5 = 6。含义:小于等于4的元素共有6个。
  4. i = 4: counts[4] = counts[4] + counts[3] = 0 + 6 = 6。含义:小于等于5的元素共有6个。
  5. i = 5: counts[5] = counts[5] + counts[4] = 0 + 6 = 6。含义:小于等于6的元素共有6个。
  6. i = 6: counts[6] = counts[6] + counts[5] = 0 + 6 = 6。含义:小于等于7的元素共有6个。
  7. i = 7: counts[7] = counts[7] + counts[6] = 1 + 6 = 7。含义:小于等于8的元素共有7个。

最终的累积计数数组: [1, 3, 5, 6, 6, 6, 6, 7]
这已经不再是简单的频率统计,而是一张精确的“定位地图”。例如,counts[2](代表值3)的值是5,这告诉我们,在最终排好序的数组里,最后一个3应该被放在索引为5-1=4的位置上。

2.3 第三阶段:放置排序 (The Placement Phase) - 依图寻位与稳定性的奥秘

这是最后,也是最精妙的一步。我们将利用刚刚绘制好的“定位地图”,将原始数组 A 中的每一个元素,精准地安放到最终的输出数组 B 中。

这里有一个至关重要的细节,也是计数排序能够成为稳定排序算法的关键:我们必须从后向前遍历原始输入数组 A。为什么?让我们在代码实现后来深入探讨这个“稳定性的奥秘”。

代码实现:phase_three_placement

def phase_three_placement(input_array, cumulative_counts, min_value):
    """
    执行计数排序的第三阶段:放置排序。
    根据累积计数数组,将元素放置到新的输出数组中。

    参数:
        input_array (list): 原始的、未排序的输入列表。
        cumulative_counts (list): 经过第二阶段处理后的累积计数数组。
        min_value (int): 输入数组中的最小值(用于计算偏移)。

    返回:
        output_array (list): 一个新的、已排序的列表。
    """
    # 创建一个和输入数组同样大小的输出数组,初始值可以任意,这里用None填充
    output_array = [None] * len(input_array)
    
    # --- 核心放置循环:从后向前遍历输入数组 ---
    # range(len(input_array) - 1, -1, -1) 生成一个从 len-1 到 0 的逆序序列
    for i in range(len(input_array) - 1, -1, -1):
        # 获取当前遍历到的元素值
        value = input_array[i]
        
        # 1. 在累积计数数组中查找该元素的位置信息
        #    首先计算它在计数数组中的索引
        count_index = value - min_value
        #    然后获取该元素在排序后数组中的目标位置(基于1的索引)
        position_in_output = cumulative_counts[count_index]
        
        # 2. 将元素放置到输出数组的正确位置
        #    因为 position_in_output 是基于1的索引,所以数组下标是 position - 1
        output_array[position_in_output - 1] = value
        
        # 3. 更新累积计数数组中的值,将其减一
        #    为下一个相同值的元素“预留”出它前面的位置
        cumulative_counts[count_index] -= 1
        
        # --- 单步追踪 (用于教学) ---
        print(f"处理 A[{
     i}] = {
     value}:")
        print(f"  -> 查地图: cumulative_counts[{
     count_index}] = {
     position_in_output}")
        print(f"  -> 放置到 output_array[{
     position_in_output - 1}]")
        print(f"  -> 更新地图: cumulative_counts[{
     count_index}] 变为 {
     cumulative_counts[count_index]}")
        print(f"  当前 output_array 状态: {
     output_array}\n")

    return output_array

# --- 延续上面的示例 ---
# A = [4, 2, 2, 8, 3, 3, 1]
# min_val = 1
# cumulative_counts 在此阶段开始前是 [1, 3, 5, 6, 6, 6, 6, 7]
print("\n--- 第三阶段:放置排序 开始 ---")
# 为了不影响后续完整函数的调用,我们复制一份
cumulative_counts_for_phase3 = counts[:] 
sorted_A = phase_three_placement(A, cumulative_counts_for_phase3, min_val)

print("\n--- 第三阶段:放置排序 完成 ---")
print(f"最终排序后的数组: {
     sorted_A}") # 预期输出: [1, 2, 2, 3, 3, 4, 8]

稳定性的奥秘:为何必须从后向前遍历?

让我们用一个带有重复项的、更需要体现稳定性的例子来说明:A = [(3, 'a'), (2, 'b'), (3, 'c')]。我们只按数字排序。

  • min=2, max=3count_array大小为2。
  • 阶段一后: counts = [1, 2] (1个2,2个3)
  • 阶段二后: counts = [1, 3] (<=2的有1个,<=3的有3个)

场景一:从后向前遍历 (正确方式)

  1. 处理 A[2] = (3, ‘c’):
    • value=3, count_index=1.
    • pos = counts[1] = 3.
    • output[2] = (3, 'c').
    • counts[1] 变为 2.
    • output = [None, None, (3, 'c')]
  2. 处理 A[1] = (2, ‘b’):
    • value=2, count_index=0.
    • pos = counts[0] = 1.
    • output[0] = (2, 'b').
    • counts[0] 变为 0.
    • output = [(2, 'b'), None, (3, 'c')]
  3. 处理 A[0] = (3, ‘a’):
    • value=3, count_index=1.
    • pos = counts[1] = 2.
    • output[1] = (3, 'a').
    • counts[1] 变为 1.
    • output = [(2, 'b'), (3, 'a'), (3, 'c')]

最终结果 [(2, 'b'), (3, 'a'), (3, 'c')]。观察两个3(3, 'a') 在原始数组中先出现,在排序后数组中也先出现。稳定性得到保持。这是因为我们是从后向前处理的,原始数组中靠后的相同元素,会先被放置到最终位置的靠后的槽位里。

场景二:从前向后遍历 (错误方式)

  1. 处理 A[0] = (3, ‘a’):
    • value=3, count_index=1.
    • pos = counts[1] = 3.
    • output[2] = (3, 'a').
    • counts[1] 变为 2.
    • output = [None, None, (3, 'a')]
  2. 处理 A[1] = (2, ‘b’):
    • value=2, count_index=0.
    • pos = counts[0] = 1.
    • output[0] = (2, 'b').
    • counts[0] 变为 0.
    • output = [(2, 'b'), None, (3, 'a')]
  3. 处理 A[2] = (3, ‘c’):
    • value=3, count_index=1.
    • pos = counts[1] = 2.
    • output[1] = (3, 'c').
    • counts[1] 变为 1.
    • output = [(2, 'b'), (3, 'c'), (3, 'a')]

最终结果 [(2, 'b'), (3, 'c'), (3, 'a')]。观察两个3(3, 'a') 在原始数组中先出现,但在排序后数组中,(3, 'c') 跑到了它前面。稳定性被破坏

这个简单的对比,深刻地揭示了“从后向前遍历”在保证计数排序稳定性中不可替代的核心作用。

2.4 宏伟的统一:完整的 counting_sort 函数

现在,我们已经分别剖析并实现了三个阶段。是时候将它们组装起来,形成一个完整、健壮、可用的 counting_sort 函数了。

def counting_sort(input_array):
    """
    一个完整、稳定且能处理负数的计数排序实现。

    参数:
        input_array (list): 包含整数(可正可负)的待排序列表。

    返回:
        一个与输入列表具有相同元素,但已稳定排序的新列表。
    """
    # 处理空列表或只有一个元素的列表的边界情况,直接返回其拷贝
    if len(input_array) <= 1:
        return input_array[:]

    # --- 阶段一: 计数 ---
    min_value = min(input_array) # 找到最小值,用于处理偏移
    max_value = max(input_array) # 找到最大值,确定计数数组范围

    count_array_size = max_value - min_value + 1 # 计算计数数组大小
    count_array = [0] * count_array_size # 初始化计数数组

    for value in input_array: # 遍历输入数组
        count_array[value - min_value] += 1 # 在对应位置上增加频率计数

    # --- 阶段二: 计算累积计数 ---
    for i in range(1, count_array_size): # 遍历计数数组
        count_array[i] += count_array[i-1] # 将其转换为累积计数数组

    # --- 阶段三: 放置排序 ---
    output_array = [None] * len(input_array) # 创建输出数组

    # 从后向前遍历原始输入数组以保证稳定性
    for i in range(len(input_array) - 1, -1, -1):
        value = input_array[i] # 获取当前元素
        
        # 从累积计数数组中找到它的目标位置(基于1的索引)
        position = count_array[value - min_value] 
        # 将其放入输出数组的正确槽位(基于0的索引)
        output_array[position - 1] = value
        
        # 将该位置的计数值减一,为下一个相同元素准备
        count_array[value - min_value] -= 1
        
    return output_array

# --- 最终测试 ---
test_data_1 = [4, 2, 2, 8, 3, 3, 1]
print(f"\n--- 完整函数测试 1 ---")
print(f"输入: {
     test_data_1}")
sorted_data_1 = counting_sort(test_data_1)
print(f"输出: {
     sorted_data_1}")
# 验证结果
assert sorted_data_1 == sorted
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宅男很神经

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值