漫画算法读书笔记

漫画算法:小灰的算法之旅

算法概述

算法
  1. 在数学领域里,算法是用于解决某一类问题的公式和思想。
  2. 计算机领域里,本质是一系列程序指令,用于解决特定的运算和逻辑问题。
    衡量算法优劣的主要标准是时间复杂度和空间复杂度。
数据结构

数据结构是数据的组织、管理和存储格式,其使用目的是高效地访问和修改数据。
数据结构的组成方式:

  1. 线性结构
    • 数组
    • 链表
    • 队列
    • 哈希表
  2. 其他数据结构
    • 跳表
    • 哈希链表
    • 位图
时间复杂度
  1. 基本操作执行次数的函数T(n)。
  2. 渐进时间复杂度(大O表示法)
    • 定义
      若存在函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称为O(f(n)),O为算法的渐进时间复杂度,简称为时间复杂度。

    • 原则

      1. 如果运行时间是常数量级,则用常数1表示。
      2. 只保留时间函数中的最高阶项。
      3. 如果最高阶项存在,则省去最高阶项前面的系数。
    • 常见复杂度
      O(1)<O(logn)<O(n)<O(n2)

空间复杂度
  1. 常见情形
    • 常量空间(O(1))
    • 线性空间(O(n))
    • 二维空间(O(n²))
    • 递归空间(O(n))
      递归是一个比较特殊的场景。虽然递归代码中并没有显式地声明变量或集合,但是计算机在执行程序时,会专门分配一块内存,用来存储“函数调用栈”。
      “函数调用栈”包括进栈和出栈两个行为。
      当进入一个新函数时,执行入栈操作,把调用的函数和参数信息压入栈中。
      当函数返回时,执行出栈操作,把调用的函数和参数信息从栈中弹出。
      纯粹的递归操作的空间复杂度也是线性的,如果递归的深度是n,那么空间复杂度就是O(n)

数据结构基础

数组
  1. 什么是数组
    数组对应的英文是array,是有限个相同类型的变量所组成的有序集合,数组中的每一个变量称为元素。数组是最简单、最常用的数据结构。
    注:在Python语言中,并没有直接使用数组这个概念,而是使用列表(list)和元组(tuple)这两种集合,它们本质上都是对数组的封装。其中,列表是一个动态可扩展的数组,支持任意地添加、删除、修改元素;而元组是一个不可变集合,一旦创建就不再支持修改。

  2. 存储方式
    数组在内存中的存储方式是顺序存储

  3. 数据的基本操作

    • 读取元素
      根据下表读取元素,也叫随机读取。
    • 更新元素
      要把数组中某一个元素的值替换为一个新值,直接利用数组下标,就可以把新值赋给该元素。
    • 插入元素
      1. 尾部插入
        直接在数组尾部插入元素
      2. 中间插入
        先把插入位置及后面的元素向后移动,腾出地方,再插入(python的列表底层用c编写,如果插入的位置大于原列表的最后一个位置,就插入在最后一个位置)
      3. 超范围插入
        数组的长度在创建时就已经确定了,如果原数组已经装满元素了,还想插入元素,可以创建一个新数组,长度是旧数组的2倍,再把旧数组中的元素统统复制过去,这样就实现了数组的扩容。
    • 删除元素
      数组的删除操作和插入操作的过程相反,如果删除的元素位于数组中间,其后的元素都需要向前挪动1位。
  4. 数组的优劣势

    • 优势
      拥有非常高效的随机访问能力,只要给出下标,就可以用常量时间找到对应元素。
    • 劣势
      劣势体现在插入和删除元素方面。由于数组元素连续紧密地存储在内存中,插入、删除元素都会导致大量元素被迫移动,影响效率。
      总的来说,适合读操作多、写操作少的场景。
