字节跳动面试官:说说HashMap 的设计与优化?(1),牛客视频面试链接显示第二轮

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

else

xp.right = x;

xp.next = x;

x.parent = x.prev = xp;

if (xpn != null)

((TreeNode<K,V>)xpn).prev = x;

moveRootToFront(tab, balanceInsertion(root, x));

return null;

}

}

}

treeifyBin 链表树化

===============

如果 hashmap 的长度小于 64 会优先选择拓容,否则会当前冲突 key 所在的结构由链表转换为红黑树。 这个是 jdk 1.8 才有的新特征,hashmap 在 hash 冲突后可能由链表变化为红黑树结构。这样做的目的是为了提高读写效率。

final void treeifyBin(Node<K,V>[] tab, int hash) {

int n, index; Node<K,V> e;

// 不一定树化还可能是拓容,需要看数组的长度是否小于 64 MIN_TREEIFY_CAPACITY

if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)

resize();

else if ((e = tab[index = (n - 1) & hash]) != null) {

// hd 头节点, tl 尾节点

TreeNode<K,V> hd = null, tl = null;

do {

// 将链表转换为树结构

TreeNode<K,V> p = replacementTreeNode(e, null);

if (tl == null)

hd = p;

else {

p.prev = tl;

tl.next = p;

}

tl = p;

} while ((e = e.next) != null);

if ((tab[index] = hd) != null)

// 转换红黑树操作,这里循环比较,染色、旋转等

hd.treeify(tab);

}

}

replacementTreeNode 方法

======================

replacementTreeNode 方法主要是将 Node 转换为 TreeNode

TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {

return new TreeNode<>(p.hash, p.key, p.value, next);

}

TreeNode#treeify 方法

===================

treeify 方法主要是将树结构转换为红黑树。

