6天通吃树结构—— 第二天 平衡二叉树

原 :

http://www.cnblogs.com/huangxincheng/archive/2012/07/22/2603956.html

 

上一篇我们聊过,二叉查找树不是严格的O(logN),导致了在真实场景中没有用武之地,谁也不愿意有O(N)的情况发生,

作为一名码农,肯定会希望能把“范围查找”做到地球人都不能优化的地步。

     当有很多数据灌到我的树中时,我肯定会希望最好是以“完全二叉树”的形式展现,这样我才能做到“查找”是严格的O(logN),

比如把这种”树“调正到如下结构。

     

这里就涉及到了“树节点”的旋转,也是我们今天要聊到的内容。

 

一:平衡二叉树(AVL)

1:定义

       父节点的左子树和右子树的高度之差不能大于1,也就是说不能高过1层,否则该树就失衡了,此时就要旋转节点,在

编码时,我们可以记录当前节点的高度,比如空节点是-1,叶子节点是0,非叶子节点的height往根节点递增,比如在下图

中我们认为树的高度为h=2。

 

#region 平衡二叉树节点
    /// <summary>
    /// 平衡二叉树节点
    /// </summary>
    /// <typeparam name="K"></typeparam>
    /// <typeparam name="V"></typeparam>
    public class AVLNode<K, V>
    {
        /// <summary>
        /// 节点元素
        /// </summary>
        public K key;

        /// <summary>
        /// 增加一个高度信息
        /// </summary>
        public int height;

        /// <summary>
        /// 节点中的附加值
        /// </summary>
        public HashSet<V> attach = new HashSet<V>();

        /// <summary>
        /// 左节点
        /// </summary>
        public AVLNode<K, V> left;

        /// <summary>
        /// 右节点
        /// </summary>
        public AVLNode<K, V> right;

        public AVLNode() { }

        public AVLNode(K key, V value, AVLNode<K, V> left, AVLNode<K, V> right)
        {
            //KV键值对
            this.key = key;
            this.attach.Add(value);

            this.left = left;
            this.right = right;
        }
    }
    #endregion

 

2:旋转

    节点再怎么失衡都逃不过4种情况,下面我们一一来看一下。

① 左左情况(左子树的左边节点)

我们看到,在向树中追加“节点1”的时候,根据定义我们知道这样会导致了“节点3"失衡,满足“左左情况“,可以这样想,把这

棵树比作齿轮,我们在“节点5”处把齿轮往下拉一个位置,也就变成了后面这样“平衡”的形式,如果用动画解释就最好理解了。

#region 第一种:左左旋转(单旋转)
        /// <summary>
        /// 第一种:左左旋转(单旋转)
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        public AVLNode<K, V> RotateLL(AVLNode<K, V> node)
        {
            //top:需要作为顶级节点的元素
            var top = node.left;

            //先截断当前节点的左孩子
            node.left = top.right;

            //将当前节点作为temp的右孩子
            top.right = node;

            //计算当前两个节点的高度
            node.height = Math.Max(Height(node.left), Height(node.right)) + 1;
            top.height = Math.Max(Height(top.left), Height(top.right)) + 1;

            return top;
        }
        #endregion

② 右右情况(右子树的右边节点)

同样,”节点5“满足”右右情况“,其实我们也看到,这两种情况是一种镜像,当然操作方式也大同小异,我们在”节点1“的地方

将树往下拉一位,最后也就形成了我们希望的平衡效果。

#region 第二种:右右旋转(单旋转)
        /// <summary>
        /// 第二种:右右旋转(单旋转)
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        public AVLNode<K, V> RotateRR(AVLNode<K, V> node)
        {
            //top:需要作为顶级节点的元素
            var top = node.right;

            //先截断当前节点的右孩子
            node.right = top.left;

            //将当前节点作为temp的右孩子
            top.left = node;

            //计算当前两个节点的高度
            node.height = Math.Max(Height(node.left), Height(node.right)) + 1;
            top.height = Math.Max(Height(top.left), Height(top.right)) + 1;

            return top;
        }
        #endregion