链表
  1. 什么是链表
    链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。

  2. 存储方式
    链表在内存中的存储方式则是随机存储。

  3. 链表的基本操作

    • 查找节点
      链表不像数组那样可以通过下标快速进行定位,只能从头节点开始向后一个一个节点逐一查找。
    • 更新节点
      如果不考虑查找节点的过程,链表的更新过程会像数组那样简单,直接把旧数据替换成新数据即可。
    • 插入节点
      1. 尾部插入
        把最后一个节点的next指针指向新插入的节点即可。
      2. 头部插入
        头部插入,可以分成两个步骤:
        第1步,把新节点的next指针指向原先的头节点。
        第2步,把新节点变为链表的头节点。
      3. 中间插入
        中间插入,同样分为两个步骤:
        第1步,新节点的next指针指向插入位置的节点。
        第2步,插入位置前置节点的next指针,指向新节点。
    • 删除节点
      1. 尾部删除
        把倒数第2个节点的next指针指向空即可。
      2. 头部删除
        把链表的头节点设为原先头节点的next指针所指向的节点即可。
      3. 中间删除
        把要删除节点的前置节点的next指针,指向要删除元素的下一个节点即可。
        注:许多高级语言,如Java、Python,拥有自动化的垃圾回收机制,所以我们不用刻意去释放被删除的节点,只要没有外部引用指向它们,被删除的节点会被自动回收。
  4. 链表的优劣势

    • 优势
      更新、插入、删除操作更灵活。
    • 劣势
      劣势体现在查找的时候只能从头节点一个一个查找
      总的来说,适合频繁插入、删除元素的场景。
栈和队列
  1. 物理结构和逻辑结构
    物理结构是内存中实实在在的存储结构,数组和链表都是物理结构。
    逻辑结构是抽象的概念,它依赖于物理结构而存在。


  2. 栈(stack)是一种线性数据结构,栈中的元素只能先入后出(First In Last Out,简称FILO)。最早进入的元素存放的位置叫作栈底(bottom),最后进入的元素存放的位置叫作栈顶(top)。

  3. 栈的基本操作

    • 入栈(push)
    • 出栈(pop)
  4. 队列
    队列(queue)是一种线性数据结构,队列中的元素只能先入先出(First In First Out,简称FIFO)。队列的出口端叫作队头(front),队列的入口端叫作队尾(rear)。

  5. 队列的基本操作

    • 入队(enqueue)
    • 出队(dequeue)
  6. 栈的应用
    栈的输出顺序和输入顺序相反,所以栈通常用于对“历史”的回溯,也就是逆流而上追溯“历史”。
    egg:

    1. 实现递归的逻辑,就可以用栈来代替,因为栈可以回溯方法的调用链。
    2. 面包屑导航,使用户在浏览页面时可以轻松地回溯到上一级或更上一级页面。
  7. 队列的应用
    队列的输出顺序和输入顺序相同,所以队列通常用于对“历史”的回放,也就是按照“历史”顺序,把“历史”重演一遍。
    egg:

    1. 多线程中,争夺公平锁的等待队列,就是按照访问顺序来决定线程在队列中的次序的。
    2. 再如网络爬虫实现网站抓取时,也是把待抓取的网站URL存入队列中,再按照存入队列的顺序来依次抓取和解析的。
  8. 双端队列

  9. 优先队列
    谁的优先级最高,谁先出队。不属于线性数据结构的范畴了,它是基于二叉堆来实现的。

哈希表
  1. 定义
    哈希表(hash table)也叫作散列表,这种数据结构提供了键(Key)和值(Value)的映射关系。只要给出一个Key,就可以高效查找到它所匹配的Value,时间复杂度接近于O(1)。

  2. 哈希函数

  3. 哈希表的读写操作

    • 写操作
      写操作就是在哈希表中插入新的键值对(也被称为Entry)。通过哈希函数将key值转换为数组的下标,在对应的下标位置上插入数据。

    • 读操作
      读操作就是通过给定的Key,在哈希表中查找对应的Value。通过哈希函数将key值转换为数组的下标,查看对应下标的元素的key是不是一致,是就找到了,不是就继续往下找。

  4. 哈希冲突

    • 开放寻址法
      当一个Key通过哈希函数获得对应的数组下标已被占用时,寻找下一个空当位置。
      egg:Python中的dict

    • 链表法
      哈希表数组的每一个元素不仅是一个Entry对象,还是一个链表的头节点。每一个 Entry对象通过next指针指向它的下一个Entry节点。当 新来的Entry映射到与之冲突的数组位置时,只需要插入对应的链表中即可。
      egg:Java中的HashMap

