java 树 插入 root_[Java]AVL树 插入:单旋、双旋

目录

代码出处

各种实现

概念说明

算法说明

测试运行

代码修改

完整源码

代码出处

各种实现

Java实现

C++11

Data Structures and Algorithm Analysis in C++ (Fourth Edition)

CS-7 Text

https://users.cs.fiu.edu/~weiss/dsaa_c++4/code/

g++ -std=c++0x TestAvlTree.cpp

g++ -std=c++0x TestIntCell.cpp IntCell.cpp

C

概念说明

AVL树

https://en.wikipedia.org/wiki/AVL_tree

In an AVL tree, the heights of the two child subtrees of any node differ by at most one

在AVL树种,任意两棵子树的高度不能超过1;

路径 path 、长度 length、 高度height 与 深度depth

The root node has depth zero, leaf nodes have height zero.

根结点的深度是0,叶子结点的高度是0;

006dfcd012d3

路径 path

从结点n1到结点nk的路径是,一路上的结点序列 n1 n2 ... nk

006dfcd012d3

长度 length

路径的长度,是指这条路径上,经过的边数

006dfcd012d3

高度height 与 深度depth

结点nk的深度depth,是从根结点到nk结点的路径(即结点序列);

结点nk的高度height,是从nk结点到叶结点的最长路径;

叶结点的高度是0,根结点的深度是0;

要求某个结点的高度height,要往下走down,找叶子结点leaf;

要求某个结点的深度depth,要往上走up,找根结点root;

算法说明

流程

insert()以BST的插入规则新增结点;

insert()最后return balance( t );,调用balance(),完成AVL的平衡;

balance()4种case,进行相应的旋转;

一次插入,只需要进行一次平衡(一次单旋或者一次双旋),树就会重新回到平衡状态;

一次删除,可能会朝着祖先结点一直蔓延上去,最多需要调整平衡logn次;

单旋

Figure 4.31 Single rotation to fix case 1

height( t.left.left ) >= height( t.left.right )

006dfcd012d3

Figure 4.31 Single rotation to fix case 1

private AvlNode rotateWithLeftChild( AvlNode k2 )

{

AvlNode k1 = k2.left;

k2.left = k1.right;

k1.right = k2;

k2.height = Math.max( height( k2.left ), height( k2.right ) ) + 1;

k1.height = Math.max( height( k1.left ), k2.height ) + 1;

return k1;

}

Figure 4.33 Single rotation fixes case 4

height( t.right.right ) >= height( t.right.left )

006dfcd012d3

Figure 4.33 Single rotation fixes case 4

private AvlNode rotateWithRightChild( AvlNode k1 )

{

AvlNode k2 = k1.right;

k1.right = k2.left;

k2.left = k1;

k1.height = Math.max( height( k1.left ), height( k1.right ) ) + 1;

k2.height = Math.max( height( k2.right ), k1.height ) + 1;

return k2;

}

双旋

Figure 4.34 Single rotation fails to fix case 2

006dfcd012d3

Figure 4.34 Single rotation fails to fix case 2

Figure 4.35 Left–right double rotation to fix case 2

height( t.left.left) < height( t.left.right )

006dfcd012d3

Figure 4.35 Left–right double rotation to fix case 2

private AvlNode doubleWithLeftChild( AvlNode k3 )

{

k3.left = rotateWithRightChild( k3.left );

return rotateWithLeftChild( k3 );

}

Figure 4.36 Right–left double rotation to fix case 3

height( t.right.right ) < height( t.right.left )

006dfcd012d3

Figure 4.36 Right–left double rotation to fix case 3

private AvlNode doubleWithRightChild( AvlNode k1 )

{

k1.right = rotateWithLeftChild( k1.right );

return rotateWithRightChild( k1 );

}

感想

平衡操作是伴随着树的生长同步进行的,插一个结点就要做一次平衡,而不是先构建好一整棵树再去平衡,我觉得AVL树更像是一种从下往上叠着长出来的结构 ,而不是第一眼所见那种纵深向下的结构,因此,从整棵树的生命来看,的确就是在从下往上地做平衡;

单旋,本质就是一些链表重新连接;