final void treeify(Node<K,V>[] tab) {

// 根节点默认为 null

TreeNode<K,V> root = null;

// 链表遍历,x 指向当前节点,next 指向下一个节点

for (TreeNode<K,V> x = this, next; x != null; x = next) {

// 下一个节点

next = (TreeNode<K,V>)x.next;

// 设置当前节点的 left, right 为 null

x.left = x.right = null;

// 如果没有根节点

if (root == null) {

// 当前父节点为 null

x.parent = null;

// 当前红色节点属性设置为 false (把当前节点设置为黑色)

x.red = false;

// 根节点指向当前节点

root = x;

}

// 如果已经存在根节点

else {

// 获取当前链表的 key

K k = x.key;

// 获取当前节点的 hash

int h = x.hash;

// 定义当前 key 所属类型

Class<?> kc = null;

// 从根节点开始遍历,此遍历设置边界,只能从内部跳出

for (TreeNode<K,V> p = root;😉 {

// dir 标识方向(左右)ph 标识当前节点的 hash 值

int dir, ph;

// 当前节点的 key

K pk = p.key;

// 如果当前节点 hash 值大于当前 链表节点的 hash 值

if ((ph = p.hash) > h)

// 标识当前节链表节点会放在当前红黑树节点的左侧

dir = -1;

else if (ph < h)

// 右侧

dir = 1;

// 如果两个节点的 key 的 hash 值相等,那么通过其他方式进行比较

// 如果当前链表节点的 key 实现了comparable 接口,并且当前树节点和链表节点是相同的 class 实例,那么通过 comparable 比较

// 如果还是相等再通过 tieBreakOrder 比较一次

else if ((kc == null &&

(kc = comparableClassFor(k)) == null) ||

(dir = compareComparables(kc, k, pk)) == 0)

dir = tieBreakOrder(k, pk);

TreeNode<K,V> xp = p; // 保存当前树节点

// 如果 dir 小于等于 0: 当前链表节点一定放置在当前树节点的左侧,但不一定是该树节点的左子节点,

// 也可能是左子节点或者右子节点或者更深层次的节点

// 如果dir 大于等于 0: 当前链表节点一定放置在当前树节点的右侧,但不一定是该树节点的右子节点,

// 也可能是右子节点或者左子节点或者更深层次的节点

// 如果当前树节点不是叶子,那么最终以当前树节点的左子节点或者右子节点为起始几点,然后再重新开始寻找自己当前链表节点的位置。

// 如果当前树节点就是叶子节点,那么更具 dir 的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。

// 挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了

if ((p = (dir <= 0) ? p.left : p.right) == null) {

x.parent = xp; // 当前链表节点作为当前树节点的子节点

if (dir <= 0)

xp.left = x; // 左子节点

else

xp.right = x; // 右子节点

root = balanceInsertion(root, x); // 重新平衡

break;

}

}

}

}

// 把所有的链表节点都遍历之后,最终构造出来的树可能是经历多个平衡操作,根节点目前到底是链表的那个节点是不确定的

// 因为我们需要基于树来做查找,所以就应该把 tab[N] 得到的对象一定是根节点对象,而且是链表的第一个节点对象,所以要做对应的调整。

// 把红黑树的节点设置为所在数组槽的第一个元素

// 说明: TreeNode 既是一个红黑树也是一个双向链表

// 这个方法做的事情是保证树根节点一定要成为链表的首节点

moveRootToFront(tab, root);

}

balanceInsertion 树平衡

====================

这个方法分析之前,我们可以先看看红黑树的规则:红黑树是每个结点都带有颜色属性的二叉查找树,颜色或红色或黑色。 在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

  • 性质1. 节点是红色或黑色。

  • 性质2. 根节点是黑色。

  • 性质3. 所有叶子都是黑色。(叶子是NIL结点)

  • 性质4. 每个红色节点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

  • 性质5. 从任一节节点其每个叶子的所有路径都包含相同数目的黑色节点。

// root 为根节点

// x 为需要插入的节点

// 最后返回的是一个平很后的根节点

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,

TreeNode<K,V> x) {

// 查询节点标记为红色

x.red = true;

// 设置一个只可以内部退出的循环

// 变量说明:

// xp 当前节点, xpp 父节点的父节点, xppl 父节点的父节点的左节点, xppr 父节点的父节点的右节点

for (TreeNode<K,V> xp, xpp, xppl, xppr;😉 {

// 如果父节点为空, 说明当前节点就是根节点,那么直接把当前接待你标记为黑色返回当前节点。

if ((xp = x.parent) == null) {

x.red = false;

return x;

}

// 如果当前节点为黑设色并且当前父节点为 null, 或者

// 父节点为红色,但是 xpp 节点为空

else if (!xp.red || (xpp = xp.parent) == null)

return root;

// 当前节点等于 xppl

if (xp == (xppl = xpp.left)) {

//xppr != null 并且 是红色

if ((xppr = xpp.right) != null && xppr.red) {

xppr.red = false;

xp.red = false;

xpp.red = true;

x = xpp; // 当前节点等于 xpp, 进入下一次循环

}

else {

if (x == xp.right) { // 当前节点是父节点的右子节点

root = rotateLeft(root, x = xp); //父节点左旋

xpp = (xp = x.parent) == null ? null : xp.parent; // 获取 xpp

}

if (xp != null) { // 父节点不为空

xp.red = false; // 父节点设置为黑色

if (xpp != null) { // xpp 不为空

xpp.red = true; // xpp 为红色

root = rotateRight(root, xpp); // xpp 右旋转

}

}

}

}

else { // 如果 xp 是 xpp 的右节点

if (xppl != null && xppl.red) { // xppl 不为空,并且为红色

xppl.red = false; // xppl 设置为黑色

xp.red = false; // 父节点为黑色

xpp.red = true; // xpp 为红色

x = xpp; // x = xpp 进入下次循环

}

else {

if (x == xp.left) { // 当前节点为父节点的左子节点

root = rotateRight(root, x = xp); // 根节点你右旋转

xpp = (xp = x.parent) == null ? null : xp.parent; // xpp = xp.parent

}

if (xp != null) { // xp != null

xp.red = false; // xp 为黑色

if (xpp != null) { // xpp != null

xpp.red = true; // xpp 为红色

root = rotateLeft(root, xpp); // 左旋

}

}

}

}

}

}

// 节点左旋转

// root 当前根节点

// p 指定选装的节点

// 返回旋转后的根接待你(平衡涉及左旋右旋根根节点改变,所以需要返回最新的根节点)

// 示意图

// pp pp

// | |

// p —> r

// / \ / \

// l r p rr

// / \ / \

// rl rr l rl

// 旋转做了几件事情?

// 1. 将 rl 设置为 p 的子接待你,将 rl 设置为父节点 p

// 2. 将 r 的父节点设置 pp, 将 pp 的左子节点设或者右子接待你设置为 r

// 3. 将 r 的左子节点设置为 p, 将 p 的父节点设置为 r

static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,

TreeNode<K,V> p) {

TreeNode<K,V> r, pp, rl;

// 左旋的节点以及需要左旋的节点的右节点不为空

if (p != null && (r = p.right) != null) {

// 要左旋转的右子节点 = rl ,

if ((rl = p.right = r.left) != null)

// 设置 rl 父亲节点设置为 p

rl.parent = p;

// 将 r 的父节点设置为 p 的父节点,如果 pp == null

if ((pp = r.parent = p.parent) == null)

// 染黑

(root = r).red = false;

else if (pp.left == p) // 判断父节点是在 pp 的左边还是右边

pp.left = r; // 如果是左子节点,把 pp.letf = r

else

pp.right = r; // 如果是右子节点, pp.reight = r

r.left = p; // 最后将 r的左子节点设置为 p

p.parent = r; // 最后将 p.parent 设置为 r

}

return root;

}

