数据结构(6)-二叉搜索树

本文深入探讨了二叉搜索树的定义、特性及其在Java中的实现。覆盖了节点查找、插入、删除等关键操作,以及不同遍历方式的示例代码。通过分析二叉树的效率,展示了其在数据处理上的优势。


二叉树和队列、链表等一样,都是常用的数据结构。前面我们介绍数组的数据结构,我们知道对于有序数组,查找很快,并介绍可以通过二分法查找,但是想要在有序数组中插入一个数据项,就必须先找到插入数据项的位置,然后将所有插入位置后面的数据项全部向后移动一位,来给新数据腾出空间,平均来讲要移动N/2次,这是很费时的。同理,删除数据也是。但是二叉树的出现很好地解决了这些问题。

一、什么是树结构

树(tree)是一种抽象数据类型(ADT),用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点通过连接它们的边组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

1、节点:上图的圆圈,比如A,B,C等都是表示节点。节点一般代表一些实体,在java面向对象编程中,节点一般代表对象。
2、边:连接节点的线称为边,边表示节点的关联关系。一般从一个节点到另一个节点的唯一方法就是沿着一条顺着有边的道路前进。在Java当中通常表示引用。

二、树的常用术语

在这里插入图片描述

1、路径:顺着节点的边从一个节点走到另一个节点,所经过的节点的顺序排列就称为“路径”。

2、根:树顶端的节点称为根。一棵树只有一个根,如果要把一个节点和边的集合称为树,那么从根到其他任何一个节点都必须有且只有一条路径。A是根节点。

3、父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;B是D的父节点。

4、子节点:一个节点含有的子树的根节点称为该节点的子节点;D是B的子节点。

5、兄弟节点:具有相同父节点的节点互称为兄弟节点;比如上图的D和E就互称为兄弟节点。

6、叶节点:没有子节点的节点称为叶节点,也叫叶子节点,比如上图的A、E、F、G都是叶子节点。

7、子树:每个节点都可以作为子树的根,它和它所有的子节点、子节点的子节点等都包含在子树中。

8、节点的层次:从根开始定义,根为第一层,根的子节点为第二层,以此类推。

9、深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0;

10、高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;

三、二叉树搜索树

二叉树:树的每个节点最多只能有两个子节点
在这里插入图片描述

如果我们给二叉树加一个额外的条件,就可以得到一种被称作二叉搜索树(binary search tree)的特殊二叉树。

二叉搜索树要求:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。

四、用Java实现二叉搜索树(BST)

1、定义
public class BinarySearchTree<K extends Comparable<K>,V> {
    private Node root;
    private Long nodeNumber = 0l;

    public class Node {
        private K key;
        private V value;
        private Node parent,left, right;
 
		//各种方法


    }
2、二叉树的节点类
public class Node {
        private K key;
        private V value;
        private Node parent,left, right;

        public Node(K key, V value, Node parent, Node left, Node right) {
            this.key = key;
            this.value = value;
            this.parent = parent;
            this.left = left;
            this.right = right;
        }
    }
3、查找节点

查找某个节点,我们必须从根节点开始遍历。

1、查找值比当前节点值大,则搜索右子树;

2、查找值等于当前节点值,停止搜索(终止条件);

3、查找值小于当前节点值,则搜索左子树;

	/**
     * @auther: Arong
     * @description: 获取key的值
     * @param: [key]
     * @return: V
     */
    public V get(K key) {
        V value = get(root, key).value;
        return value == null ? null : value;
    }