③左右情况(左子树的右边节点)

从图中我们可以看到,当我们插入”节点3“时,“节点5”处失衡,注意,找到”失衡点“是非常重要的,当面对”左右情况“时,我们将

失衡点的左子树进行"右右情况旋转",然后进行”左左情况旋转“,经过这样两次的旋转就OK了,很有意思,对吧。

#region 第三种:左右旋转(双旋转)
        /// <summary>
        /// 第三种:左右旋转(双旋转)
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        public AVLNode<K, V> RotateLR(AVLNode<K, V> node)
        {
            //先进行RR旋转
            node.left = RotateRR(node.left);

            //再进行LL旋转
            return RotateLL(node);
        }
        #endregion

④右左情况(右子树的左边节点)

这种情况和“情景3”也是一种镜像关系,很简单,我们找到了”节点15“是失衡点,然后我们将”节点15“的右子树进行”左左情况旋转“,

然后进行”右右情况旋转“,最终得到了我们满意的平衡。

#region 第四种:右左旋转(双旋转)
        /// <summary>
        /// 第四种:右左旋转(双旋转)
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        public AVLNode<K, V> RotateRL(AVLNode<K, V> node)
        {
            //执行左左旋转
            node.right = RotateLL(node.right);

            //再执行右右旋转
            return RotateRR(node);

        }
        #endregion

3:添加

    如果我们理解了上面的这几种旋转,那么添加方法简直是轻而易举,出现了哪一种情况调用哪一种方法而已。

#region 添加操作
        /// <summary>
        /// 添加操作
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="tree"></param>
        /// <returns></returns>
        public AVLNode<K, V> Add(K key, V value, AVLNode<K, V> tree)
        {
            if (tree == null)
                tree = new AVLNode<K, V>(key, value, null, null);

            //左子树
            if (key.CompareTo(tree.key) < 0)
            {
                tree.left = Add(key, value, tree.left);

                //如果说相差等于2就说明这棵树需要旋转了
                if (Height(tree.left) - Height(tree.right) == 2)
                {
                    //说明此时是左左旋转
                    if (key.CompareTo(tree.left.key) < 0)
                    {
                        tree = RotateLL(tree);
                    }
                    else
                    {
                        //属于左右旋转
                        tree = RotateLR(tree);
                    }
                }
            }

            //右子树
            if (key.CompareTo(tree.key) > 0)
            {
                tree.right = Add(key, value, tree.right);

                if ((Height(tree.right) - Height(tree.left) == 2))
                {
                    //此时是右右旋转
                    if (key.CompareTo(tree.right.key) > 0)
                    {
                        tree = RotateRR(tree);
                    }
                    else
                    {
                        //属于右左旋转
                        tree = RotateRL(tree);
                    }
                }
            }

            //将value追加到附加值中(也可对应重复元素)
            if (key.CompareTo(tree.key) == 0)
                tree.attach.Add(value);

            //计算高度
            tree.height = Math.Max(Height(tree.left), Height(tree.right)) + 1;

            return tree;
        }
        #endregion

4:删除

删除方法跟添加方法也类似,当删除一个结点的时候,可能会引起祖先结点的失衡,所以在每次”结点“回退的时候计算结点高度。