树和二叉树
  1. 定义
    树(tree)是n(n≥0)个节点的有限集。当n=0时,称为空树。在任意一个非空树中,有如下特点。

    • 有且仅有一个特定的称为根的节点。
    • 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。
  2. 二叉树
    二叉树(binary tree)是树的一种特殊形式。二叉,顾名思义,这种树的每个节点最多有2个孩子节点。注意,这里是最多有2个,也可能只有1个,或者没有孩子节点。

  3. 满二叉树
    一个二叉树的所有非叶子节点都存在左孩子和右孩子,并且所有叶子节点都在同一层级上,那么这个树就是满二叉树。

  4. 完全二叉树
    对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个树所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同,则这个二叉树为完全二叉树。

  5. 二叉树物理存储结构

    • 链式存储结构
      1. 存储数据的data变量
      2. 指向左孩子的left指针
      3. 指向右孩子的right指针
    • 数组
      按照层级顺序把二叉树的节点放到数组中对应的位置上。如果某一个节点的左孩子或右孩子空缺,则数组的相应位置也空出来。
      1. 假设一个父节点的下标是parent,那么它的左孩子节点的下标就是2×parent +1;右孩子节点的下标就是2×parent +2。
      2. 反过来,假设一个左孩子节点的下标是leftChild,那么它的父节点下标就是(leftChild-1)/2。
  6. 二叉树的应用

    • 二叉查找树(binary search tree)
      也叫二叉排序树(binary sort tree)。
      对于一个节点分布相对均衡的二叉查找树来说,如果节点总数是n,那么搜索节点的时间复杂度就是O(logn),和树的深度是一样的。
      二叉查找树在二叉树的基础上增加了以下几个条件:
      1. 如果左子树不为空,则左子树上所有节点的值均小于根节点的值。
      2. 如果右子树不为空,则右子树上所有节点的值均大于根节点的值。
      3. 左子树、右子树也都是二叉查找树。
二叉树的遍历
  1. 深度优先遍历

    1. 前序遍历
    2. 中序遍历
    3. 后序遍历
    • 遍历方法:
      1. 递归
      2. 栈(每一步都跟上一步有直接关联)
  2. 广度优先遍历

    1. 层序遍历
    • 遍历方法:
      1. 队列(每一步都没跟上一步有直接关联,跟顺序有关)
二叉堆
  1. 定义
    二叉堆本质上是一种完全二叉树,它分为两种类型:最大堆、.最小堆。

    • 最大堆的任何一个父节点的值,都大于或等于它左孩子或右孩子节点的值。
    • 最小堆的任何一个父节点的值,都小于或等于它左孩子或右孩子节点的值。
  2. 二叉堆的自我调整

    1. 插入节点
      • 当在二叉堆中插入节点时,插入位置是完全二叉树的最后一个位置。
      • 新节点跟父节点比较,如果不符合规则则跟父节点交换位置(上浮)。
      • 继续上一个步骤,直到符合规则就完成了。
    2. 删除节点
      • 把最后一个节点临时补上堆顶,这个节点跟它的左右孩子节点比较。
      • 如果不符合规则,假设是最小堆,则跟最小的那个孩子节点互换位置(下沉)。
      • 继续上一个步骤,直到符合规则就完成了。
    3. 构建二叉堆
      构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质就是让所有非叶子节点依次“下沉”。
      • 假设是最小堆,从最后一个非叶子节点开始,如果节点大于它的左孩子、右孩子节点中最小的一个,则跟最小的那个孩子节点互换位置(下沉)。
      • 按层序遍历的方法往回找下一个非叶子节点,继续跟上一步的操作方式一样。
      • 直到判断并处理完根节点,完成构建。
  3. 二叉堆的存储方式
    二叉堆虽然是一个完全二叉树,但它的存储方式并不是链式存储,而是顺序存储。换句话说,二叉堆的所有节点都存储在数组中。

  4. 二叉堆的用途
    二叉堆是实现堆排序及优先队列的基础。