// 节点右旋转

// 右旋同理

static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,

TreeNode<K,V> p) {

TreeNode<K,V> l, pp, lr;

if (p != null && (l = p.left) != null) {

if ((lr = p.left = l.right) != null)

lr.parent = p;

if ((pp = l.parent = p.parent) == null)

(root = l).red = false;

else if (pp.right == p)

pp.right = l;

else

pp.left = l;

l.right = p;

p.parent = l;

}

return root;

}

moveRootToFront 方法

==================

把所有的链表节点都遍历之后,最终构造出来的树可能是经历多个平衡操作,根节点目前到底是链表的那个节点是不确定的。 因为我们需要基于树来做查找,所以就应该把 tab[N] 得到的对象一定是根节点对象,而且是链表的第一个节点对象,所以要做对应的调整。 把红黑树的节点设置为所在数组槽的第一个元素,这个方法做的事情是保证树根节点一定要成为链表的首节点。

static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {

int n;

// root 节点不为空, 并且表不为空, 并且数组长度大于 0

if (root != null && tab != null && (n = tab.length) > 0) {

// 当前 Node 所在槽位

int index = (n - 1) & root.hash;

// 获取当前槽所在接待你

TreeNode<K,V> first = (TreeNode<K,V>)tab[index];

// 如果当前槽位节点不是首节点

if (root != first) {

// 后驱节点

Node<K,V> rn;

// 修改为首节点

tab[index] = root;

// rp 前驱节点为 root 的前驱节点

TreeNode<K,V> rp = root.prev;

// 后驱节点不为空

if ((rn = root.next) != null)

((TreeNode<K,V>)rn).prev = rp;

if (rp != null)

rp.next = rn;

if (first != null)

// 原来的头节点前驱节点指向新的头节点 root 节点

first.prev = root;

// root 节点的后驱节点指向之前的头节点

root.next = first;

// root 由于是头节点所以前驱节点为 null

root.prev = null;

}

assert checkInvariants(root);

}

}

remove 方法

=========

remove 方法的本质是将 key 值所在的节点的值设置为 nu

public V remove(Object key) {

Node<K,V> e;

return (e = removeNode(hash(key), key, null, false, true)) == null ?

null : e.value;

}

removeNode 方法

=============