#region 删除当前树中的节点
        /// <summary>
        /// 删除当前树中的节点
        /// </summary>
        /// <param name="key"></param>
        /// <param name="tree"></param>
        /// <returns></returns>
        public AVLNode<K, V> Remove(K key, V value, AVLNode<K, V> tree)
        {
            if (tree == null)
                return null;

            //左子树
            if (key.CompareTo(tree.key) < 0)
            {
                tree.left = Remove(key, value, tree.left);

                //如果说相差等于2就说明这棵树需要旋转了
                if (Height(tree.left) - Height(tree.right) == 2)
                {
                    //说明此时是左左旋转
                    if (key.CompareTo(tree.left.key) < 0)
                    {
                        tree = RotateLL(tree);
                    }
                    else
                    {
                        //属于左右旋转
                        tree = RotateLR(tree);
                    }
                }
            }
            //右子树
            if (key.CompareTo(tree.key) > 0)
            {
                tree.right = Remove(key, value, tree.right);

                if ((Height(tree.right) - Height(tree.left) == 2))
                {
                    //此时是右右旋转
                    if (key.CompareTo(tree.right.key) > 0)
                    {
                        tree = RotateRR(tree);
                    }
                    else
                    {
                        //属于右左旋转
                        tree = RotateRL(tree);
                    }
                }
            }
            /*相等的情况*/
            if (key.CompareTo(tree.key) == 0)
            {
                //判断里面的HashSet是否有多值
                if (tree.attach.Count > 1)
                {
                    //实现惰性删除
                    tree.attach.Remove(value);
                }
                else
                {
                    //有两个孩子的情况
                    if (tree.left != null && tree.right != null)
                    {
                        //根据平衡二叉树的中顺遍历,需要找到”有子树“的最小节点
                        tree.key = FindMin(tree.right).key;

                        //删除右子树的指定元素
                        tree.right = Remove(tree.key, value, tree.right);
                    }
                    else
                    {
                        //自减高度
                        tree = tree.left == null ? tree.right : tree.left;

                        //如果删除的是叶子节点直接返回
                        if (tree == null)
                            return null;
                    }
                }
            }

            //统计高度
            tree.height = Math.Max(Height(tree.left), Height(tree.right)) + 1;

            return tree;
        }
        #endregion

5: 测试

不像上一篇不能在二叉树中灌有序数据,平衡二叉树就没关系了,我们的需求是检索2012-7-30 4:00:00 到 2012-7-30 5:00:00

的登陆用户的ID,数据量在500w,看看平衡二叉树是如何秒杀对手。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO;
using System.Diagnostics;

namespace DataStruct
{
    class Program
    {
        static void Main(string[] args)
        {
            AVLTree<int, int> avl = new AVLTree<int, int>();

            Dictionary<DateTime, int> dic = new Dictionary<DateTime, int>();

            AVLTree<DateTime, int> tree = new AVLTree<DateTime, int>();

            //500w
            for (int i = 1; i < 5000000; i++)
            {
                dic.Add(DateTime.Now.AddMinutes(i), i);

                tree.Add(DateTime.Now.AddMinutes(i), i);
            }

            //检索2012-7-30 4:00:00 到 2012-7-30 5:00:00的登陆人数
            var min = Convert.ToDateTime("2012/7/30 4:00:00");

            var max = Convert.ToDateTime("2012/7/30 5:00:00");

            var watch = Stopwatch.StartNew();

            var result1 = dic.Keys.Where(i => i >= min && i <= max).Select(i => dic[i]).ToList();

            watch.Stop();

            Console.WriteLine("字典查找耗费时间:{0}ms", watch.ElapsedMilliseconds);

            watch = Stopwatch.StartNew();

            var result2 = tree.SearchRange(min, max);

            watch.Stop();

            Console.WriteLine("平衡二叉树查找耗费时间:{0}ms", watch.ElapsedMilliseconds);
        }
    }

    #region 平衡二叉树节点
    /// <summary>
    /// 平衡二叉树节点
    /// </summary>
    /// <typeparam name="K"></typeparam>
    /// <typeparam name="V"></typeparam>
    public class AVLNode<K, V>
    {
        /// <summary>
        /// 节点元素
        /// </summary>
        public K key;

        /// <summary>
        /// 增加一个高度信息
        /// </summary>
        public int height;

        /// <summary>
        /// 节点中的附加值
        /// </summary>
        public HashSet<V> attach = new HashSet<V>();

        /// <summary>
        /// 左节点
        /// </summary>
        public AVLNode<K, V> left;

        /// <summary>
        /// 右节点
        /// </summary>
        public AVLNode<K, V> right;

        public AVLNode() { }

        public AVLNode(K key, V value, AVLNode<K, V> left, AVLNode<K, V> right)
        {
            //KV键值对
            this.key = key;
            this.attach.Add(value);

            this.left = left;
            this.right = right;
        }
    }
    #endregion