    private Node get(Node node, K key) {
        Node currentNode = node;
        while (currentNode != null) {
            int cmp = key.compareTo(currentNode.key);
            if (cmp < 0) {
                //查询左子树
                currentNode = currentNode.left;
            } else if (cmp > 0) {
                //查询右子树
                currentNode = currentNode.right;
            } else {
                //就是该节点
                break;
            }
        }
        return currentNode;
    }

说明:用变量currentNode来保存当前查找的节点,参数key是要查找的值,刚开始查找将传入节点赋值到currentNode。接在在while循环中,将要查找的值和currentNode保存的节点进行对比。如果key小于当前节点,则搜索当前节点的左子节点,如果大于,则搜索右子节点,如果等于,则直接返回节点信息。当整个树遍历完全,即currentNode == null,那么说明没找到查找值,返回null。

查找操作的时间复杂度:查找节点的时间取决于这个节点所在的层数,每一层最多有2n-1个节点,总共N层共有2n-1个节点,那么时间复杂度为O(logn),底数为2,这个复杂度和用二分法查找有序数组时的效率一样。

4、插入节点

要插入节点,必须先找到插入的位置。与查找操作相似,由于二叉搜索树的特殊性,待插入的节点也需要从根节点开始进行比较,小于根节点则与根节点左子树比较,反之则与右子树比较,直到左子树为空或右子树为空,则插入到相应为空的位置。

/**
     * @auther: Arong
     * @description:  插入键值对
     * @param: [key, value]
     * @return: void
     */
    public void put(K key, V value) {
        put(root, key, value);
    }

    private int put(Node node, K key, V value) {
        if (node == null) {
            //根节点为空
            root = new Node(key, value, null, null, null);
            nodeNumber++;
            return 1;
        }
        //寻找该节点的合适位置
        Node parentNode = node;
        Node currentNode = node;
        while (currentNode != null) {
            int cmp = key.compareTo(currentNode.key);
            if (cmp < 0) {
                //在左子树插入
                parentNode = currentNode;
                currentNode = currentNode.left;
            } else if (cmp > 0) {
                //在右子树中插入
                parentNode = currentNode;
                currentNode = currentNode.right;
            } else {
                //键相等,说明已存在,更新这个值
                currentNode.value = value;
                return 1;
            }
        }
        //已找到合适的位置,建立节点关系
        int cmp = key.compareTo(parentNode.key);
        if (cmp < 0) {
            parentNode.left = new Node(key, value, parentNode, null, null);
        } else {
            parentNode.right = new Node(key, value, parentNode, null, null);;
        }
        nodeNumber++;
        return 1;
    }
5、遍历树

遍历树是根据一种特定的顺序访问树的每一个节点。比较常用的有前序遍历,中序遍历和后序遍历。而二叉搜索树最常用的是中序遍历。

5.1、深度优先遍历(DFS)

1、中序遍历:左子树->根节点->右子树

2、前序遍历:根节点->左子树->右子树

3、后序遍历:左子树->右子树->根节点

 private ArrayList<Node> nodes = new ArrayList<>();

    /**
     * @auther: Arong
     * @description: 前序遍历
     * @return: List<Node>
     */
    public List<Node> preOrder() {
        preOrder(root);
        return nodes;
    }


    private void preOrder(Node node) {
        if (node != null) {
            //先获取自身
            nodes.add(node);
            //递归获取左子节点
            preOrder(node.left);
            //递归获取右子节点
            preOrder(node.right);
        }
    }

	//迭代版本
	public void preOrder(Node node) {
        //  判断stack不为空即弹栈,并压入当前弹栈的右节点和左节点,再次弹栈,直至右节点和左节点为空
        if (node == null) return;
        Stack<Node> stack = new Stack<>();
        stack.push(node);
        while (!stack.isEmpty()) {
            //弹出的元素即为前序遍历取得的元素
            Node pop = stack.pop();
            nodes.add(pop);
            //压入右子节点
            if (pop.right != null) {
                stack.push(pop.right);
            }
            //压入左子节点
            if (pop.left != null) {
                stack.push(pop.left);
            }
        }
    }


    /**
     * @auther: Arong
     * @description: 中序遍历
     * @return: List<Node>
     */
    public List<Node> midOrder() {
        midOrder(root);
        return nodes;
    }


    private void midOrder(Node node) {
        if (node != null) {
            //递归获取左子节点
            midOrder(node.left);
            //先获取自身
            nodes.add(node);
            //递归获取右子节点
            midOrder(node.right);
        }
    }

	//迭代版本
	private void midOrder(Node node) {
		if (node == null) return;
		Stack<Node> stack = new Stack<>();
		while (!stack.isEmpty() || node != null) {
			if (node != null) {
				stack.push(node);
				node = node.left;
			} else {
				Node pop = stack.pop();
				nodes.add(pop.val);
				node = pop.right;
			}
		}	
	}