优先队列
  1. 定义
    优先队列不再遵循先入先出的原则,而是分为两种情况:

    1. 最大优先队列,无论入队顺序如何,都是当前最大的元素优先出队。
    2. 最小优先队列,无论入队顺序如何,都是当前最小的元素优先出队。
  2. 实现
    利用二叉堆的特性实现(因为不需要全排序,只要直到最大最小),入队跟出队的时间复杂度都是O(logn)

排序算法

  1. 常见算法

    • 时间复杂度为O(n²)的排序算法

      1. 冒泡排序
      2. 选择排序
      3. 插入排序
      4. 希尔排序(希尔排序比较特殊,它的性能略优于O(n²),但又比不上O(nlogn),姑且把它归入本类)
    • 时间复杂度为O(nlogn)的排序算法

      1. 快速排序
      2. 归并排序
      3. 堆排序
    • 时间复杂度为线性的排序算法

      1. 计数排序
      2. 桶排序
      3. 基数排序
  2. 冒泡排序

    • 定义
      一种基础的交换排序。
    • 时间复杂度
      冒泡排序是一种稳定排序,值相等的元素并不会打乱原本的顺序。由于该排序算法的每一轮都要遍历所有元素,总共遍历(元素数量-1)轮,所以平均时间复杂度是O(n²)
    • 优化
      1. 利用一个布尔值记录进行到m轮的时候是否已经有序(没有元素交换)的了,如果是,停止循环。
      2. 记录无序边界,超过无序边界的不用再比较。
      3. 鸡尾酒排序,左右轮流循环,目的是减少循环的次数。
  3. 快速排序

    • 定义
      快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成两个部分。
    • 时间复杂度
      每一轮的比较和交换,需要把数组中的全部元素都遍历一遍,时间复杂度是O(n)。这样的遍历一共需要多少轮呢?假如元素个数是n,那么平均情况下需要logn轮,因此快速排序算法总体的平均时间复杂度是O(nlogn)。最坏情况是O(n²)
  4. 堆排序

    • 步骤

      1. 把无序数组构建成二叉堆。需要从小到大排序,则构建成最大堆;需要从大到小排序,则构建成最小堆。
      2. 循环删除堆顶元素,替换到二叉堆的末尾,调整堆产生新的堆顶。
    • 时间复杂度
      第1步,把无序数组构建成二叉堆,这一步的时间复杂度是O(n)。
      第2步,需要进行n-1次循环。每次循环调用一次down_adjust方法,所以第2步的计算规模是(n-1)×logn,时间复杂度为O(nlogn)。
      两个步骤是并列关系,所以整体的时间复杂度是O(nlogn)。

    • 堆排序和快速排序异同点

      1. 堆排序和快速排序的平均时间复杂度都是O(nlogn),并且都是不稳定排序。
      2. 快速排序的最坏时间复杂度是O(n²),而堆排序的最坏时间复杂度稳定在O(nlogn)。
      3. 快速排序递归和非递归方法的平均空间复杂度都是O(logn),而堆排序的空间复杂度是O(1)。
  5. 计数排序

    • 时间复杂度
      假设原始数列的规模是n,最大整数和最小整数的差值是m,空间复杂度O(m),时间复杂度是O(n+m)。
    • 局限性
      1. 当数列最大和最小值差距过大时,并不适合用计数排序。
      2. 当数列元素不是整数时,也不适合用计数排序。
  6. 桶排序

    • 时间复杂度
      1. 求数列最大值、最小值,运算量为n。
      2. 创建空桶,运算量为n。
      3. 把原始数列的元素分配到各个桶中,运算量为n。
      4. 在每个桶内部做排序,在元素分布相对均匀的情况下,所有桶的运算量之和为n。
      5. 输出排序数列,运算量为n。
      6. 桶排序的总体时间复杂度为O(n)。

面试中的算法