final Node<K,V> removeNode(int hash, Object key, Object value,

boolean matchValue, boolean movable) {

Node<K,V>[] tab; Node<K,V> p; int n, index;

// tab 不为空, 数组长度大于 0, 当前节点数据不为 null

// 不得不说 hashmap 源码的逻辑还是非常严谨的

if ((tab = table) != null && (n = tab.length) > 0 &&

(p = tab[index = (n - 1) & hash]) != null) {

// node 用来存储当前节点信息

Node<K,V> node = null, e; K k; V v;

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

node = p;

else if ((e = p.next) != null) {

// 如果是树形结构

if (p instanceof TreeNode)

// 获取节点

node = ((TreeNode<K,V>)p).getTreeNode(hash, key);

else {

// 链表查找

do {

if (e.hash == hash &&

((k = e.key) == key ||

(key != null && key.equals(k)))) {

node = e;

break;

}

p = e;

} while ((e = e.next) != null);

}

}

//

if (node != null && (!matchValue || (v = node.value) == value ||

(value != null && value.equals(v)))) {

// 如果是红黑树,删除节点

if (node instanceof TreeNode)

((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);

else if (node == p) // 如果是头节点

// 那么头节点指针指向移除节点的后驱节点

tab[index] = node.next;

else

// 前驱节点的后驱指针,指向当前节点的后驱指针

p.next = node.next;

// 修改次数累加

++modCount;

// 数据长度减少

–size;

afterNodeRemoval(node);

return node;

}

}

return null;

}

字节跳动面试官:说说HashMap 的设计与优化?

removeTreeNode 方法

=================

removeTreeNode 是删除节点的核心方法,删除的时候如果是一个普通节点就可以直接情况,如果是链表的话需要将当前节点删除。如果是红黑树的话,需要删除 TreeNode , 然后进行一次树平衡,或者将树转换为链表。

final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,

boolean movable) {

int n;

if (tab == null || (n = tab.length) == 0)

return;

// 获取索引值

int index = (n - 1) & hash;

// 获取头节点,即树的根节点

TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;

// 当前节点的后驱节点,当前节点的前驱节点保存

TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;

// 前驱节点为 null

if (pred == null)

// 当前是头节点,删除之后,头节点直接指向了删除节点的后继节点

tab[index] = first = succ;

else

pred.next = succ;

if (succ != null)

succ.prev = pred;

// 如果头节点(即根节点)为空,说明当前节点删除后,红黑树为空,直接返回

if (first == null)

return;

// 如果头接单不为空,直接调用 root() 方法获取根节点

if (root.parent != null)

root = root.root();

if (root == null

|| (movable

&& (root.right == null

|| (rl = root.left) == null

|| rl.left == null))) {

// 链表化,英文前面的链表节点完成删除操作,故这里直接返回,即可完成节点删除

tab[index] = first.untreeify(map); // too small

return;

}

// p 当前节点; pl 当前节点左节点,pr 当前节点右节点

// replacement p 节点删除后替代的节点

TreeNode<K,V> p = this, pl = left, pr = right, replacement;

if (pl != null && pr != null) {

// p 节点删除后, 他的左右节点不为空时, 遍历他的右节点上的左子树

// (以下操作先让 p 节点和 s 节点交换位置,然后再找到 replacement 节点替换他 )

TreeNode<K,V> s = pr, sl;

while ((sl = s.left) != null) // find successor

s = sl;

// 通过上述操作 s 节点是大于 p 节点的最小节点(替换它的节点)

// 将 s 节点和 p 节点的颜色交换

boolean c = s.red; s.red = p.red; p.red = c; // swap colors

// sr s 节点的右节点

TreeNode<K,V> sr = s.right;

// pp p 节点的父节点

TreeNode<K,V> pp = p.parent;

// 如果 pr 就是 s 节点

if (s == pr) { // p was s’s direct parent

// 节点交换

p.parent = s;

s.right = p;

}

else {

// 获取 s 的父节点

TreeNode<K,V> sp = s.parent;

// 将 p 节点的父节点指向 sp, 且 sp 节点存在

if ((p.parent = sp) != null) {

// 判断 s 节点的 sp 节点在左侧还是右侧, 将 p 节点存放在 s 节点一侧

if (s == sp.left)

sp.left = p;

else

sp.right = p;

}

// 将 pr 节点编程 s 节点的右节点,并且 pr 节点存在

if ((s.right = pr) != null)

// 将 s 节点编程 pr 节点的父节点

pr.parent = s;

}

// 因为 s 节点的性质, s 节点没有左节点

// 当 p 节点和 s 节点交换了位置,所以将 p 节点的左几点指向空

p.left = null;

// 将 sr 节点编程 p 节点的左节点,并且 sr 节点存在

if ((p.right = sr) != null)

// 将 p 节点编程 sr 的父节点

sr.parent = p;

// 将 pl 节点编程 s 节点的左节点,并且存在 pl 节点

if ((s.left = pl) != null)

// 将 pl 父节点赋值为s

pl.parent = s;

// s 父节点设置为 pp 并且 pp 节点存在

if ((s.parent = pp) == null)

// root 节点为 s

root = s;

// p 节点等于 pp.left

else if (p == pp.left)

// pp 的左节点为 s

pp.left = s;

else

// p 节点等于 pp.right

// pp 右节点为 s

pp.right = s;

// sr 不为空

if (sr != null)

// 替换节点为 sr

replacement = sr;

else

// 否则替换节点为 p

replacement = p;

}

else if (pl != null)

// 如果 pl 节点存在, pr 节点不存在,不用交换位置, pl 节点为替换为 replacement 节点

replacement = pl;

else if (pr != null)

// 如果 pr 节点存在, pl 节点不存在, 不用交换位置, pr 节点为替换为 replacement 节点

replacement = pr;

else

// 如果都不存在 p 节点成为 replacement 节点

replacement = p;

// 以下判断根据上述逻辑查看,仅以p 节点的当前位置为性质, 对 replacement 节点进行操作

if (replacement != p) {

// 如果 replacement 不是 p 节点

// 将 p 节点的父节点 pp 变成 replacement 节点的父节点

TreeNode<K,V> pp = replacement.parent = p.parent;

// 如果 pp 节点不存在

if (pp == null)

// replacement 变成根节点

root = replacement;

else if (p == pp.left)

// 如果 pp 节点存在,根据 p 节点在 pp 节点的位置,设置 replacement 节点的位置

pp.left = replacement;

else

pp.right = replacement;

// 将 p 节点所有的引用关系设置为 null

p.left = p.right = p.parent = null;

}

// 如果 p 节点是红色,删除后不影响 root 节点,如果是黑色,找到平衡后的根节点,并且用 r 表示

TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

// 如果 p 是 replacement 节点

if (replacement == p) { // detach

// 得到 pp

TreeNode<K,V> pp = p.parent;

p.parent = null;

if (pp != null) {

// pp 存在

// 根据 p 节点的位置,将 pp 节点的对应为位置设置为空

if (p == pp.left)

pp.left = null;

else if (p == pp.right)

pp.right = null;

}

}

// 移动新的节点到数组上

if (movable)

moveRootToFront(tab, r);

}