    /**
     * @auther: Arong
     * @description: 后序遍历
     * @return: List<Node>
     */
    public List<Node> postOrder() {
        postOrder(root);
        return nodes;
    }


    private void postOrder(Node node) {
        if (node != null) {
            //递归获取左子节点
            postOrder(node.left);
            //递归获取右子节点
            postOrder(node.right);
            //先获取自身
            nodes.add(node);
        }
    }

5.2、广度优先遍历(BFS)
    /**
     * @auther: Arong
     * @description: 广度优先遍历(层序遍历)
     * @param: []
     * @return: java.util.List<data_struct.BinarySearchTree<K,V>.Node>
     */
    public List<Node> bfs() {
        LinkedListQuene<Node> quene = new LinkedListQuene<>();
        ArrayList<Node> nodes = new ArrayList<>();
        if (root == null) {
            return nodes;
        }
        quene.enquene(root);
        while (!quene.isEmpty()) {
            Node currentNode = quene.dequene();
            if (currentNode.left != null) quene.enquene(currentNode.left );
            if (currentNode.right != null) quene.enquene(currentNode.right);
            nodes.add(currentNode);
        }

        return nodes;
    }

6、查找最大值和最小值

要找最小值,先找根的左节点,然后一直找这个左节点的左节点,直到找到没有左节点的节点,那么这个节点就是最小值。同理要找最大值,一直找根节点的右节点,直到没有右节点,则就是最大值。

 	/**
     * @auther: Arong
     * @description:  查找最大值
     * @param: []
     * @return: V
     */
    public V max() {
        return max(root).value == null ? null : max(root).value;
    }

    private Node max(Node node) {
        Node currentNode = node;
        while (currentNode != null && currentNode.right != null) {
            currentNode = currentNode.right;
        }
        return currentNode;
    }


	/**
     * @auther: Arong
     * @description:  查找最小值
     * @param: []
     * @return: V
     */
    public V min() {
        V value = min(root).value;
        return value == null ? null : value;
    }

    private Node min(Node node) {
        Node currentNode = node;
        while (currentNode != null && currentNode.left != null) {
            currentNode = currentNode.left;
        }
        return currentNode;
    }
7、删除节点

删除节点是二叉搜索树中最复杂的操作,删除的节点有三种情况,前两种比较简单,但是第三种却很复杂。

1、该节点是叶节点(没有子节点)

2、该节点只有一个子节点

3、该节点有两个子节点

下面我们分别对这三种情况进行讲解。
  
  
(1)删除没有子节点的节点

要删除叶节点,只需要改变该节点的父节点引用该节点的值,即将其引用改为 null 即可。要删除的节点依然存在,但是它已经不是树的一部分了,由于Java语言的垃圾回收机制,我们不需要非得把节点本身删掉,一旦Java意识到程序不在与该节点有关联,就会自动把它清理出存储器。
  在这里插入图片描述

@Override
        if (left == null && right == null) {
            if (keyNode == root) {
                //是根节点
                root = null;
                nodeNumber--;
                return deleteNode;
            }

            //直接删除父节点和它的关系
            if (keyNode.key == parent.left.key) {
                parent.left = null;
            } else {
                parent.right = null;
            }

            keyNode.parent = null;
        }

删除节点,我们要先找到该节点,并记录该节点的父节点。在检查该节点是否有子节点。如果没有子节点,接着检查其是否是根节点,如果是根节点,只需要将其设置为null即可。如果不是根节点,是叶节点,那么断开父节点和其的 关系即可。

(2)删除有一个子节点的节点
  删除有一个子节点的节点,我们只需要将其父节点原本指向该节点的引用,改为指向该节点的子节点即可。
  在这里插入图片描述