    public class AVLTree<K, V> where K : IComparable
    {
        public AVLNode<K, V> node = null;

        #region 添加操作
        /// <summary>
        /// 添加操作
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void Add(K key, V value)
        {
            node = Add(key, value, node);
        }
        #endregion

        #region 添加操作
        /// <summary>
        /// 添加操作
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="tree"></param>
        /// <returns></returns>
        public AVLNode<K, V> Add(K key, V value, AVLNode<K, V> tree)
        {
            if (tree == null)
                tree = new AVLNode<K, V>(key, value, null, null);

            //左子树
            if (key.CompareTo(tree.key) < 0)
            {
                tree.left = Add(key, value, tree.left);

                //如果说相差等于2就说明这棵树需要旋转了
                if (Height(tree.left) - Height(tree.right) == 2)
                {
                    //说明此时是左左旋转
                    if (key.CompareTo(tree.left.key) < 0)
                    {
                        tree = RotateLL(tree);
                    }
                    else
                    {
                        //属于左右旋转
                        tree = RotateLR(tree);
                    }
                }
            }

            //右子树
            if (key.CompareTo(tree.key) > 0)
            {
                tree.right = Add(key, value, tree.right);

                if ((Height(tree.right) - Height(tree.left) == 2))
                {
                    //此时是右右旋转
                    if (key.CompareTo(tree.right.key) > 0)
                    {
                        tree = RotateRR(tree);
                    }
                    else
                    {
                        //属于右左旋转
                        tree = RotateRL(tree);
                    }
                }
            }

            //将value追加到附加值中(也可对应重复元素)
            if (key.CompareTo(tree.key) == 0)
                tree.attach.Add(value);

            //计算高度
            tree.height = Math.Max(Height(tree.left), Height(tree.right)) + 1;

            return tree;
        }
        #endregion

        #region 计算当前节点的高度
        /// <summary>
        /// 计算当前节点的高度
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        public int Height(AVLNode<K, V> node)
        {
            return node == null ? -1 : node.height;
        }
        #endregion

        #region 第一种:左左旋转(单旋转)
        /// <summary>
        /// 第一种:左左旋转(单旋转)
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        public AVLNode<K, V> RotateLL(AVLNode<K, V> node)
        {
            //top:需要作为顶级节点的元素
            var top = node.left;

            //先截断当前节点的左孩子
            node.left = top.right;

            //将当前节点作为temp的右孩子
            top.right = node;

            //计算当前两个节点的高度
            node.height = Math.Max(Height(node.left), Height(node.right)) + 1;
            top.height = Math.Max(Height(top.left), Height(top.right)) + 1;

            return top;
        }
        #endregion

        #region 第二种:右右旋转(单旋转)
        /// <summary>
        /// 第二种:右右旋转(单旋转)
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        public AVLNode<K, V> RotateRR(AVLNode<K, V> node)
        {
            //top:需要作为顶级节点的元素
            var top = node.right;

            //先截断当前节点的右孩子
            node.right = top.left;

            //将当前节点作为temp的右孩子
            top.left = node;

            //计算当前两个节点的高度
            node.height = Math.Max(Height(node.left), Height(node.right)) + 1;
            top.height = Math.Max(Height(top.left), Height(top.right)) + 1;

            return top;
        }
        #endregion

        #region 第三种:左右旋转(双旋转)
        /// <summary>
        /// 第三种:左右旋转(双旋转)
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        public AVLNode<K, V> RotateLR(AVLNode<K, V> node)
        {
            //先进行RR旋转
            node.left = RotateRR(node.left);

            //再进行LL旋转
            return RotateLL(node);
        }
        #endregion

        #region 第四种:右左旋转(双旋转)
        /// <summary>
        /// 第四种:右左旋转(双旋转)
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        public AVLNode<K, V> RotateRL(AVLNode<K, V> node)
        {
            //执行左左旋转
            node.right = RotateLL(node.right);

            //再执行右右旋转
            return RotateRR(node);

        }
        #endregion