链表有环
  1. 如何判断

    • 暴力二次循环
      从头节点开始,依次遍历单链表中的每一个节点。每遍历一个新节点,就从头检查新节点之前的所有节点,用新节点和此节点之前所有节点依次做比较。如果发现新节点和之前的某个节点相同,则说明该节点被遍历过两次,链表有环;如果之前的所有节点中不存在与新节点相同的节点,就继续遍历下一个新节点,继续重复刚才的操作。
    • 一次循环+空间
      创建一个以节点ID为Key的set集合,用来存储曾经遍历过的节点。然后同样从头节点开始,依次遍历单链表中的每一个节点。每遍历一个新节点,都用新节点和set集合中存储的节点进行比较,如果发现set中存在与之相同的节点ID,则说明链表有环,如果set中不存在与新节点相同的节点ID,就把这个新节点ID存入 set中,之后进入下一节点,继续重复刚才的操作。
    • 追及解
      创建两个指针p1和p2(在Python里就是两个对象引用),让它们同时指向这个链表的头节点。然后开始一个大循环,在循环体中,让指针p1每次向后移动1个节点,让指针p2每次向后移动2个节点,然后比较两个指针指向的节点是否相同。如果相同,则可以判断出链表有环,如果不同,则继续下一次循环。
  2. 拓展

    • 环长
      首次相遇之后继续走,首次相遇到第二次相遇两个指针步差。
    • 入环点
      数学推导得出,只要把其中一个指针放回到头节点位置,另一个指针保持在首次相遇点,两个指针都是每次向前走1步。那么,它们最终相遇的节点,就是入环节点。
最小栈的实现
  • 题目
    实现一个栈,该栈带有出栈(pop)、入栈(push)、取最小元素(get_min)3个方法。要保证这3个方法的时间复杂度都是O(1)。

    1. 原有的栈叫作栈A,此时创建一个额外的“备胎”栈B,用于辅助栈A。
    2. 当第1个元素进入栈A时,让新元素也进入栈B。这个唯一的元素是栈A的当前最小值。
    3. 之后,每当新元素进入栈A时,比较新元素和栈A当前最小值的大小,如果小于栈A当前最小值,则让新元素进入栈B,此时栈B的栈顶元素就是栈A当前最小值。
    4. 每当栈A有元素出栈时,如果出栈元素是栈A当前最小值,则让栈B的栈顶元素也出栈。此时栈B余下的栈顶元素所指向的,是栈A中原本第2小的元素,代替刚才的出栈元素成为栈A的当前最小值。
    5. 当调用get_min方法时,返回栈B的栈顶所存储的值,这也是栈A的最小值。