balanceDeletion 方法

==================

删除节点后的树平衡方法 。

static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,

TreeNode<K,V> x) {

// x 当前需要删除的节点

// xp x 父节点

// xpl x 父节点的左子节点

// xpr x 父节点的右子节点

for (TreeNode<K,V> xp, xpl, xpr;😉 {

if (x == null || x == root)

// x 为空或者 x 为根节点

最后

面试前一定少不了刷题,为了方便大家复习,我分享一波个人整理的面试大全宝典

  • Java核心知识整理

2020年五面蚂蚁、三面拼多多、字节跳动最终拿offer入职拼多多

Java核心知识

  • Spring全家桶(实战系列)

2020年五面蚂蚁、三面拼多多、字节跳动最终拿offer入职拼多多

  • 其他电子书资料

2020年五面蚂蚁、三面拼多多、字节跳动最终拿offer入职拼多多

Step3:刷题

既然是要面试,那么就少不了刷题,实际上春节回家后,哪儿也去不了,我自己是刷了不少面试题的,所以在面试过程中才能够做到心中有数,基本上会清楚面试过程中会问到哪些知识点,高频题又有哪些,所以刷题是面试前期准备过程中非常重要的一点。

以下是我私藏的面试题库:

2020年五面蚂蚁、三面拼多多、字节跳动最终拿offer入职拼多多

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
eft)

pp.left = null;

else if (p == pp.right)

pp.right = null;

}

}

// 移动新的节点到数组上

if (movable)

moveRootToFront(tab, r);

}

balanceDeletion 方法

==================

删除节点后的树平衡方法 。

static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,

TreeNode<K,V> x) {

// x 当前需要删除的节点

// xp x 父节点

// xpl x 父节点的左子节点

// xpr x 父节点的右子节点

for (TreeNode<K,V> xp, xpl, xpr;😉 {

if (x == null || x == root)

// x 为空或者 x 为根节点

最后

面试前一定少不了刷题,为了方便大家复习,我分享一波个人整理的面试大全宝典

  • Java核心知识整理

[外链图片转存中…(img-iImkDqa3-1713121860215)]

Java核心知识

  • Spring全家桶(实战系列)

[外链图片转存中…(img-xmaub7J6-1713121860215)]

  • 其他电子书资料

[外链图片转存中…(img-YMrtjyke-1713121860216)]

Step3:刷题

既然是要面试,那么就少不了刷题,实际上春节回家后,哪儿也去不了,我自己是刷了不少面试题的,所以在面试过程中才能够做到心中有数,基本上会清楚面试过程中会问到哪些知识点,高频题又有哪些,所以刷题是面试前期准备过程中非常重要的一点。

以下是我私藏的面试题库:

[外链图片转存中…(img-XoK21XoV-1713121860216)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-mUVH0LDF-1713121860216)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值