当前位置:首页 » 社会万象 » 正文

分类页和文章页“当前位置”下方广告(PC版)
分类页和文章页“当前位置”下方广告(移动版)

拉脱维亚,3分钟让你理解:HashMap之红黑树树化进程,天天动听

272 人参与  2019年11月20日 15:53  分类:社会万象  评论:0  
  移步手机端

1、打开你手机的二维码扫描APP
2、扫描左则的二维码
3、点击扫描获得的网址
4、可以在手机端阅读此文章

<引荐阅览>

我的美团offer凉凉了?开发工程师(Java岗)三面完毕等告诉...

01 概述

HashMap是Java程序员运用频率最高的用于映射(键值对)处理的数据类型。跟着JDK(Java Developmet Kit)版别的更新,JDK1.8对HashMap底层的完结进行了优化,例如引入红黑树的数据结构和扩容的优化等。本文首要剖析一下HashMap中红黑树树化的进程。

0拉脱维亚,3分钟让你了解:HashMap之红黑树树化进程,天天悦耳2 红黑树(red black tree)

一个节点标记为赤色或许黑色。 根是黑色的。 假如一个节点是赤色的,那么它的子节点有必要是黑色的(这便是为什么叫红黑树)。 一个节点到到一个null引证的每一条途径有必要包括相同数目的黑色节点(所以赤色节点不影响)。 其实RB Tree和闻名的AVL Tree有许多相同的当地,困难的当地都在于将一个新项刺进到树中。了解AVL Tree的朋友应该都知道为了保持树的高度有必要在刺进一个新的项后有必要在树的结构上进行改动,这儿首要是通过旋转,当然在RB Tree中原理也是如此。

03 两种旋转和一种典型的改换

旋转的方向:

改换进程:

相相互关:

单向相关:

代表赤色的节点:

代表黑色的节点:

代表一个不会损坏红黑树结构的部分,或许是节点,或许是一个子树,总归不会破环当时树的结构。这个部分会由于旋转而衔接到其他的节点后边,咱们能够了解成由于重力原因它掉到了下面的节点上:

  • 单旋转改换。
  • 双旋转改换(需求两次反方向的单旋转)。
  • 当遇到两个子几点都为赤色的话履行色彩改换,由于刺进 是赤色的会发作抵触。假如根节点两头的子节点都是赤色,两个叶子节点变成黑色,根节点变成赤色,然后再将根节点变成黑色。

上面的图中描绘了红黑树中三种典型的改换,其实前两种改换这正是AVL Tree中的两种典型的改换。

04 几个问题

4.1 为什么要进行旋转?

由于P和X节点都为赤色节点这破环了红节点下面的节点有必要为黑色节点的规矩。

4.2 新参加的节点总是赤色的,这是为什么呢?

由于被刺进前的树结构是构建好的,一但咱们进行增加黑色的节点,不论增加在哪里都会损坏原有途径上的黑色节点的数量相等联系,所以刺进赤色节点是正确的挑选。

4.3 为什么要进行色彩改换?

正如第一种旋转新参加的节点X损坏了红黑树的结构不得不进行旋转,后边的便是旋转后的成果,旋转后构成新的结构,此刻咱们发现两个子节点都是赤色的所以履行第三个改换特性,色彩改换,由于假如子节点是赤色的那么咱们在增加的时分只能增加黑色的节点,但是增加任何黑色叶子节点都会损坏树的第四条性质,所以要对其进行改换。当进行改换后叶子节点是赤色的并且咱们默许增加的叶子节点是赤色的,所以增加到黑色节点后并不会损坏树的第四条结构,所以这种改换很有用。

第二种双改换中在树的内500px部怎样呈现的赤色的节点? 正是由于上面的色彩改换导致新色彩改换后的节点与他的父节点发作了色彩抵触。

与AVL树比较? 比AVL树比较长处是不用在节点类中保存一个节点高度这个变量,节省了内存。