        #region 是否包含指定元素
        /// <summary>
        /// 是否包含指定元素
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public bool Contain(K key)
        {
            return Contain(key, node);
        }
        #endregion

        #region 是否包含指定元素
        /// <summary>
        /// 是否包含指定元素
        /// </summary>
        /// <param name="key"></param>
        /// <param name="tree"></param>
        /// <returns></returns>
        public bool Contain(K key, AVLNode<K, V> tree)
        {
            if (tree == null)
                return false;
            //左子树
            if (key.CompareTo(tree.key) < 0)
                return Contain(key, tree.left);

            //右子树
            if (key.CompareTo(tree.key) > 0)
                return Contain(key, tree.right);

            return true;
        }
        #endregion

        #region 树的指定范围查找
        /// <summary>
        /// 树的指定范围查找
        /// </summary>
        /// <param name="min"></param>
        /// <param name="max"></param>
        /// <returns></returns>
        public HashSet<V> SearchRange(K min, K max)
        {
            HashSet<V> hashSet = new HashSet<V>();

            hashSet = SearchRange(min, max, hashSet, node);

            return hashSet;
        }
        #endregion

        #region 树的指定范围查找
        /// <summary>
        /// 树的指定范围查找
        /// </summary>
        /// <param name="range1"></param>
        /// <param name="range2"></param>
        /// <param name="tree"></param>
        /// <returns></returns>
        public HashSet<V> SearchRange(K min, K max, HashSet<V> hashSet, AVLNode<K, V> tree)
        {
            if (tree == null)
                return hashSet;

            //遍历左子树(寻找下界)
            if (min.CompareTo(tree.key) < 0)
                SearchRange(min, max, hashSet, tree.left);

            //当前节点是否在选定范围内
            if (min.CompareTo(tree.key) <= 0 && max.CompareTo(tree.key) >= 0)
            {
                //等于这种情况
                foreach (var item in tree.attach)
                    hashSet.Add(item);
            }

            //遍历右子树(两种情况:①:找min的下限 ②:必须在Max范围之内)
            if (min.CompareTo(tree.key) > 0 || max.CompareTo(tree.key) > 0)
                SearchRange(min, max, hashSet, tree.right);

            return hashSet;
        }
        #endregion

        #region 找到当前树的最小节点
        /// <summary>
        /// 找到当前树的最小节点
        /// </summary>
        /// <returns></returns>
        public AVLNode<K, V> FindMin()
        {
            return FindMin(node);
        }
        #endregion

        #region 找到当前树的最小节点
        /// <summary>
        /// 找到当前树的最小节点
        /// </summary>
        /// <param name="tree"></param>
        /// <returns></returns>
        public AVLNode<K, V> FindMin(AVLNode<K, V> tree)
        {
            if (tree == null)
                return null;

            if (tree.left == null)
                return tree;

            return FindMin(tree.left);
        }
        #endregion

        #region 找到当前树的最大节点
        /// <summary>
        /// 找到当前树的最大节点
        /// </summary>
        /// <returns></returns>
        public AVLNode<K, V> FindMax()
        {
            return FindMin(node);
        }
        #endregion

        #region 找到当前树的最大节点
        /// <summary>
        /// 找到当前树的最大节点
        /// </summary>
        /// <param name="tree"></param>
        /// <returns></returns>
        public AVLNode<K, V> FindMax(AVLNode<K, V> tree)
        {
            if (tree == null)
                return null;

            if (tree.right == null)
                return tree;

            return FindMax(tree.right);
        }
        #endregion

        #region 删除当前树中的节点
        /// <summary>
        /// 删除当前树中的节点
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public void Remove(K key, V value)
        {
            node = Remove(key, value, node);
        }
        #endregion