双旋,站在一次单旋算作一个动作的抽象高度上,人脑处理双旋其实更倾向于感觉没什么旋转,无非是找到当新根的k2,左孩子一定是k1,右孩子一定是k3,剩余各部分依次填好即可,然而,对应的实现里,本质却是两次旋转;

双旋内部的第一次旋转,其实不会改变整棵树的平衡,更准确的说是,转了这一次,树还是失衡的,但是这一次旋转本质上却达到了顺利把失衡的case从2,3变成了1,4,左右左→左左左 右左右→右右右,这种思想叫做规约,遇到新问题不能一步解决,就先把新问题转换成老问题,之后老问题就可以用老方法干掉了;

双旋代码实现的精妙之处在于,不仅仅老办法是单旋,甚至连转换也是用的单旋,而如果只孤立地看转换用的第一次单旋,会觉得这有何用,无济于事等等,事实上,事情上这时候距离完成全部的平衡就是临门一脚了。

测试运行

至少需要Requires Java 7.

源码:AvlTree.java 以及UnderflowException.java(官网完整源码包)

编译运行输出:每插入一个结点node[i],就以先序序列输出当前的AVL树,这里展示最后一次输出;

> javac AvlTree.java

> java AvlTree

INSERT: 9

7

4

2

1

3

6

5

13

11

9

8

10

12

15

14

16

如何读?从上而下,从左而右,以缩进判断层次,类似文件目录;

对应的AVL树示意图:

006dfcd012d3

image.png

小BUG:其实仅仅从输出来看,的确是无法判断结点5是结点6的哪个孩子的,但是这仅仅是自己写的输出部分的BUG,不重要,暂时不进行优化;

代码修改:对AvlTree.java进行以下两处修改

修改main()函数测试代码,指定一组结点依次插入;

// Test program

public static void main( String [ ] args )

{

AvlTree t = new AvlTree<>( );

int[] nodes = {3,2,1,4,5,6,7,16,15,14,13,12,11,10,8,9};

for( int i = 0; i < nodes.length; i++ )

{

System.out.println( "INSERT: " + nodes[i] );

t.insert( nodes[i] );

t.preorder();

}

}

新增preorder()相关:前序序列输出AVL树,带空格表示层次结构

/**

* Print the tree contents in preorder.

*/

public void preorder()

{

if( isEmpty() )

System.out.println( "Empty tree" );

else

preorder( root , 0 );

System.out.println();

}

/**

* Internal method to print a subtree in preorder.

* @param t the node that roots the tree.

*/

private void preorder( AvlNode t, int blanks)

{

if( t != null )

{

System.out.println();

for(int i = 0 ;i < blanks; i++)

System.out.print(" ");

System.out.print( t.element );

preorder( t.left, blanks+1 );

preorder( t.right, blanks+1 );

}

}

完整源码 AvlTree.java

// AvlTree class

//

// CONSTRUCTION: with no initializer

//

// ******************PUBLIC OPERATIONS*********************

// void insert( x ) --> Insert x

// void remove( x ) --> Remove x (unimplemented)

// boolean contains( x ) --> Return true if x is present

// boolean remove( x ) --> Return true if x was present

// Comparable findMin( ) --> Return smallest item

// Comparable findMax( ) --> Return largest item

// boolean isEmpty( ) --> Return true if empty; else false

// void makeEmpty( ) --> Remove all items

// void printTree( ) --> Print tree in sorted order

// ******************ERRORS********************************

// Throws UnderflowException as appropriate

/**

* Implements an AVL tree.

* Note that all "matching" is based on the compareTo method.

* @author Mark Allen Weiss

*/

public class AvlTree>