并且红黑树一般不是以递归办法完结的而是以循环的办法完结。

一般的操作在最坏景象下花费O(logN)时刻。

05 好了有了这些胸前长痘痘是什么原因根本的概念让咱们去看一下HashMap中的代码的完结

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
{
Node[] tab;
Node p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else
{
Node e;
K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
// 假如当时的bucket里边现已是红黑树的话,履行红黑树的增加操作
e = ((TreeNode) p).putTreeVal(this, tab, hash, key, value);
else
{
for (int binCount = 0;; ++binCount)
{
if ((e = p.next) == null)
{
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD = 8,判别假如当时bucket的方位链表长度大于8的话就将此链表变成红黑树。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null)
{ // existing mapping for拉脱维亚,3分钟让你了解:HashMap之红黑树树化进程,天天悦耳 key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

上面的办法通过hash核算刺进的项的槽位,假如有是相同的key则依据设置的参数是否履行掩盖,假如相应槽位空的话直接刺进,假如对应的槽位有项则判别是红黑树结构仍是链表结构的槽位,链表的话则顺着链表寻觅假如找到相同的key则依据参数挑选掩盖,没有找到则链接在链表终究边,链表项的数目大于8则对其进行树化,假如是红黑树结构则依照树的增加办法增加项。

让咱们看一下treeifyBin这个办法。

final void treeifyBin(Node[] tab, int hash)
{
int n, index;
Node e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// resize()办法这儿不过多介绍,感兴趣的能够去看上面的链接。
resize();
// 通过hash求出bucket的方位。
else if ((e = tab[index = (n - 1) & hash]) != null)
{
TreeNode hd = null, tl = null;
do
{
//固特异 将每个节点包装成TreeNode。
TreeNode p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else
{
// 将一切TreeNode衔接在一起此刻仅仅链表结构。
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
// 对TreeNode链表进行树化。
hd.treeify(tab);
}
}

找个办法所做的工作便是将方才九个项以链表的办法衔接在一起,然后通过它构建红黑树。

看代码之前咱们先了解一下TreeNode

static final class TreeNode extends LinkedHashMap.Entry
{
TreeNode parent; // red-black tree links
TreeNode left;
TreeNode right;
TreeNode prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node next)
{
super(hash, key, val, next);
}

final void treeify(Node[] tab)
{
// ......
}

static TreeNode balanceInsertion(TreeNode root, T拉脱维亚,3分钟让你了解:HashMap之红黑树树化进程,天天悦耳reeNode x)
{
// ......
}

static TreeNode rotateLeft(TreeNode root, TreeNode p)
{
// ......
}

static TreeNode rotateRight(TreeNode root, TreeNode p)
{
// ......
}

// ......其他办法省掉
}

能够看出出真实的保护红黑树结构的办法并没有在HashMap中,悉数都在TreeNode类内部。

咱们看一下treeify代码

final void treeify(Node[] tab)
{
TreeNode root = null;
// 以for循环的办法遍历方才咱们创立的链表。
for (TreeNode x = this, next; x != null; x = next)
{
// next向前推动。
next = (TreeNode) x.next;
x.left = x.right = null;
// 为树根节点赋值。
if (root == null)
{
x.parent = null;
x.red = false;
root = x;
} else
{
// x即为当时拜访链表中的项。
K k = x.key;
int h = x.hash;
Class
// 此刻红黑树现已有了根节点,上面获取了当时参加红黑树的项的key和hash值进入中心循环。
// 这儿从root开端,是以一个自顶向下的办法遍历增加。
// for循环没有操控条件,由代码内break跳出循环。
for (TreeNode p = root;;)
{
// dir:directory,比较增加项与当时树中拜访节点的hash值判别参加项的途径,-1为左子树,+1为右子树。
// ph:parent hash。
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null && (kc = comparableClassFor(k)) == null)
|| (dir = com拉脱维亚,3分钟让你了解:HashMap之红黑树树化进程,天天悦耳pareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
// x无极限p:x parent。
TreeNode xp = p;
// 找到契合x增加条件的节点。
if ((p = (dir <= 0) ? p.left : p.right) == null)
{
x.parent = xp;
// 假如xp的hash值大于x的hash值,将x增加在xp的左面。
if (dir <= 0)
xp.left = x;
// 反之增加在xp的右边。
else
xp.right = x;
// 保护增加后红黑树的红黑结构。
root = balanceInsertion(root, x);

// 跳出循环当时链表中的项成功的增加到了拉脱维亚,3分钟让你了解:HashMap之红黑树树化进程,天天悦耳红黑树中。
break;
}
}
}
}
// Ensures that the given root is the first node of its bin,自己翻译一下。
moveRootToFront(tab, root);
}

第一次循环会将链表中的首节点作为红黑树的根,然后的循环会将链表中的的项通过比较hash值然后衔接到相应树节点的左面或许右边,刺进或许会损坏树的结构所以接着履行balanceInsertion。

咱们看balanceInsertion

static  TreeNode balanceInsertion(TreeNode root, TreeNode x)
{
// 正如最初所说,新参加树节点默许都是赤色的,不会损坏树的结构。
x.red = true;
// 这些变量名不是作者随意界说的都是有意义的。
// xp:x parent,代表x的父节点。
// xpp:x parent parent,代表x的祖父节点
// xppl:x parent parent left,代表x的祖父的左节点。
// xppr:x parent parent right,代表x的祖父的右节点。
for (TreeNode xp, xpp, xppl, xppr;;)
{
// 假如x的父节点为null阐明只要一个节点,该节点为根节点,根节点为黑色,red = false。
i朱彦辉f ((xp = x.parent) == null)
{
x.red = false;
return x;
}
// 进入else阐明不是根节点。
// 假如父节点是黑色,那么大吉大利(今晚吃鸡),赤色的x节点能够直接增加到黑色节点后边,回来根就行了不需求任何剩余的操作。
// 假如父节点是赤色的,但祖父节点为空的话也能够直接回来根此刻父节点便是根节点,由于根有必要是黑色的,增加在后边没有任何问题。
else if (!xp.red || (xpp = xp.parent) == null)
return root;

// 一旦咱们进入到这儿就阐明晰两件是情
// 1.x的父节点xp是赤色的,这样就遇到两个赤色节点相连的问题,所以有必要通过旋转改换。
// 2.x的祖父节点xpp不为空。

// 判别假如父节点是否是祖父节点的左节点
if (xp == (xppl = xpp.left))
{
// 父节点xp是祖父的左节点xppr
// 判别祖父节点的右节点不为空并且是否是赤色的
// 此刻xpp的左右节点都是红的,所以直接进行上面所说的第三种改换,将两个子节点变成黑色,将xpp变成赤色,然后将赤色节点x顺畅的增加到了xp的后边。
// 这儿咱们有疑问为什么将x = xpp?
// 这是由于将xpp变成赤色今后或许与xpp的父节点发作两个相连赤色节点的抵触,这就又构成了第二种旋转改换,所以有必要从底向上的进行改换,直到根。
// 所以令x = xpp,然后进行下下一层循环,接着往上走。
if ((xppr = xpp.right) != null && xppr.red)
{
xppr.red = false;
xp.red = false;
xpp.red毛笔书法 = true;
x = xpp;
}
// 进入到这个else里边阐明。
// 父节点xp是祖父的左节点xppr。
// 祖父节点xpp的右节点xppr是黑色节点或许为空,默许规矩空节点也是黑色的。
// 下面要判别x是xp的左节点仍是右节点。
else
{
// x是xp的右节点,此刻的结构是:xpp左->xp右->x。这显着是第二中改换需求进行两次旋转,这儿先进行一次旋转。
// 下面是第一次旋转。
if (x == xp.right)
{
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 针对自身便是xpp左->xp左->x的结构或许由于上面的旋转形成的这种结构进行一次旋转。
if (xp != null)
{
xp.red = false;
if (xpp != null)
{
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
// 这儿的剖析办法和前面的相对称只不过悉数在右测不再重复剖析。
else
{
if (xppl != null && xppl.red)
{
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
} else
{
if (x == xp.left)
{
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null)
{
xp.red = false;
if (xpp != null)
{
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}

假如您的联想才能很强的话估量到这儿应该现已了解这会集改换的首要的联系。

下面简述一下前面的两种种走运的状况

x自身为根节点回来x。 x的父节点为黑色或许x的父节点是根节点直接回来不需求改换。 如若上述两个条件不满意的话,就要进行改换了,答应我再贴一点代码......没有代码剖析起来很困难。

06 色彩改换

 if (xp == (xppl = xpp.left))
{
if ((xppr = xpp.right) != null && xppr.red)
{
xppr.red = f拉脱维亚,3分钟让你了解:HashMap之红黑树树化进程,天天悦耳alse;
xp.red = false;
xpp.red = true;
x = xpp;
}
}

这儿是一个典型的一个黑色节点的两个子节点都是赤色的所以要进行色彩改换,由于刺进的都是赤色节点,当检测到祖父节点的左右子节点都是赤色的时分两个赤色就发作了抵触,所以先将节点进行这种色彩改换,将祖父节点变成吉本多香美赤色,然后将祖父的两个子节点变成黑色,这样咱们刺进的赤色节点就不会违反红黑拉脱维亚,3分钟让你了解:HashMap之红黑树树化进程,天天悦耳树的规矩了。

这儿有人会有疑问,假如祖父节点是根节点呢,那样的话祖父节点也会变成黑色,由于每次循环进行刺进平衡的操作当进行这种色彩改换之后都会将刺进节点的引证指向祖父节点,当进行下一轮循环的时分会优先检测当时节点是否是根节点,假如是根节点那就将色彩变成黑色,下面看图:

当将节点指向祖父节点进行下一轮循环时:

07 两个中心旋转(左旋转和右旋转)

 // 一旦咱们进入到这儿就阐明晰两件是情
// 1.x的父节点xp是赤色的,这样就遇到两个赤色节点相连的问题,所以有必要通过旋转改换。
// 2.x的祖父节点xpp不为空。

// 判别假如父节点是否是祖父节点的左节点
if (xp == (xppl = xpp.left))
{
if ((xppr = xpp.right) != null && xppr.red)
{// ......
}
// 进入到这个else里边阐明。
// 父节点xp是祖父的左节点xppr。
// 祖父节点xpp的右节点xppr是黑色节点或许为空,默许规矩空节点也是黑色的。
// 下面要判别x是xp的左节点仍是右节点。
else
{
// x是xp的右节点,此刻的结构是:xpp左->xp右->x。这显着是第二中改换需求进行两次旋转,这儿先进行一次旋转。
// 下面是第一次旋转。
if (x == xp.right)
{
root = rotateLeft(root, x = xp);
xpp = (xp = x.paren松下洗衣机t) == null ? null : xp.parent;
}
// 针对自身便是xpp左->xp左->x的结构或许由于上面的旋转形成的这种结构进行一次旋转。
if (xp != null)
{
xp.red = false;
if (xpp != null)
{
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}红斑狼疮图片
}

色彩改换完结后进入下面的else块

咱们已知xp是xp绿壳蛋鸡p的左节点,首要判别了x是xp的左节点仍是右节点,假如是右节点的话构成了下面的结构。

这中结构需求进行双旋转,首要先进行一次向左旋转。

7.1 左旋转

 1 static  TreeNode rotateLeft(TreeNode root, TreeNode p)
2 {
3 // r:right,右节点。
4 // pp:parent parent,父节点的父节点。
5 // rl:right left,右节点的左节点。
6 TreeNode r, pp, rl;
7 if (p != null && (r = p.right) != null)
8 {
9 if ((rl = p.right = r.left) != null)
10 rl.parent = p;
11 if ((pp = r.parent = p.parent) == null)
12 (root = r).red = false;
13 else if (pp.left == p)
14 pp.left = r;
15 else
16 pp.right = r;
17 r.left = p;
18 p.parent = r;
19 }
20 return root;
21 }

1.刚进入办法时,状况如下图。(RL或许是空的)

2.进入了if块后履行到第10行后。

9 if ((rl = p.right = r.left) != null)
10 rl.parent = p;

此刻假如9行的条件契合的话履行10行RL指向P,假如RL为null的话,P的右节点指向null。

3.接着看11和12行代码。

11 if ((pp = r.parent = p.parent) == null)
12 (root = r).red = false;

首要咱们看11行if里边的赋值句子所形成的影响。

在if里边的表达式不论符不契合条件()内的内容都会履行。

假如契合条件的话会履行12行的代码,变成了下面的成果。

由于PP为空所以只剩下这三个。

4.假如11行的条件为假的话,履行完11行()内的表达式后履行13行

13 else if (pp.left == p)
14 pp.left = r;

满意条件的话R和PP相相互关。

5.由于进入了13和14行所以不进入15和16行的else句子。

15 else
16 pp.right = r;

6.看17和18行。

17 r.left = p;
18 p.parent = r;

终究履行完了一个旋转变成了咱们开端说的第一种旋转的办法,这个结构还需求向右旋转一次。

 if (x == xp.right)
{
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
xpp = (xp = x.parent) == null ? null : xp.parent;

履行完上面的代码,旋转后调整x,xp,和xpp的联系得到下图。

7.2 右旋转

 if (xp != null)
{
xp.red = false;
if (xpp != null)
{
xpp.red = true;
root = rotateLeft(root, xpp);
}
}

1.首要让XP变成黑色。

2.假如XPP不为空的话变成赤色。

由于咱们在rotat男孩鸡鸡eLeft(root, xpp),传进来的是XXP所以下面的的旋转中实际上便是对XP和XXP履行了一次与上面的方向相反其他完全相同的旋转。

接着咱们看向右旋转的代码

static  TreeNode rotateRight(TreeNode root, Tr邱心仪eeNode p)
{
// l:left,左节点。
// pp:parent parent,父节点的父节点。
// lr:left right,左节点的右节点。
TreeNode 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;
}
re染发色彩大全turn root;
}

3.刚进来的时分结构是这个姿态。

在这儿的P便是方才传进来的XPP。

4.这儿咱们以为LR是存在的,其实这个不影响首要的旋转,为空就指向null呗,直接履行完9和10行。

 9 if ((lr = p.left = l.right) != null)
10 lr.parent = p;

5.在这儿咱们倘若PP是存在的,直接履行完11的表达式不再履行12行。(不再剖析不存在的状况)。

11 if ((pp = l.parent = p.parent) == null)
12 (root = l).red = false;

6.由于11行的条件不契合,现在直接履行13行的表达式,不契合履行15行else,履行16行。

15 else
16 pp.left = l;

7.终究履行层17和18行。

17 l.right = p;
18 p.parent = l;

终究完结两次的旋转。

08 疑问?

咱们或许觉得和方才接不上其实是这乱云飞渡样的,方才在右旋转前的时分的图像是这个样的。

由于咱们传进来的是XPP,所以结合上一次的向左旋转咱们在向右旋转的时分看到全图应该是这个姿态的。(注:XPPP或许是XPP的左父节点也或许是右父节点这儿不影响,并且能够是恣意色彩)

现在知道为什么XPPP能够是恣意色彩的了吧,由于旋转往后X是黑色的即使XPPP是赤色,此刻咱们又能够对两个赤色的子节点进行色彩改换了,改换后X和XPPP有发作了色彩抵触,接着进行旋转直到根。

static  TreeNode balanceInsertion(TreeNode root, TreeNode x)
{
x.red = true;
for (TreeNode xp, xpp, x海宁皮衣ppl, xppr;;)
{
if ((xp = x.parent) == null)
{
x.red = false;
return x;
}
else if (!xp.red || (xpp = xp.parent) == null)
return root;
if (xp == (xppl = xpp.left))
{
// 刺进方位父节点在祖苏燃陆廷风父节点的左面。
}
else
{
// 刺进方位父节点在祖父节点的右边。
}
}
}

咱们值剖析了刺进方位父节点在祖父节点的左面的状况,并没有剖析别的一面的对称状况,其实是相同的由于调用的都是相同的办法。

以上就染发是在1.8中的HashMap新引入的红黑树树化的进程,与本来的链表比较当鸿茅药酒同一个bucket上存储许多entry的话树形的查找结构显着要比链表线性的的功率要高。

原文链接:微邮付https://juejin.im/post/5d9fbd72e51d45781a677c87

转载请保留出处和链接!

本文链接:http://www.zjsign.com/articles/1596.html

文章底部广告(PC版)
文章底部广告(移动版)
百度分享获取地址:http://share.baidu.com/
百度推荐获取地址:http://tuijian.baidu.com/,百度推荐可能会有一些未知的问题,使用中有任何问题请直接联系百度官方客服!
评论框上方广告(PC版)
评论框上方广告(移动版)
推荐阅读
12月13日

五五影院,民国时好莱坞放的什么电影,形成民众激愤,被要求焚毁影片,京香

发布 : | 分类 : 社会万象 | 评论 : 0人 | 浏览 : 168次

第一次世界大战后,美国好莱坞各大制片公司都看好中国的电影市场,纷纷登陆各大城市影院。不过,这些好莱坞影片可不怎么讨喜,因为他们的影片常常损坏背景国的国家形象。比如在这些好莱坞电影中,华人的形象就很差,不是盗匪、囚犯、烟鬼,就是仆役,而且蓬头垢面、面目可憎。...

标签 :
12月13日

长安悦翔,为何“大和号”在淹没前只击落三架美机,彗星来的那一夜

发布 : | 分类 : 社会万象 | 评论 : 0人 | 浏览 : 112次

“大和号”难敌空中打击,这是必然的结果。但是,大和号上装备了各种口径高炮共180管,在被击沉前只击落了3架美机,这个结果着实令战列舰的拥趸感到悲哀。很多人都关注到日式25毫米高炮的缺陷,这只是重要原因之一;除此之外,军舰并不是一个好的防空武器发射平台,也是重要的原因之一。...

标签 :
12月08日

张智尧,TUF宗族再添新成员,海韵金牌全模组电源GX650装机体会,小k

发布 : | 分类 : 社会万象 | 评论 : 0人 | 浏览 : 266次

电脑市场真是百家争鸣,百花齐放,AMD、Intel两家产品不断升级,各大硬件厂商也极力配合,纷纷丰富着自家的产品线,就拿电源产品来说,电源厂商们除了不断推出性能更强的产品,还纷纷将自家电源的售后质保提高到5年、7年、甚至10年。...

标签 :
12月08日

学习计划,家长带娃游水时要留神:头发丝钻进2岁男孩脚趾,差点因而截肢,阳朔天气

发布 : | 分类 : 社会万象 | 评论 : 0人 | 浏览 : 239次

分享亲子快乐,交流教育知识,欢迎关注顾晓安随着人们生活水平的提高和对锻炼身体的重视,锻炼身体成了孩子的一门必修课。锻炼项目也是多种多样的,足球、篮球、游泳、轮滑等等。但是无论你选择哪种体育项目,都要保证孩子的安全。...

标签 :