          //第二种情况
        if (left != null || right != null) {
            //是根节点
            if (parent == null) {
                //判断是根含有左还是右子节点
                if (left != null) {
                    root = keyNode.left;
                } else {
                    root = keyNode.right;
                }
                return deleteNode;
            }

            //判断该节点是父节点的左子节点还是右子节点
            if (key == parent.left.key) {
                //是父节点的左子节点
                //判断该节点是存在左节点还是右节点
                if (left != null) {
                    //左子节点不为空,让该节点的父节点直接连接该节点的左子节点,建立关系
                    parent.left = left;
                } else {
                    //右子节点不为空,让该节点的父节点直接连接该节点的右子节点
                    parent.left = right;
                }
            } else {
                //是父节点的右子节点
                //判断该节点是存在左节点还是右节点
                if (left != null) {
                    //左子节点不为空,让该节点的父节点直接连接该节点的左子节点,建立关系
                    parent.right = left;
                } else {
                    //右子节点不为空,让该节点的父节点直接连接该节点的右子节点
                    parent.right = right;
                }
            }

(3)删除有两个子节点的节点
这是最复杂的一个操作,可以参考大神的博客:https://blog.youkuaiyun.com/isea533/article/details/80345507

 //被删除节点的父、左、右节点
        Node deleteNode = keyNode;
        Node parent = keyNode.parent;
        Node left = keyNode.left;
        Node right = keyNode.right;

        //第三种情况,将被删除节点替换为后继节点,然后以第一二种情况删掉旧后继节点
        if (left != null && right != null) {
            Node successorNode = findSuccessorNode(keyNode);
            V deleteValue = keyNode.value;
            //用后继节点替换掉需要被删除的节点
            keyNode.key = successorNode.key;
            keyNode.value = successorNode.value;
            //下列的删除操作都是对后继节点的操作了
            successorNode.key = key;
            successorNode.value = deleteValue;
            deleteNode = successorNode;

            parent = successorNode.parent;
            left = successorNode.left;
            right = successorNode.right;
            //现在直接把后继节点当成1、2这两种情况删除即可
        }

五、二叉树的效率

从前面的大部分对树的操作来看,都需要从根节点到下一层一层的查找。

一颗满树,每层节点数大概为2n-1,那么最底层的节点个数比树的其它节点数多1,因此,查找、插入或删除节点的操作大约有一半都需要找到底层的节点,另外四分之一的节点在倒数第二层,依次类推。

总共N层共有2n-1个节点,那么时间复杂度为O(logn),底数为2。

在有1000000 个数据项的无序数组和链表中,查找数据项平均会比较500000 次,但是在有1000000个节点的二叉树中,只需要20次或更少的比较即可。

有序数组可以很快的找到数据项,但是插入数据项的平均需要移动 500000 次数据项,在 1000000 个节点的二叉树中插入数据项需要20次或更少比较,在加上很短的时间来连接数据项。

同样,从 1000000 个数据项的数组中删除一个数据项平均需要移动 500000 个数据项,而在 1000000 个节点的二叉树中删除节点只需要20次或更少的次数来找到他,然后在花一点时间来找到它的后继节点,一点时间来断开节点以及连接后继节点。

所以,树对所有常用数据结构的操作都有很高的效率。

遍历可能不如其他操作快,但是在大型数据库中,遍历是很少使用的操作,它更常用于程序中的辅助算法来解析算术或其它表达式。

六、完整代码

public class BinarySearchTree2<K extends Comparable<K>,V> {
    private Node root;
    private Long nodeNumber = 0l;

    public class Node {
        public  K key;
        public  V value;
        public Node parent,left, right;

        public Node(K key, V value, Node parent, Node left, Node right) {
            this.key = key;
            this.value = value;
            this.parent = parent;
            this.left = left;
            this.right = right;
        }
    }



    /**
     * @auther: Arong
     * @description: 获取key的值
     * @param: [key]
     * @return: V
     */
    public V get(K key) {
        V value = get(root, key).value;
        return value == null ? null : value;
    }

    private Node get(Node node, K key) {
        Node currentNode = node;
        while (currentNode != null) {
            int cmp = key.compareTo(currentNode.key);
            if (cmp < 0) {
                //查询左子树
                currentNode = currentNode.left;
            } else if (cmp > 0) {
                //查询右子树
                currentNode = currentNode.right;
            } else {
                //就是该节点
                break;
            }
        }
        return currentNode;
    }
    /**
     * @auther: Arong
     * @description:  插入键值对
     * @param: [key, value]
     * @return: void
     */
    public void put(K key, V value) {
        put(root, key, value);
    }

    private int put(Node node, K key, V value) {
        if (node == null) {
            //根节点为空
            root = new Node(key, value, null, null, null);
            nodeNumber++;
            return 1;
        }
        //寻找该节点的合适位置
        Node parentNode = node;
        Node currentNode = node;
        while (currentNode != null) {
            int cmp = key.compareTo(currentNode.key);
            if (cmp < 0) {
                //在左子树插入
                parentNode = currentNode;
                currentNode = currentNode.left;
            } else if (cmp > 0) {
                //在右子树中插入
                parentNode = currentNode;
                currentNode = currentNode.right;
            } else {
                //键相等,说明已存在,更新这个值
                currentNode.value = value;
                return 1;
            }
        }
        //已找到合适的位置,建立节点关系
        int cmp = key.compareTo(parentNode.key);
        if (cmp < 0) {
            parentNode.left = new Node(key, value, parentNode, null, null);
        } else {
            parentNode.right = new Node(key, value, parentNode, null, null);;
        }
        nodeNumber++;
        return 1;
    }

    /**
     * @auther: Arong
     * @description: 删除键值对
     * @param: [key]
     * @return: V
     */
    public V delete(K key) {
        Node node = delete(root, key);
        return node == null ? null : node.value;
    }

    private Node delete(Node node, K key) {
        //分三种情况:
        //1、需要被删除的节点没有子节点,此时直接删除父节点和它的关系即可
        //2、需要被删除的节点只有左子节点或者右子节点,此时需要让父节点指向该节点的子节点
        //3、需要被删除的节点同时有左右两个子节点,此时从左子树中找“最大值”或者从右子树中找“最小值”来替换调该节点的值,并且将该“最大值”或“最小值”节点通过1、2的方法删去即可

        //找到key的节点
        Node keyNode = get(node, key);
        if (keyNode == null) {
            return null;
        }


        //被删除节点的父、左、右节点
        Node deleteNode = keyNode;
        Node parent = keyNode.parent;
        Node left = keyNode.left;
        Node right = keyNode.right;

        //第三种情况,将被删除节点替换为后继节点,然后以第一二种情况删掉旧后继节点
        if (left != null && right != null) {
            Node successorNode = findSuccessorNode(keyNode);
            V deleteValue = keyNode.value;
            //用后继节点替换掉需要被删除的节点
            keyNode.key = successorNode.key;
            keyNode.value = successorNode.value;
            //下列的删除操作都是对后继节点的操作了
            successorNode.key = key;
            successorNode.value = deleteValue;
            deleteNode = successorNode;

            parent = successorNode.parent;
            left = successorNode.left;
            right = successorNode.right;
            //现在直接把后继节点当成1、2这两种情况删除即可
        }


        //第一种情况
        if (left == null && right == null) {
            if (parent == null) {
                //是根节点
                root = null;
                nodeNumber--;
                return deleteNode;
            }

            //直接删除父节点和它的关系
            if (key == parent.left.key) {
                parent.left = null;
            } else {
                parent.right = null;
            }

            keyNode.parent = null;
        }

        //第二种情况
        if (left != null || right != null) {
            //是根节点
            if (parent == null) {
                //判断是根含有左还是右子节点
                if (left != null) {
                    root = keyNode.left;
                } else {
                    root = keyNode.right;
                }
                return deleteNode;
            }

            //判断该节点是父节点的左子节点还是右子节点
            if (key == parent.left.key) {
                //是父节点的左子节点
                //判断该节点是存在左节点还是右节点
                if (left != null) {
                    //左子节点不为空,让该节点的父节点直接连接该节点的左子节点,建立关系
                    parent.left = left;
                } else {
                    //右子节点不为空,让该节点的父节点直接连接该节点的右子节点
                    parent.left = right;
                }
            } else {
                //是父节点的右子节点
                //判断该节点是存在左节点还是右节点
                if (left != null) {
                    //左子节点不为空,让该节点的父节点直接连接该节点的左子节点,建立关系
                    parent.right = left;
                } else {
                    //右子节点不为空,让该节点的父节点直接连接该节点的右子节点
                    parent.right = right;
                }
            }

        }

        nodeNumber--;
        return deleteNode;
    }

    private ArrayList<Node> nodes = new ArrayList<>();

    /**
     * @auther: Arong
     * @description: 前序遍历
     * @return: List<Node>
     */
    public List<Node> preOrder() {
        preOrder(root);
        return nodes;
    }


    private void preOrder(Node node) {
        if (node != null) {
            //先获取自身
            nodes.add(node);
            //递归获取左子节点
            preOrder(node.left);
            //递归获取右子节点
            preOrder(node.right);
        }
    }


    /**
     * @auther: Arong
     * @description: 中序遍历
     * @return: List<Node>
     */
    public List<Node> midOrder() {
        midOrder(root);
        return nodes;
    }


    private void midOrder(Node node) {
        if (node != null) {
            //递归获取左子节点
            midOrder(node.left);
            //先获取自身
            nodes.add(node);
            //递归获取右子节点
            midOrder(node.right);
        }
    }

    /**
     * @auther: Arong
     * @description: 后序遍历
     * @return: List<Node>
     */
    public List<Node> postOrder() {
        postOrder(root);
        return nodes;
    }


    private void postOrder(Node node) {
        if (node != null) {
            //递归获取左子节点
            postOrder(node.left);
            //递归获取右子节点
            postOrder(node.right);
            //先获取自身
            nodes.add(node);
        }
    }

    /**
     * @auther: Arong
     * @description: 寻找key的后继节点
     * @param: [root, key]
     * @return: data_struct.BinarySearchTree2<K,V>.Node
     */
    private Node findSuccessorNode(Node node) {
        if (node.left == null) {
            return node;
        }
        //左子树最大键
        Node max = max(node.left);
        return max;
    }
   /**
     * @auther: Arong
     * @description: 寻找key的后继节点
     * @param: [root, key]
     * @return: data_struct.BinarySearchTree2<K,V>.Node
     * @date: 下午1:22 19-5-20
     */
    private Node findSuccessorNode(Node node) {
        if (node.left == null) {
            return node;
        }
        //左子树最大键
        Node max = max(node.left);
        return max;
    }
    /**
     * @auther: Arong
     * @description:  查找最大值
     * @param: []
     * @return: V
     * @date: 下午7:32 19-5-21
     */
    public V max() {
        Node node = max(root);
        return node == null ? null : node.value;
    }

    private Node max(Node node) {
        Node currentNode = node;
        while (currentNode != null && currentNode.right != null) {
            currentNode = currentNode.right;
        }
        return currentNode;
    }


    /**
     * @auther: Arong
     * @description:  查找最小值
     * @param: []
     * @return: V
     * @date: 下午7:32 19-5-21
     */
    public V min() {
        Node node = min(root);
        return node == null ? null : node.value;
    }

    private Node min(Node node) {
        Node currentNode = node;
        while (currentNode != null && currentNode.left != null) {
            currentNode = currentNode.left;
        }
        return currentNode;
    }

    /**
     * @auther: Arong
     * @description: 广度优先遍历(层序遍历)
     * @param: []
     * @return: java.util.List<data_struct.BinarySearchTree2<K,V>.Node>
     * @date: 下午2:18 19-5-22
     */
    public List<Node> bfs() {
        LinkedListQuene<Node> quene = new LinkedListQuene<>();
        ArrayList<Node> nodes = new ArrayList<>();
        if (root == null) {
            return nodes;
        }
        quene.enquene(root);
        while (!quene.isEmpty()) {
            Node currentNode = quene.dequene();
            if (currentNode.left != null) quene.enquene(currentNode.left );
            if (currentNode.right != null) quene.enquene(currentNode.right);
            nodes.add(currentNode);
        }

        return nodes;
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

BoringRong

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

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

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

打赏作者

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

抵扣说明:

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

余额充值