{

/**

* Construct the tree.

*/

public AvlTree( )

{

root = null;

}

/**

* Insert into the tree; duplicates are ignored.

* @param x the item to insert.

*/

public void insert( AnyType x )

{

root = insert( x, root );

}

/**

* Remove from the tree. Nothing is done if x is not found.

* @param x the item to remove.

*/

public void remove( AnyType x )

{

root = remove( x, root );

}

/**

* Internal method to remove from a subtree.

* @param x the item to remove.

* @param t the node that roots the subtree.

* @return the new root of the subtree.

*/

private AvlNode remove( AnyType x, AvlNode t )

{

if( t == null )

return t; // Item not found; do nothing

int compareResult = x.compareTo( t.element );

if( compareResult < 0 )

t.left = remove( x, t.left );

else if( compareResult > 0 )

t.right = remove( x, t.right );

else if( t.left != null && t.right != null ) // Two children

{

t.element = findMin( t.right ).element;

t.right = remove( t.element, t.right );

}

else

t = ( t.left != null ) ? t.left : t.right;

return balance( t );

}

/**

* Find the smallest item in the tree.

* @return smallest item or null if empty.

*/

public AnyType findMin( )

{

if( isEmpty( ) )

throw new UnderflowException( );

return findMin( root ).element;

}

/**

* Find the largest item in the tree.

* @return the largest item of null if empty.

*/

public AnyType findMax( )

{

if( isEmpty( ) )

throw new UnderflowException( );

return findMax( root ).element;

}

/**

* Find an item in the tree.

* @param x the item to search for.

* @return true if x is found.

*/

public boolean contains( AnyType x )

{

return contains( x, root );

}

/**

* Make the tree logically empty.

*/

public void makeEmpty( )

{

root = null;

}

/**

* Test if the tree is logically empty.

* @return true if empty, false otherwise.

*/

public boolean isEmpty( )

{

return root == null;

}

/**

* Print the tree contents in sorted order.

*/

public void printTree( )

{

if( isEmpty( ) )

System.out.println( "Empty tree" );

else

printTree( root );

}

/**

* Print the tree contents in preorder.

*/

public void preorder()

{

if( isEmpty() )

System.out.println( "Empty tree" );

else

preorder( root , 0 );

System.out.println();

}

private static final int ALLOWED_IMBALANCE = 1;

// Assume t is either balanced or within one of being balanced

private AvlNode balance( AvlNode t )

{

if( t == null )

return t;

if( height( t.left ) - height( t.right ) > ALLOWED_IMBALANCE )

if( height( t.left.left ) >= height( t.left.right ) )

t = rotateWithLeftChild( t );

else

t = doubleWithLeftChild( t );

else

if( height( t.right ) - height( t.left ) > ALLOWED_IMBALANCE )

if( height( t.right.right ) >= height( t.right.left ) )

t = rotateWithRightChild( t );

else

t = doubleWithRightChild( t );

t.height = Math.max( height( t.left ), height( t.right ) ) + 1;

return t;

}

public void checkBalance( )

{

checkBalance( root );

}

private int checkBalance( AvlNode t )

{

if( t == null )

return -1;

if( t != null )

{

int hl = checkBalance( t.left );

int hr = checkBalance( t.right );

if( Math.abs( height( t.left ) - height( t.right ) ) > 1 ||

height( t.left ) != hl || height( t.right ) != hr )

System.out.println( "OOPS!!" );

}

return height( t );

}

/**

* Internal method to insert into a subtree.

* @param x the item to insert.

* @param t the node that roots the subtree.

* @return the new root of the subtree.

*/

private AvlNode insert( AnyType x, AvlNode t )

{

if( t == null )

return new AvlNode<>( x, null, null );

int compareResult = x.compareTo( t.element );

if( compareResult < 0 )

t.left = insert( x, t.left );

else if( compareResult > 0 )

t.right = insert( x, t.right );

else

; // Duplicate; do nothing

return balance( t );

}

/**

* Internal method to find the smallest item in a subtree.

* @param t the node that roots the tree.

* @return node containing the smallest item.

*/

private AvlNode findMin( AvlNode t )

{

if( t == null )

return t;

while( t.left != null )

t = t.left;

return t;

}

/**

* Internal method to find the largest item in a subtree.

* @param t the node that roots the tree.

* @return node containing the largest item.

*/

private AvlNode findMax( AvlNode t )

{

if( t == null )

return t;

while( t.right != null )

t = t.right;

return t;

}

/**

* Internal method to find an item in a subtree.

* @param x is item to search for.

* @param t the node that roots the tree.

* @return true if x is found in subtree.

*/

private boolean contains( AnyType x, AvlNode t )

{

while( t != null )

{

int compareResult = x.compareTo( t.element );

if( compareResult < 0 )

t = t.left;

else if( compareResult > 0 )

t = t.right;

else

return true; // Match

}

return false; // No match

}

/**

* Internal method to print a subtree in sorted order.

* @param t the node that roots the tree.

*/

private void printTree( AvlNode t )

{

if( t != null )

{

printTree( t.left );

System.out.println( t.element );

printTree( t.right );

}

}

/**

* Internal method to print a subtree in preorder.

* @param t the node that roots the tree.

*/

private void preorder( AvlNode t, int blanks)

{

if( t != null )

{

System.out.println();

for(int i = 0 ;i < blanks; i++)

System.out.print(" ");

System.out.print( t.element );

preorder( t.left, blanks+1 );

preorder( t.right, blanks+1 );

}

}

/**

* Return the height of node t, or -1, if null.

*/

private int height( AvlNode t )

{

return t == null ? -1 : t.height;

}

/**

* Rotate binary tree node with left child.

* For AVL trees, this is a single rotation for case 1.

* Update heights, then return new root.

*/

private AvlNode rotateWithLeftChild( AvlNode k2 )

{

AvlNode k1 = k2.left;

k2.left = k1.right;

k1.right = k2;

k2.height = Math.max( height( k2.left ), height( k2.right ) ) + 1;

k1.height = Math.max( height( k1.left ), k2.height ) + 1;

return k1;

}

/**

* Rotate binary tree node with right child.

* For AVL trees, this is a single rotation for case 4.

* Update heights, then return new root.

*/

private AvlNode rotateWithRightChild( AvlNode k1 )

{

AvlNode k2 = k1.right;

k1.right = k2.left;

k2.left = k1;

k1.height = Math.max( height( k1.left ), height( k1.right ) ) + 1;

k2.height = Math.max( height( k2.right ), k1.height ) + 1;

return k2;

}

/**

* Double rotate binary tree node: first left child

* with its right child; then node k3 with new left child.

* For AVL trees, this is a double rotation for case 2.

* Update heights, then return new root.

*/

private AvlNode doubleWithLeftChild( AvlNode k3 )

{

k3.left = rotateWithRightChild( k3.left );

return rotateWithLeftChild( k3 );

}

/**

* Double rotate binary tree node: first right child

* with its left child; then node k1 with new right child.

* For AVL trees, this is a double rotation for case 3.

* Update heights, then return new root.

*/

private AvlNode doubleWithRightChild( AvlNode k1 )

{

k1.right = rotateWithLeftChild( k1.right );

return rotateWithRightChild( k1 );

}

private static class AvlNode

{

// Constructors

AvlNode( AnyType theElement )

{

this( theElement, null, null );

}

AvlNode( AnyType theElement, AvlNode lt, AvlNode rt )

{

element = theElement;

left = lt;

right = rt;

height = 0;

}

AnyType element; // The data in the node

AvlNode left; // Left child

AvlNode right; // Right child

int height; // Height

}

/** The tree root. */

private AvlNode root;

// Test program

public static void main( String [ ] args )

{

AvlTree t = new AvlTree<>( );

int[] nodes = {3,2,1,4,5,6,7,16,15,14,13,12,11,10,8,9};

for( int i = 0; i < nodes.length; i++ )

{

System.out.println( "INSERT: " + nodes[i] );

t.insert( nodes[i] );

t.preorder();

}

}

}