        #region 删除当前树中的节点
        /// <summary>
        /// 删除当前树中的节点
        /// </summary>
        /// <param name="key"></param>
        /// <param name="tree"></param>
        /// <returns></returns>
        public AVLNode<K, V> Remove(K key, V value, AVLNode<K, V> tree)
        {
            if (tree == null)
                return null;

            //左子树
            if (key.CompareTo(tree.key) < 0)
            {
                tree.left = Remove(key, value, tree.left);

                //如果说相差等于2就说明这棵树需要旋转了
                if (Height(tree.left) - Height(tree.right) == 2)
                {
                    //说明此时是左左旋转
                    if (key.CompareTo(tree.left.key) < 0)
                    {
                        tree = RotateLL(tree);
                    }
                    else
                    {
                        //属于左右旋转
                        tree = RotateLR(tree);
                    }
                }
            }
            //右子树
            if (key.CompareTo(tree.key) > 0)
            {
                tree.right = Remove(key, value, tree.right);

                if ((Height(tree.right) - Height(tree.left) == 2))
                {
                    //此时是右右旋转
                    if (key.CompareTo(tree.right.key) > 0)
                    {
                        tree = RotateRR(tree);
                    }
                    else
                    {
                        //属于右左旋转
                        tree = RotateRL(tree);
                    }
                }
            }
            /*相等的情况*/
            if (key.CompareTo(tree.key) == 0)
            {
                //判断里面的HashSet是否有多值
                if (tree.attach.Count > 1)
                {
                    //实现惰性删除
                    tree.attach.Remove(value);
                }
                else
                {
                    //有两个孩子的情况
                    if (tree.left != null && tree.right != null)
                    {
                        //根据平衡二叉树的中顺遍历,需要找到”有子树“的最小节点
                        tree.key = FindMin(tree.right).key;

                        //删除右子树的指定元素
                        tree.right = Remove(tree.key, value, tree.right);
                    }
                    else
                    {
                        //自减高度
                        tree = tree.left == null ? tree.right : tree.left;

                        //如果删除的是叶子节点直接返回
                        if (tree == null)
                            return null;
                    }
                }
            }

            //统计高度
            tree.height = Math.Max(Height(tree.left), Height(tree.right)) + 1;

            return tree;
        }
        #endregion
    }
}

wow,相差98倍,这个可不是一个级别啊...AVL神器。

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 什么是二叉树? 二叉树是一种树形结构,其中每个节点最多有两个子节点。一个节点的左子节点比该节点小,右子节点比该节点大。二叉树通常用于搜索和排序。 2. 二叉树的遍历方法有哪些? 二叉树的遍历方法包括前序遍历、中序遍历和后序遍历。前序遍历是从根节点开始遍历,先访问根节点,再访问左子树,最后访问右子树。中序遍历是从根节点开始遍历,先访问左子树,再访问根节点,最后访问右子树。后序遍历是从根节点开始遍历,先访问左子树,再访问右子树,最后访问根节点。 3. 二叉树的查找方法有哪些? 二叉树的查找方法包括递归查找和非递归查找。递归查找是从根节点开始查找,如果当前节点的值等于要查找的值,则返回当前节点。如果要查找的值比当前节点小,则继续在左子树中查找;如果要查找的值比当前节点大,则继续在右子树中查找。非递归查找可以使用栈或队列实现,从根节点开始,每次将当前节点的左右子节点入栈/队列,直到找到要查找的值或者栈/队列为空。 4. 二叉树的插入与删除操作如何实现? 二叉树的插入操作是将要插入的节点与当前节点的值进行比较,如果小于当前节点的值,则继续在左子树中插入;如果大于当前节点的值,则继续在右子树中插入。当找到一个空节点时,就将要插入的节点作为该空节点的子节点。删除操作需要分为三种情况:删除叶子节点、删除只有一个子节点的节点和删除有两个子节点的节点。删除叶子节点很简单,只需要将其父节点的对应子节点置为空即可。删除只有一个子节点的节点,需要将其子节点替换为该节点的位置。删除有两个子节点的节点,则可以找到该节点的后继节点(即右子树中最小的节点),将其替换为该节点,然后删除后继节点。 5. 什么是平衡二叉树平衡二叉树是一种特殊的二叉树,它保证左右子树的高度差不超过1。这种平衡可以确保二叉树的查找、插入和删除操作的时间复杂度都是O(logn)。常见的平衡二叉树包括红黑树和AVL树。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值