最大公约数
  • 题目
    写一段代码,求出两个整数的最大公约数,要尽量优化算法的性能。

    1. 暴力枚举

      • 时间复杂度
        O(min(a,b))
    2. 辗转相除法
      两个正整数a和b(a>b),它们的最大公约数等于a除以b的余数c和b之间的最大公约数。
      缺点是如果两个整数比较大时,取模运算性能会比较差。

      • 时间复杂度
        时间复杂度不太好计算,可以近似为O(log(max(a,b)),但是取模运算性能较差。
    3. 更相减损术
      两个正整数a和b(a>b),它们的最大公约数等于a-b的差值c和较小数b的最大公约数。
      缺点是如果两个数差很大时,要运算次数会很多。

      • 时间复杂度
        避免了取模运算,但是算法性能不稳定,最坏时间复杂度为O(max(a, b))。
    4. 移位运算

      1. 当a和b均为偶数时,gcd(a,b)=2×gcd(a/2, b/2)=2×gcd(a>>1, b>>1)。
      2. 当a为偶数,b为奇数时,gcd(a, b)= gcd(a/2, b)= gcd(a>>1, b)。
      3. 当a为奇数,b为偶数时,gcd(a, b)= gcd(a, b/2)= gcd(a, b>>1)。
      4. 当a和b均为奇数时,先利用更相减损术运算一次,gcd(a, b)= gcd(b, a-b),此时a-b必然是偶数,然后又可以继续进行移位运算。
      • 时间复杂度
        不但避免了取模运算,而且算法性能稳定,时间复杂度为O(log(max(a, b)))。
判断2的整数次幂
  • 题目
    实现一个方法,来判断一个正整数是否是2的整数次幂(如16是2的4次方,返回true;18不是2的整数次幂,则返回false),要求性能尽可能高。

    1. 暴力除2
    2. 位运算
      如果一个数是2的整数次幂,假设这个数是n,则n & n-1 = 0。
无序数组排序后的最大相邻差
  • 题目
    有一个无序整型数组,如何求出该数组排序后的任意两个相邻元素的最大差值?要求时间复杂度和空间复杂度尽可能低。

    1. 计数排序
    2. 桶排序(最优)
如何用栈实现队列
  • 题目
    用栈来模拟一个队列,要求实现队列的两个基本操作:入队、出队。


  • 两个栈,一个做入队,一个做出队。当出队的栈为空的时候入队的栈将元素压入出队的栈。

寻找全排列的下一个数
  • 题目
    给出一个正整数,找出这个正整数所有数字全排列的下一个数。


  • 字典序算法。

    1. 从后向前查看逆序区域,找到逆序区域的前一位,也就是数字置换的边界。
    2. 让逆序区域的前一位和逆序区域中大于它的最小的数字交换位置。
    3. 把原来的逆序区域转为顺序状态。
删去k个数字后的最小值
  • 题目
    给出一个整数,从该整数中去掉k个数字,要求剩下的数字形成的新整数尽可能小。应该如何选取被去掉的数字。


  • 思路:原整数的所有数字从左到右进行比较,如果发现某一位数字大于它右面的数字,那么在删除该数字后,必然会使该数位的值降低,因为右面比它小的数字顶替了它的位置。
    实现:运用了栈的特性,在遍历原整数的数字时,让所有数字一个一个入栈,当某个数字需要被删除时,让该数字出栈。最后,程序把栈中的元素转化为字符串类型的结果。

找到两个数组的中位数
  • 题目
    给定两个升序数组,如何找出这两个数组归并以后新的升序数组的中位数。

  • 在这里插入图片描述

利用数学公式:i+j=(m+n+1)/2
由于m+n的值是恒定的,所以我们只要确定一个合适的i,就可以确定j,从而找到大数组左半部分和右半部分的分界,也就找到了归并之后大数组的中位数。

  1. 二分查找那样,把i设在数组A的正中位置。

  2. 根据i的值来确定j的值,j=(m+n+1)/2-i。

  3. 验证:

    • B[j-1]≤A[i] && A[i-1]≤B[j]
      说明i和j左侧的元素都小于或等于右侧的元素,这一组i和j是我们想要的。
    • A[i]<B[j-1]
      说明i对应的元素偏小了,i应该向右侧移动。
    • A[i-1]>B[j]
      说明i-1对应的元素偏大了,i应该向左侧移动。
  4. 在数组A的右半部分,重新确定i的位置。
    在这里插入图片描述

  5. 同第二步,根据i的值来确定j的值,j=(m+n+1)/2-i。
    在这里插入图片描述

  6. 同第三步,验证i和j。
    如果大数组的长度是奇数,那么:
    中位数= Max(A[i-1], B[j-1])
    (也就是大数组左半部分的最大值。)

    如果大数组的长度是偶数,那么:
    中位数=(Max(A[i-1], B[j-1]) + Min(A[i], B[i]))/2
    (也就是大数组左半部分的最大值和大数组右半部分的最小值取平均。)

  7. 特殊情况

    • 数组A的长度远大于数组B
      提前把数组A和数组B进行交换
    • 无法找到合适的i值
      1. 数组A的长度小于数组B的长度,并且数组A的所有元素都大于数组B的元素。
        可以跳出二分查找的循环,所求的中位数是B[j-1]。(仅限奇数情况。)
      2. 数组A的长度小于数组B的长度,并且数组A的所有元素都小于数组B的元素。
        可以跳出二分查找的循环,所求的中位数是Max(A[i-1], B[j-1])。(仅限奇数情况。)

算法的实际应用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值