完整源码 TestAvlTree.cpp

#ifndef AVL_TREE_H

#define AVL_TREE_H

#include "dsexceptions.h"

#include

#include

using namespace std;

// AvlTree class

//

// CONSTRUCTION: zero parameter

//

// ******************PUBLIC OPERATIONS*********************

// void insert( x ) --> Insert x

// void remove( x ) --> Remove x (unimplemented)

// bool contains( x ) --> Return true if x is present

// Comparable findMin( ) --> Return smallest item

// Comparable findMax( ) --> Return largest item

// boolean isEmpty( ) --> Return true if empty; else false

// void makeEmpty( ) --> Remove all items

// void printTree( ) --> Print tree in sorted order

// ******************ERRORS********************************

// Throws UnderflowException as warranted

template

class AvlTree

{

public:

AvlTree( ) : root{ nullptr }

{ }

AvlTree( const AvlTree & rhs ) : root{ nullptr }

{

root = clone( rhs.root );

}

AvlTree( AvlTree && rhs ) : root{ rhs.root }

{

rhs.root = nullptr;

}

~AvlTree( )

{

makeEmpty( );

}

/**

* Deep copy.

*/

AvlTree & operator=( const AvlTree & rhs )

{

AvlTree copy = rhs;

std::swap( *this, copy );

return *this;

}

/**

* Move.

*/

AvlTree & operator=( AvlTree && rhs )

{

std::swap( root, rhs.root );

return *this;

}

/**

* Find the smallest item in the tree.

* Throw UnderflowException if empty.

*/

const Comparable & findMin( ) const

{

if( isEmpty( ) )

throw UnderflowException{ };

return findMin( root )->element;

}

/**

* Find the largest item in the tree.

* Throw UnderflowException if empty.

*/

const Comparable & findMax( ) const

{

if( isEmpty( ) )

throw UnderflowException{ };

return findMax( root )->element;

}

/**

* Returns true if x is found in the tree.

*/

bool contains( const Comparable & x ) const

{

return contains( x, root );

}

/**

* Test if the tree is logically empty.

* Return true if empty, false otherwise.

*/

bool isEmpty( ) const

{

return root == nullptr;

}

/**

* Print the tree contents in sorted order.

*/

void printTree( ) const

{

if( isEmpty( ) )

cout << "Empty tree" << endl;

else

printTree( root );

}

/**

* Make the tree logically empty.

*/

void makeEmpty( )

{

makeEmpty( root );

}

/**

* Insert x into the tree; duplicates are ignored.

*/

void insert( const Comparable & x )

{

insert( x, root );

}

/**

* Insert x into the tree; duplicates are ignored.

*/

void insert( Comparable && x )

{

insert( std::move( x ), root );

}

/**

* Remove x from the tree. Nothing is done if x is not found.

*/

void remove( const Comparable & x )

{

remove( x, root );

}

private:

struct AvlNode

{

Comparable element;

AvlNode *left;

AvlNode *right;

int height;

AvlNode( const Comparable & ele, AvlNode *lt, AvlNode *rt, int h = 0 )

: element{ ele }, left{ lt }, right{ rt }, height{ h } { }

AvlNode( Comparable && ele, AvlNode *lt, AvlNode *rt, int h = 0 )

: element{ std::move( ele ) }, left{ lt }, right{ rt }, height{ h } { }

};

AvlNode *root;

/**

* Internal method to insert into a subtree.

* x is the item to insert.

* t is the node that roots the subtree.

* Set the new root of the subtree.

*/

void insert( const Comparable & x, AvlNode * & t )

{

if( t == nullptr )

t = new AvlNode{ x, nullptr, nullptr };

else if( x < t->element )

insert( x, t->left );

else if( t->element < x )

insert( x, t->right );

balance( t );

}

/**

* Internal method to insert into a subtree.

* x is the item to insert.

* t is the node that roots the subtree.

* Set the new root of the subtree.

*/

void insert( Comparable && x, AvlNode * & t )

{

if( t == nullptr )

t = new AvlNode{ std::move( x ), nullptr, nullptr };

else if( x < t->element )

insert( std::move( x ), t->left );

else if( t->element < x )

insert( std::move( x ), t->right );

balance( t );

}

/**

* Internal method to remove from a subtree.

* x is the item to remove.

* t is the node that roots the subtree.

* Set the new root of the subtree.

*/

void remove( const Comparable & x, AvlNode * & t )

{

if( t == nullptr )

return; // Item not found; do nothing

if( x < t->element )

remove( x, t->left );

else if( t->element < x )

remove( x, t->right );

else if( t->left != nullptr && t->right != nullptr ) // Two children

{

t->element = findMin( t->right )->element;

remove( t->element, t->right );

}

else

{

AvlNode *oldNode = t;

t = ( t->left != nullptr ) ? t->left : t->right;

delete oldNode;

}

balance( t );

}

static const int ALLOWED_IMBALANCE = 1;

// Assume t is balanced or within one of being balanced

void balance( AvlNode * & t )

{

if( t == nullptr )

return;

if( height( t->left ) - height( t->right ) > ALLOWED_IMBALANCE )

if( height( t->left->left ) >= height( t->left->right ) )

rotateWithLeftChild( t );

else

doubleWithLeftChild( t );

else

if( height( t->right ) - height( t->left ) > ALLOWED_IMBALANCE )

if( height( t->right->right ) >= height( t->right->left ) )

rotateWithRightChild( t );

else

doubleWithRightChild( t );

t->height = max( height( t->left ), height( t->right ) ) + 1;

}

/**

* Internal method to find the smallest item in a subtree t.

* Return node containing the smallest item.

*/

AvlNode * findMin( AvlNode *t ) const

{

if( t == nullptr )

return nullptr;

if( t->left == nullptr )

return t;

return findMin( t->left );

}

/**

* Internal method to find the largest item in a subtree t.

* Return node containing the largest item.

*/

AvlNode * findMax( AvlNode *t ) const

{

if( t != nullptr )

while( t->right != nullptr )

t = t->right;

return t;

}

/**

* Internal method to test if an item is in a subtree.

* x is item to search for.

* t is the node that roots the tree.

*/

bool contains( const Comparable & x, AvlNode *t ) const

{

if( t == nullptr )

return false;

else if( x < t->element )

return contains( x, t->left );

else if( t->element < x )

return contains( x, t->right );

else

return true; // Match

}

/****** NONRECURSIVE VERSION*************************

bool contains( const Comparable & x, AvlNode *t ) const

{

while( t != nullptr )

if( x < t->element )

t = t->left;

else if( t->element < x )

t = t->right;

else

return true; // Match

return false; // No match

}

*****************************************************/

/**

* Internal method to make subtree empty.

*/

void makeEmpty( AvlNode * & t )

{

if( t != nullptr )

{

makeEmpty( t->left );

makeEmpty( t->right );

delete t;

}

t = nullptr;

}

/**

* Internal method to print a subtree rooted at t in sorted order.

*/

void printTree( AvlNode *t ) const

{

if( t != nullptr )

{

printTree( t->left );

cout << t->element << endl;

printTree( t->right );

}

}

/**

* Internal method to clone subtree.

*/

AvlNode * clone( AvlNode *t ) const

{

if( t == nullptr )

return nullptr;

else

return new AvlNode{ t->element, clone( t->left ), clone( t->right ), t->height };

}

// Avl manipulations

/**

* Return the height of node t or -1 if nullptr.

*/

int height( AvlNode *t ) const

{

return t == nullptr ? -1 : t->height;

}

int max( int lhs, int rhs ) const

{

return lhs > rhs ? lhs : rhs;

}

/**

* Rotate binary tree node with left child.

* For AVL trees, this is a single rotation for case 1.

* Update heights, then set new root.

*/

void rotateWithLeftChild( AvlNode * & k2 )

{

AvlNode *k1 = k2->left;

k2->left = k1->right;

k1->right = k2;

k2->height = max( height( k2->left ), height( k2->right ) ) + 1;

k1->height = max( height( k1->left ), k2->height ) + 1;

k2 = k1;

}

/**

* Rotate binary tree node with right child.

* For AVL trees, this is a single rotation for case 4.

* Update heights, then set new root.

*/

void rotateWithRightChild( AvlNode * & k1 )

{

AvlNode *k2 = k1->right;

k1->right = k2->left;

k2->left = k1;

k1->height = max( height( k1->left ), height( k1->right ) ) + 1;

k2->height = max( height( k2->right ), k1->height ) + 1;

k1 = k2;

}

/**

* Double rotate binary tree node: first left child.

* with its right child; then node k3 with new left child.

* For AVL trees, this is a double rotation for case 2.

* Update heights, then set new root.

*/

void doubleWithLeftChild( AvlNode * & k3 )

{

rotateWithRightChild( k3->left );

rotateWithLeftChild( k3 );

}

/**

* Double rotate binary tree node: first right child.

* with its left child; then node k1 with new right child.

* For AVL trees, this is a double rotation for case 3.

* Update heights, then set new root.

*/

void doubleWithRightChild( AvlNode * & k1 )

{

rotateWithLeftChild( k1->right );

rotateWithRightChild( k1 );

}

};

#endif

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值